import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import i18next from "i18next"; import { runInAction } from "mobx"; import { http, HttpResponse } from "msw"; import { I18nextProvider } from "react-i18next"; import { ThemeProvider } from "styled-components"; import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; import CatalogSearchProvider from "../../../lib/Models/SearchProviders/CatalogSearchProvider"; import MapboxSearchProvider, { MapboxGeocodingResponse } from "../../../lib/Models/SearchProviders/MapboxSearchProvider"; import NominatimSearchProvider from "../../../lib/Models/SearchProviders/NominatimSearchProvider"; import Terria from "../../../lib/Models/Terria"; import ViewState from "../../../lib/ReactViewModels/ViewState"; import SearchBoxAndResults from "../../../lib/ReactViews/Search/SearchBoxAndResults"; import { terriaTheme } from "../../../lib/ReactViews/StandardUserInterface"; import { worker } from "../../mocks/browser"; import { renderWithContexts } from "../withContext"; describe("SearchBoxAndResults", function () { let terria: Terria; let viewState: ViewState; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); viewState = new ViewState({ terria: terria }); terria.catalog.searchProvider = new CatalogSearchProvider( "catalog-search-provider", terria ); }); it("renders with an input(SearchBox), but no SearchInDataCatalog without showLocationSearchResults", function () { const searchText = "neko"; runInAction(() => { viewState.searchState.locationSearchText = searchText; viewState.searchState.showLocationSearchResults = false; }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); expect(searchBox).toBeVisible(); expect(searchBox).toHaveValue(searchText); expect( screen.queryByText("search.searchInDataCatalog") ).not.toBeInTheDocument(); }); it("renders with an input & SearchInDataCatalog when showLocationSearchResults", async () => { const searchText = "mochi"; runInAction(() => { viewState.searchState.locationSearchText = searchText; viewState.searchState.showLocationSearchResults = true; }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); expect(searchBox).toBeVisible(); expect(searchBox).toHaveValue(searchText); expect(screen.getByText("search.searchInDataCatalog")).toBeVisible(); }); it("renders with an input & no SearchInDataCatalog without catalogSearchProvider", function () { const searchText = "timmynook"; runInAction(() => { viewState.searchState.locationSearchText = searchText; viewState.searchState.showLocationSearchResults = true; viewState.terria.catalog.searchProvider = undefined; }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); expect(searchBox).toBeVisible(); expect(searchBox).toHaveValue(searchText); expect( screen.queryByText("search.searchInDataCatalog") ).not.toBeInTheDocument(); }); it("renders with an input & no SearchInDataCatalog when showSearchInCatalog is false", function () { const searchText = "timmynook"; runInAction(() => { viewState.searchState.locationSearchText = searchText; viewState.searchState.showLocationSearchResults = true; viewState.terria.searchBarModel.setTrait( CommonStrata.user, "showSearchInCatalog", false ); }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); expect(searchBox).toBeVisible(); expect(searchBox).toHaveValue(searchText); expect( screen.queryByText("search.searchInDataCatalog") ).not.toBeInTheDocument(); }); let nominatimProvider: NominatimSearchProvider; let mapboxProvider: MapboxSearchProvider; let nominatimSpy: jasmine.Spy; let mapboxSpy: jasmine.Spy; const i18n = i18next.createInstance({ lng: "spec", debug: false, resources: { spec: { translation: {} } } }); beforeEach(async () => { await i18n.init(); nominatimProvider = new NominatimSearchProvider("nominatim", terria); mapboxProvider = new MapboxSearchProvider("mapbox", terria); // @ts-expect-error: Accessing protected method for testing purposes nominatimSpy = spyOn(nominatimProvider, "doSearch").and.callThrough(); // @ts-expect-error: Accessing protected method for testing purposes mapboxSpy = spyOn(mapboxProvider, "doSearch").and.callThrough(); worker.use( http.get("https://nominatim.openstreetmap.org/search", ({ request }) => { const searchText = new URL(request.url).searchParams.get("q") || ""; return HttpResponse.json({ features: [ { properties: { display_name: `Nominatim result for ${searchText}`, importance: 0 }, geometry: { coordinates: [0, 0] }, bbox: [0, 0, 0, 0] } ] }); }), http.get( "https://api.mapbox.com/search/geocode/v6/forward", ({ request }) => { const searchText = new URL(request.url).searchParams.get("q"); return HttpResponse.json({ features: [ { properties: { full_address: `Mapbox result for ${searchText}` }, type: "Feature", geometry: { coordinates: [0, 0], type: "Point" } } ], type: "FeatureCollection" } satisfies MapboxGeocodingResponse); } ) ); }); afterEach(() => { mapboxSpy.calls.reset(); nominatimSpy.calls.reset(); // Clear the search providers after each test // @ts-expect-error: locationSearchProviders is a private property terria.searchBarModel.locationSearchProviders.clear(); }); it("should display search results from mapbox search provider", async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(mapboxProvider); }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); await user.type(searchBox, "test search"); await waitFor( () => { expect(screen.getByText("Mapbox result for")).toBeInTheDocument(); }, { timeout: 5000 } ); expect(mapboxProvider.searchResult.results.length).toBe(1); expect(mapboxSpy).toHaveBeenCalledTimes(1); }); it('should not trigger search for nominatim provider until "Enter" is pressed', async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(nominatimProvider); }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); await user.type(searchBox, "test search"); expect( screen.queryByText(/Nominatim result for.*/i) ).not.toBeInTheDocument(); await waitFor(() => { expect( screen.getByText("viewModels.enterToStartSearch") ).toBeInTheDocument(); }); expect(nominatimSpy).not.toHaveBeenCalled(); await user.keyboard("{Enter}"); await waitFor(() => { expect(screen.getByText(/Nominatim result for.*/i)).toBeInTheDocument(); }); expect(nominatimProvider.searchResult.results.length).toBe(1); expect(nominatimSpy).toHaveBeenCalledTimes(1); }); it("should cancel previous search when new search text is set", async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(nominatimProvider); terria.searchBarModel.addSearchProvider(mapboxProvider); }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); await user.type(searchBox, "first"); await user.clear(searchBox); await user.type(searchBox, "second"); await waitFor( () => { expect(screen.getByText("second")).toBeInTheDocument(); }, { timeout: 5000 } ); expect(screen.queryByText("first")).not.toBeInTheDocument(); }); it("should preserve individual provider state during searches", async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(nominatimProvider); terria.searchBarModel.addSearchProvider(mapboxProvider); }); renderWithContexts( , viewState ); await user.type(screen.getByRole("textbox"), "test query"); await waitFor( () => { expect( screen.getByText("viewModels.enterToStartSearch") ).toBeInTheDocument(); }, { timeout: 5000 } ); await waitFor( () => { expect(screen.getByText("Mapbox result for")).toBeInTheDocument(); }, { timeout: 5000 } ); await user.keyboard("{Enter}"); await waitFor( () => { expect(screen.getByText("Nominatim result for")).toBeInTheDocument(); }, { timeout: 5000 } ); await waitFor( () => { expect(screen.getByText("Mapbox result for")).toBeInTheDocument(); }, { timeout: 5000 } ); expect(nominatimSpy).toHaveBeenCalledTimes(1); expect(mapboxSpy).toHaveBeenCalledTimes(1); }); it("should clear results when search input is cleared", async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(nominatimProvider); terria.searchBarModel.addSearchProvider(mapboxProvider); }); renderWithContexts( , viewState ); await user.type(screen.getByRole("textbox"), "test"); await user.keyboard("{Enter}"); await waitFor(() => { expect(screen.getByText("Nominatim result for")).toBeInTheDocument(); }); await userEvent.clear(screen.getByRole("textbox")); await waitFor(() => { expect(viewState.searchState.showLocationSearchResults).toBe(false); }); expect(nominatimProvider.searchResult.results.length).toBe(0); expect(mapboxProvider.searchResult.results.length).toBe(0); }); it("should handle manual vs automatic search triggering correctly", async function () { const user = userEvent.setup(); runInAction(() => { terria.searchBarModel.addSearchProvider(mapboxProvider); // Has autocomplete enabled }); renderWithContexts( , viewState ); const searchBox = screen.getByRole("textbox"); await user.type(searchBox, "test"); expect(mapboxSpy).not.toHaveBeenCalled(); await user.keyboard("{Enter}"); await waitFor( () => { expect(mapboxSpy).toHaveBeenCalledTimes(1); }, { timeout: 8000 } ); }); });