diff --git a/assets/css/_map_page.scss b/assets/css/_map_page.scss index 8f8d3492f..c8e165814 100644 --- a/assets/css/_map_page.scss +++ b/assets/css/_map_page.scss @@ -96,7 +96,9 @@ $z-map-page-context: ( margin: 0.5rem auto 0; } -.c-search-display { +.c-map-page .c-search-display { + isolation: isolate; + z-index: -1; flex: 1 1 auto; overflow-y: scroll; padding: 0.625rem 1rem 0 1rem; diff --git a/assets/css/_street_view_button.scss b/assets/css/_street_view_button.scss index 04b405796..6623302c1 100644 --- a/assets/css/_street_view_button.scss +++ b/assets/css/_street_view_button.scss @@ -15,6 +15,10 @@ $street-view-button-text-color: $color-eggplant-50; /* Text Decoration */ text-decoration: none; + + span svg { + stroke: none; + } } /* Override `.leaflet-container a` */ diff --git a/assets/src/api.ts b/assets/src/api.ts index a4fdbb26c..88ba745c3 100644 --- a/assets/src/api.ts +++ b/assets/src/api.ts @@ -227,7 +227,7 @@ export const fetchLocationSearchResults = ( searchText: string ): Promise => checkedApiCall({ - url: `api/location_search/search?query=${searchText}`, + url: `api/location_search/search?query=${encodeURIComponent(searchText)}`, dataStruct: array(LocationSearchResultData), parser: nullableParser(locationSearchResultsFromData), defaultResult: [], @@ -250,7 +250,7 @@ export const fetchLocationSearchSuggestions = ( LocationSearchSuggestionData[], LocationSearchSuggestion[] | null >({ - url: `api/location_search/suggest?query=${searchText}`, + url: `api/location_search/suggest?query=${encodeURIComponent(searchText)}`, dataStruct: array(LocationSearchSuggestionData), parser: nullableParser(locationSearchSuggestionsFromData), defaultResult: null, diff --git a/assets/src/components/groupedAutocomplete.tsx b/assets/src/components/groupedAutocomplete.tsx index 1a0672d2d..8cb88abf3 100644 --- a/assets/src/components/groupedAutocomplete.tsx +++ b/assets/src/components/groupedAutocomplete.tsx @@ -20,6 +20,7 @@ import { Ghost, Vehicle } from "../realtime" import { clamp } from "../util/math" import { formatOperatorNameFromVehicle } from "../util/operatorFormatting" import { SocketContext } from "../contexts/socketContext" +import { useLocationSearchSuggestions } from "../hooks/useLocationSearchSuggestions" // #region Autocomplete Control // #region Cursor Reducer @@ -647,7 +648,8 @@ const GroupOptionList = (props: LabelledListProps) => ( */ interface GroupedAutocompleteFromSearchTextResultsProps extends GroupedAutocompleteControlRefProps, - Omit { + Omit, + GroupedAutocompleteFromSearchTextEventProps { /** * Text to search to populate the autocomplete options with. */ @@ -660,6 +662,9 @@ interface GroupedAutocompleteFromSearchTextResultsProps * Max number of options to render in a group. */ maxElementsPerGroup?: number +} + +export interface GroupedAutocompleteFromSearchTextEventProps { /** * Callback when a autocomplete vehicle option is selected. * @param selectedOption The selected option vehicle @@ -672,6 +677,17 @@ interface GroupedAutocompleteFromSearchTextResultsProps * type deduction. */ onSelectVehicleOption: (selectedOption: Vehicle | Ghost) => void + + /** + * Fired when a autocomplete option with a PlaceId is selected. + * @param selectedPlaceId Suggested PlaceId. + */ + onSelectedLocationId: (selectedPlaceId: string) => void + /** + * Fired when a autocomplete option without a PlaceId is selected. + * @param selectedLocationText Suggested Location Search Text. + */ + onSelectedLocationText: (selectedLocationText: string) => void } /** @@ -680,6 +696,8 @@ interface GroupedAutocompleteFromSearchTextResultsProps */ export const GroupedAutocompleteFromSearchTextResults = ({ onSelectVehicleOption: onSelectVehicleOptionProp, + onSelectedLocationId, + onSelectedLocationText, searchText, searchFilters, maxElementsPerGroup = 5, @@ -697,6 +715,10 @@ export const GroupedAutocompleteFromSearchTextResults = ({ maxElementsPerGroup ) + const locationResults = + useLocationSearchSuggestions(searchFilters.location ? searchText : null) || + [] + const onSelectVehicleOption = (selectedOption: Vehicle | Ghost) => () => { onSelectVehicleOptionProp(selectedOption) } @@ -732,6 +754,19 @@ export const GroupedAutocompleteFromSearchTextResults = ({ .slice(0, maxElementsPerGroup) .map((v) => autocompleteOption(v.runId, onSelectVehicleOption(v))) ), + autocompleteGroup( +

{searchPropertyDisplayConfig.location.name}

, + ...locationResults + .slice(0, maxElementsPerGroup) + .map(({ text, placeId }) => + autocompleteOption( + text, + placeId + ? () => onSelectedLocationId(placeId) + : () => onSelectedLocationText(text) + ) + ) + ), ].filter(({ group: { options } }) => options.length > 0) return diff --git a/assets/src/components/highlightedMatch.tsx b/assets/src/components/highlightedMatch.tsx index 3fbabe4f0..f88f77f47 100644 --- a/assets/src/components/highlightedMatch.tsx +++ b/assets/src/components/highlightedMatch.tsx @@ -19,13 +19,19 @@ export const HighlightedMatch = ({ ? new RegExp( "(" + [highlightText, ...highlightText.split(/\s+/)] - .map((s) => highlightRegex(s).source) + .map((s) => highlightRegex(s)) + .filter((r): r is RegExp => r !== null) + .map((r) => r.source) .join("|") + ")", "i" ) : highlightRegex(highlightText) + if (regexp === null) { + return <>{content} + } + return } @@ -61,8 +67,13 @@ const HighlightedMatchHelper = ({ ) } -const highlightRegex = (highlightText: string): RegExp => { +const highlightRegex = (highlightText: string): RegExp | null => { const stripped = filterToAlphanumeric(highlightText) const allowNonAlphanumeric = intersperseString(stripped, "[^0-9a-zA-Z]*") - return new RegExp(allowNonAlphanumeric, "i") + + if (allowNonAlphanumeric === "") { + return null + } else { + return new RegExp(allowNonAlphanumeric, "i") + } } diff --git a/assets/src/components/mapPage.tsx b/assets/src/components/mapPage.tsx index 2b20e7386..1121f2401 100644 --- a/assets/src/components/mapPage.tsx +++ b/assets/src/components/mapPage.tsx @@ -41,6 +41,7 @@ import { Socket } from "phoenix" import SearchResultsByCategory from "./mapPage/searchResultsByCategory" import { LocationSearchResult } from "../models/locationSearchResult" import LocationCard from "./mapPage/locationCard" +import { useLocationSearchResultById } from "../hooks/useLocationSearchResultById" const thereIsAnActiveSearch = ( vehicles: (Vehicle | Ghost)[] | null, @@ -176,9 +177,11 @@ const SelectedRoute = ({ const Selection = ({ selectedEntity, setSelection, + fetchedSelectedLocation, }: { selectedEntity: SelectedEntity setSelection: (selectedEntity: SelectedEntity | null) => void + fetchedSelectedLocation: LocationSearchResult | null }): ReactElement => { const [{ searchPageState }, dispatch] = useContext(StateDispatchContext) const selectRoutePattern = (routePattern: RoutePattern) => { @@ -233,11 +236,13 @@ const Selection = ({ selectedRoutePattern={selectedEntity} selectRoutePattern={selectRoutePattern} /> - ) : ( + ) : fetchedSelectedLocation ? ( + ) : ( + )} ) @@ -263,6 +268,17 @@ const MapPage = (): ReactElement => { ) // #endregion + const selectedLocationById = useLocationSearchResultById( + selectedEntity?.type === SelectedEntityType.LocationByPlaceId + ? selectedEntity.placeId + : null + ) + + const fetchedSelectedLocation = + selectedEntity?.type === SelectedEntityType.Location + ? selectedEntity.location + : selectedLocationById + const setVehicleSelection = useCallback( (selectedEntity: SelectedEntity | null) => { switch (selectedEntity?.type) { @@ -332,6 +348,7 @@ const MapPage = (): ReactElement => { ) : ( => { diff --git a/assets/src/components/mapPage/mapDisplay.tsx b/assets/src/components/mapPage/mapDisplay.tsx index f339d3a83..b089c6706 100644 --- a/assets/src/components/mapPage/mapDisplay.tsx +++ b/assets/src/components/mapPage/mapDisplay.tsx @@ -125,7 +125,8 @@ type LiveSelectedEntity = | null const useLiveSelectedEntity = ( - selectedEntity: SelectedEntity | null + selectedEntity: SelectedEntity | null, + fetchedSelectedLocation: LocationSearchResult | null ): LiveSelectedEntity => { const { socket } = useContext(SocketContext) @@ -146,6 +147,13 @@ const useLiveSelectedEntity = ( return selectedEntity // no live updates for route pattern case SelectedEntityType.Location: return selectedEntity + case SelectedEntityType.LocationByPlaceId: + return fetchedSelectedLocation + ? { + type: SelectedEntityType.Location, + location: fetchedSelectedLocation, + } + : null default: return null } @@ -369,13 +377,17 @@ const SelectionDataLayers = ({ selectedEntity, setSelection, setStateClasses, + fetchedSelectedLocation, }: { selectedEntity: SelectedEntity | null setSelection: (selectedEntity: SelectedEntity | null) => void setStateClasses: (classes: string | undefined) => void + fetchedSelectedLocation: LocationSearchResult | null }) => { - const liveSelectedEntity: LiveSelectedEntity | null = - useLiveSelectedEntity(selectedEntity) + const liveSelectedEntity: LiveSelectedEntity | null = useLiveSelectedEntity( + selectedEntity, + fetchedSelectedLocation + ) const routePatternIdentifier = routePatternIdentifierForSelection(liveSelectedEntity) @@ -452,10 +464,12 @@ const MapDisplay = ({ selectedEntity, setSelection, streetViewInitiallyEnabled = false, + fetchedSelectedLocation, }: { selectedEntity: SelectedEntity | null setSelection: (selectedEntity: SelectedEntity | null) => void streetViewInitiallyEnabled?: boolean + fetchedSelectedLocation: LocationSearchResult | null }) => { const stations = useStations() @@ -492,6 +506,7 @@ const MapDisplay = ({ selectedEntity={selectedEntity} setSelection={setSelection} setStateClasses={setStateClasses} + fetchedSelectedLocation={fetchedSelectedLocation} /> diff --git a/assets/src/components/searchForm.tsx b/assets/src/components/searchForm.tsx index 703043abd..9752aefca 100644 --- a/assets/src/components/searchForm.tsx +++ b/assets/src/components/searchForm.tsx @@ -14,7 +14,6 @@ import { isValidSearchText, searchPropertyDisplayConfig, } from "../models/searchQuery" -import { Ghost, Vehicle } from "../realtime" import { SelectedEntityType, setOldSearchProperty, @@ -26,6 +25,7 @@ import { import { CircleXIcon } from "./circleXIcon" import { GroupedAutocompleteControls, + GroupedAutocompleteFromSearchTextEventProps, GroupedAutocompleteFromSearchTextResults, autocompleteOption, } from "./groupedAutocomplete" @@ -68,7 +68,8 @@ type SearchFormEventProps = { } type SearchFormProps = SearchFormEventProps & - SearchFormConfigProps & { + SearchFormConfigProps & + GroupedAutocompleteFromSearchTextEventProps & { /** * Text to show in the search input box. */ @@ -86,10 +87,6 @@ type SearchFormProps = SearchFormEventProps & * Callback to run when {@link property} should be updated. */ onPropertyChange: (property: SearchPropertyQuery) => void - /** - * Callback to run when a autocomplete vehicle option is selected. - */ - onSelectVehicleOption: (selectedOption: Vehicle | Ghost) => void } const allFiltersOn: SearchFiltersState = { @@ -160,7 +157,8 @@ export const SearchForm = ({ onClear: onClearProp, onSubmit: onSubmitProp, onSelectVehicleOption, - + onSelectedLocationId, + onSelectedLocationText, showAutocomplete: showAutocompleteProp = true, }: SearchFormProps) => { const formSearchInput = useRef(null) @@ -287,6 +285,11 @@ export const SearchForm = ({ searchText={inputText} fallbackOption={autocompleteOption(inputText, onSubmit)} onSelectVehicleOption={onSelectVehicleOption} + onSelectedLocationId={onSelectedLocationId} + onSelectedLocationText={(text) => { + setAutocompleteEnabled(false) + onSelectedLocationText(text) + }} controllerRef={autocompleteController} onCursor={{ onCursorExitEdge: () => formSearchInput.current?.focus(), @@ -351,6 +354,18 @@ const SearchFormFromStateDispatchContext = ({ }) ) }} + onSelectedLocationId={(id) => { + dispatch( + setSelectedEntity({ + type: SelectedEntityType.LocationByPlaceId, + placeId: id, + }) + ) + }} + onSelectedLocationText={(text) => { + dispatch(setSearchText(text)) + dispatch(submitSearch()) + }} /> ) } diff --git a/assets/src/state/searchPageState.ts b/assets/src/state/searchPageState.ts index be6e9eb69..131bf8be3 100644 --- a/assets/src/state/searchPageState.ts +++ b/assets/src/state/searchPageState.ts @@ -16,6 +16,7 @@ export enum SelectedEntityType { Vehicle = 1, RoutePattern, Location, + LocationByPlaceId, } interface SelectedVehicleId { @@ -37,10 +38,16 @@ export interface SelectedLocation { location: LocationSearchResult } +export interface SelectedLocationByPlaceId { + type: SelectedEntityType.LocationByPlaceId + placeId: string +} + export type SelectedEntity = | SelectedVehicleId | SelectedRoutePattern | SelectedLocation + | SelectedLocationByPlaceId export interface SearchPageState { query: SearchQuery diff --git a/assets/tests/components/groupedAutocomplete.test.tsx b/assets/tests/components/groupedAutocomplete.test.tsx index 3b1f9482d..e3f424835 100644 --- a/assets/tests/components/groupedAutocomplete.test.tsx +++ b/assets/tests/components/groupedAutocomplete.test.tsx @@ -25,7 +25,8 @@ import { option, } from "../testHelpers/selectors/components/groupedAutocomplete" import { searchFiltersFactory } from "../factories/searchProperties" -import ghostFactory from "../factories/ghost" +import { useLocationSearchSuggestions } from "../../src/hooks/useLocationSearchSuggestions" +import locationSearchSuggestionFactory from "../factories/locationSearchSuggestion" jest.mock("../../src/hooks/useAutocompleteResults", () => ({ useAutocompleteResults: jest.fn().mockImplementation(() => ({ @@ -35,6 +36,10 @@ jest.mock("../../src/hooks/useAutocompleteResults", () => ({ })), })) +jest.mock("../../src/hooks/useLocationSearchSuggestions", () => ({ + useLocationSearchSuggestions: jest.fn().mockImplementation(() => []), +})) + describe("", () => { test("when rendered, should show results", () => { const onSelectOption = jest.fn() @@ -615,10 +620,14 @@ describe("", () => { const operatorsResultsGroup = optionGroup( searchPropertyDisplayConfig.operator.name ) + const locationResultsGroup = optionGroup( + searchPropertyDisplayConfig.location.name + ) test("when rendered, should show results", () => { const searchText = "12345" const [idVehicle, runVehicle, operatorVehicle] = vehicleFactory.buildList(3) + const locationSuggestion = locationSearchSuggestionFactory.build() ;(useAutocompleteResults as jest.Mock).mockImplementation( (_socket, text: string, _filters) => @@ -630,6 +639,9 @@ describe("", () => { }, }[text] || {}) ) + ;(useLocationSearchSuggestions as jest.Mock).mockImplementation(() => [ + locationSuggestion, + ]) render( ", () => { onSelectVehicleOption={() => {}} searchText={searchText} searchFilters={searchFiltersFactory.build()} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) // Render form and autocomplete results const autocompleteResults = listbox().get() - expect(getAllByRole(autocompleteResults, "group")).toHaveLength(3) + expect(getAllByRole(autocompleteResults, "group")).toHaveLength(4) const vehiclesResults = vehiclesResultsGroup.get() const runResults = runResultsGroup.get() const operatorsResults = operatorsResultsGroup.get() + const locationResults = locationResultsGroup.get() expect(option(idVehicle.label!).get(vehiclesResults)).toBeInTheDocument() @@ -658,6 +673,10 @@ describe("", () => { operatorsResults ) ).toBeInTheDocument() + + expect( + option(locationSuggestion.text).get(locationResults) + ).toBeInTheDocument() }) test("when rendered, should not show more than `maxElementsPerGroup` results", () => { @@ -674,6 +693,9 @@ describe("", () => { }, }[text] || {}) ) + ;(useLocationSearchSuggestions as jest.Mock).mockImplementation(() => + locationSearchSuggestionFactory.buildList(maxLength + 2) + ) render( ", () => { onSelectVehicleOption={() => {}} searchText={searchText} searchFilters={searchFiltersFactory.build()} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) // Render form and autocomplete results const autocompleteResults = listbox().get() - expect(getAllByRole(autocompleteResults, "group")).toHaveLength(1) + expect(getAllByRole(autocompleteResults, "group")).toHaveLength(2) const vehiclesResults = vehiclesResultsGroup.get() expect(vehiclesResults.children).toHaveLength(maxLength) - }) - - test("when rendered, should not show more than `maxElementsPerGroup` results", () => { - const searchText = "12345" - const maxLength = 5 - - ;(useAutocompleteResults as jest.Mock).mockImplementation( - (_socket, text: string, _) => - ({ - [searchText]: { - vehicle: [], - operator: [ - ...ghostFactory.buildList(maxLength), - ...vehicleFactory.buildList(maxLength), - ], - run: [], - }, - }[text] || {}) - ) - render( - {}} - searchText={searchText} - searchFilters={searchFiltersFactory.build()} - /> - ) + const locationResults = locationResultsGroup.get() - // Render form and autocomplete results - const autocompleteResults = listbox().get() - expect(getAllByRole(autocompleteResults, "group")).toHaveLength(1) - - const vehiclesResults = operatorsResultsGroup.get() - - expect(vehiclesResults.children).toHaveLength(maxLength) + expect(locationResults.children).toHaveLength(maxLength) }) test("when searchText changes, should show new results", () => { @@ -776,6 +766,8 @@ describe("", () => { onSelectVehicleOption={() => {}} searchText={searchText} searchFilters={searchFiltersFactory.build()} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) const { rerender } = render() @@ -813,6 +805,8 @@ describe("", () => { onSelectVehicleOption={() => {}} searchText={""} searchFilters={searchFiltersFactory.build()} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) const { rerender } = render() @@ -836,7 +830,6 @@ describe("", () => { expect(vehicleOption.get(vehicleOptions)).toBeInTheDocument() - expect(runOptions).not.toBeInTheDocument() expect(runOption.query()).not.toBeInTheDocument() }) @@ -866,6 +859,8 @@ describe("", () => { onSelectVehicleOption={() => {}} searchText={""} searchFilters={filters} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) const { rerender } = render( diff --git a/assets/tests/components/highlightedMatch.test.tsx b/assets/tests/components/highlightedMatch.test.tsx index c334efad0..2fb575be8 100644 --- a/assets/tests/components/highlightedMatch.test.tsx +++ b/assets/tests/components/highlightedMatch.test.tsx @@ -56,6 +56,14 @@ describe("HighlightedMatch", () => { expect(screen.getByText(content)).not.toHaveClass("highlighted") }) + test("renders the original content if highlight text only contains whitespace and symbols", () => { + const content = "test string" + + render() + + expect(screen.getByText(content)).not.toHaveClass("highlighted") + }) + test("when matching individual words, will still match the whole highlightText if possible", () => { const content = "test string" diff --git a/assets/tests/components/mapPage.test.tsx b/assets/tests/components/mapPage.test.tsx index 4ab1f9701..e12b97f53 100644 --- a/assets/tests/components/mapPage.test.tsx +++ b/assets/tests/components/mapPage.test.tsx @@ -69,6 +69,7 @@ import { import getTestGroups from "../../src/userTestGroups" import { TestGroups } from "../../src/userInTestGroup" import locationSearchResultFactory from "../factories/locationSearchResult" +import { useLocationSearchResultById } from "../../src/hooks/useLocationSearchResultById" jest.mock("../../src/hooks/useSearchResults", () => ({ __esModule: true, @@ -82,6 +83,12 @@ jest.mock("../../src/hooks/useLocationSearchResults", () => ({ useLocationSearchResults: jest.fn(() => null), })) +jest.mock("../../src/hooks/useLocationSearchResultById", () => ({ + __esModule: true, + default: jest.fn(() => null), + useLocationSearchResultById: jest.fn(() => null), +})) + jest.mock("../../src/hooks/usePatternsByIdForRoute", () => ({ __esModule: true, default: jest.fn(() => null), @@ -721,6 +728,42 @@ describe("", () => { expect(screen.getByTitle("Recenter Map")).toBeVisible() }) + test("Locations selected by ID result in the location card being visible", async () => { + jest.spyOn(global, "scrollTo").mockImplementationOnce(jest.fn()) + + const location = locationSearchResultFactory.build() + + ;(useLocationSearchResultById as jest.Mock).mockImplementation((id) => { + if (id === location.id) { + return location + } + return null + }) + + render( + + + + ) + + const mapSearchPanel = getMapSearchPanel() + expect(mapSearchPanel).toHaveClass("c-map-page__input-and-results--visible") + + const locationCard = screen.getByLabelText(location.name!) + expect(locationCard).toBeVisible() + }) + test("can collapse and un-collapse the search panel with the drawer tab", async () => { render() diff --git a/assets/tests/components/mapPage/mapDisplay.test.tsx b/assets/tests/components/mapPage/mapDisplay.test.tsx index ad3e7f83d..631b58a1d 100644 --- a/assets/tests/components/mapPage/mapDisplay.test.tsx +++ b/assets/tests/components/mapPage/mapDisplay.test.tsx @@ -42,6 +42,7 @@ import { zoomInButton } from "../../testHelpers/selectors/components/map" import { stopIcon } from "../../testHelpers/selectors/components/map/markers/stopIcon" import { routePropertiesCard } from "../../testHelpers/selectors/components/mapPage/routePropertiesCard" import { vehiclePropertiesCard } from "../../testHelpers/selectors/components/mapPage/vehiclePropertiesCard" +import locationSearchResultFactory from "../../factories/locationSearchResult" jest.mock("../../../src/hooks/usePatternsByIdForRoute", () => ({ __esModule: true, @@ -118,7 +119,11 @@ describe("", () => { ) const { container } = render( - + ) expect(getAllStationIcons(container)).toHaveLength(0) @@ -154,6 +159,7 @@ describe("", () => { vehicleId: vehicle.id, }} setSelection={mockSetSelection} + fetchedSelectedLocation={null} /> ) @@ -188,6 +194,7 @@ describe("", () => { vehicleId: vehicle.id, }} setSelection={mockSetSelection} + fetchedSelectedLocation={null} /> ) @@ -219,6 +226,7 @@ describe("", () => { vehicleId: vehicle.id, }} setSelection={setSelectedEntityMock} + fetchedSelectedLocation={null} /> ) @@ -257,6 +265,7 @@ describe("", () => { vehicleId: selectedVehicle.id, }} setSelection={jest.fn()} + fetchedSelectedLocation={null} /> ) @@ -288,6 +297,7 @@ describe("", () => { vehicleId: selectedVehicle.id, }} setSelection={jest.fn()} + fetchedSelectedLocation={null} /> ) @@ -320,6 +330,7 @@ describe("", () => { vehicleId: ghost.id, }} setSelection={jest.fn()} + fetchedSelectedLocation={null} /> ) @@ -351,6 +362,7 @@ describe("", () => { routePatternId: routePattern.id, }} setSelection={jest.fn()} + fetchedSelectedLocation={null} /> ) @@ -378,6 +390,7 @@ describe("", () => { routePatternId: "otherRoutePatternId", }} setSelection={jest.fn()} + fetchedSelectedLocation={null} /> ) @@ -385,6 +398,54 @@ describe("", () => { expect(routePropertiesCard.query()).not.toBeInTheDocument() }) }) + + describe("selection is a location", () => { + test("should display location marker", () => { + setHtmlWidthHeightForLeafletMap() + + const location = locationSearchResultFactory.build({ + name: "Location Name", + }) + + const { container } = render( + + ) + + expect( + container.querySelectorAll(".c-location-dot-icon") + ).toHaveLength(1) + }) + + test("should display location marker if location is fetched separately from autocomplete", () => { + setHtmlWidthHeightForLeafletMap() + + const location = locationSearchResultFactory.build({ + name: "Location Name", + }) + + const { container } = render( + + ) + + expect( + container.querySelectorAll(".c-location-dot-icon") + ).toHaveLength(1) + }) + }) }) }) @@ -419,6 +480,7 @@ describe("", () => { }} setSelection={mockSetSelection} streetViewInitiallyEnabled + fetchedSelectedLocation={null} /> ) @@ -486,6 +548,7 @@ describe("", () => { }} setSelection={mockSetSelection} streetViewInitiallyEnabled + fetchedSelectedLocation={null} /> ) diff --git a/assets/tests/components/searchForm.test.tsx b/assets/tests/components/searchForm.test.tsx index 4602a5e63..443a499df 100644 --- a/assets/tests/components/searchForm.test.tsx +++ b/assets/tests/components/searchForm.test.tsx @@ -38,6 +38,9 @@ import { useAutocompleteResults } from "../../src/hooks/useAutocompleteResults" import vehicleFactory from "../factories/vehicle" import { SearchPropertyQuery } from "../../src/models/searchQuery" import { formatOperatorName } from "../../src/util/operatorFormatting" +import locationSearchResultFactory from "../factories/locationSearchResult" +import { useLocationSearchSuggestions } from "../../src/hooks/useLocationSearchSuggestions" +import locationSearchSuggestionFactory from "../factories/locationSearchSuggestion" jest.mock("../../src/hooks/useAutocompleteResults", () => ({ useAutocompleteResults: jest.fn().mockImplementation(() => ({ @@ -47,6 +50,10 @@ jest.mock("../../src/hooks/useAutocompleteResults", () => ({ })), })) +jest.mock("../../src/hooks/useLocationSearchSuggestions", () => ({ + useLocationSearchSuggestions: jest.fn().mockImplementation(() => []), +})) + const mockDispatch = jest.fn() describe("SearchForm", () => { @@ -330,6 +337,8 @@ describe("SearchForm", () => { property="all" onPropertyChange={jest.fn()} onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) @@ -343,6 +352,8 @@ describe("SearchForm", () => { property="all" onPropertyChange={jest.fn()} onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) @@ -361,6 +372,8 @@ describe("SearchForm", () => { e.preventDefault() }} onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) @@ -405,6 +418,8 @@ describe("SearchForm", () => { e.preventDefault() }} onSelectVehicleOption={onSelectVehicleOption} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) @@ -413,6 +428,77 @@ describe("SearchForm", () => { expect(onSelectVehicleOption).toHaveBeenCalledWith(vehicle) }) + test("when a location autocomplete option is clicked, should fire event 'onSelectedLocationId'", async () => { + const onSelectLocationId = jest.fn() + const inputText = "123 Test St" + const location = locationSearchResultFactory.build({ + name: "Search Suggestion", + }) + const locationSuggestion = locationSearchSuggestionFactory.build({ + text: location.name!, + placeId: location.id, + }) + + ;(useLocationSearchSuggestions as jest.Mock).mockReturnValue([ + locationSuggestion, + ]) + + render( + { + e.preventDefault() + }} + onSelectVehicleOption={() => {}} + onSelectedLocationId={onSelectLocationId} + onSelectedLocationText={() => {}} + /> + ) + + await userEvent.click(autocompleteOption(location.name!).get()) + + expect(onSelectLocationId).toHaveBeenCalledWith(location.id) + }) + + test("when a text-only location autocomplete option is clicked, should fire event 'onSelectedLocationText' and close autocomplete", async () => { + const onSelectLocationText = jest.fn() + const inputText = "123 Test St" + const locationSuggestion = locationSearchSuggestionFactory.build({ + text: "Suggested Search Term", + placeId: null, + }) + + ;(useLocationSearchSuggestions as jest.Mock).mockReturnValue([ + locationSuggestion, + ]) + + render( + { + e.preventDefault() + }} + onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={onSelectLocationText} + /> + ) + + await userEvent.click(autocompleteOption(locationSuggestion.text).get()) + + expect(onSelectLocationText).toHaveBeenCalledWith(locationSuggestion.text) + + expect(autocompleteListbox().get()).not.toBeVisible() + }) + test("when a filter is applied, should not show disabled categories in autocomplete", async () => { const inputText = "123" const [vehicle, runVehicle] = vehicleFactory.buildList(2) @@ -442,6 +528,8 @@ describe("SearchForm", () => { property="run" onPropertyChange={jest.fn()} onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) @@ -477,6 +565,8 @@ describe("SearchForm", () => { property="all" onPropertyChange={jest.fn()} onSelectVehicleOption={() => {}} + onSelectedLocationId={() => {}} + onSelectedLocationText={() => {}} /> ) diff --git a/lib/realtime/vehicle_or_ghost.ex b/lib/realtime/vehicle_or_ghost.ex index bb119791c..24b89986f 100644 --- a/lib/realtime/vehicle_or_ghost.ex +++ b/lib/realtime/vehicle_or_ghost.ex @@ -11,7 +11,10 @@ defmodule Realtime.VehicleOrGhost do search_terms = text |> clean_for_matching |> Enum.reject(&(String.length(&1) < 2)) if search_terms == [] do - [] + %{ + matching_vehicles: [], + has_more_matches: false + } else take_limited_props_matching( sort_for_search_results(vehicles), diff --git a/test/realtime/vehicle_or_ghost_test.exs b/test/realtime/vehicle_or_ghost_test.exs index 731a902b8..790f7dc48 100644 --- a/test/realtime/vehicle_or_ghost_test.exs +++ b/test/realtime/vehicle_or_ghost_test.exs @@ -237,5 +237,16 @@ defmodule Realtime.VehicleOrGhostTest do %{text: "000", property: :all, limit: 5} ) end + + test "handles case with no search terms" do + assert %{ + matching_vehicles: [], + has_more_matches: false + } = + VehicleOrGhost.take_limited_matches( + [], + %{text: "", property: :all, limit: 5} + ) + end end end