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 }
);
});
});