Skip to content

Commit

Permalink
Provide autocomplete-related data for location search (#2177)
Browse files Browse the repository at this point in the history
* refactor: rename SearchResult to Place

* feat: return both suggestion text and place id if available

* feat: get location by ID

* refactor: get rid of single-call pipelines

* feat: useLocationSearchResultById hook

* feat: functionality for manipulating location search suggestions

* feat: fetchLocationSearchSuggestions function

* feat: useLocationSearchSuggestions hook

* chore: fix typos
  • Loading branch information
lemald authored Aug 10, 2023
1 parent ad300ac commit bf09ccc
Show file tree
Hide file tree
Showing 19 changed files with 540 additions and 39 deletions.
29 changes: 29 additions & 0 deletions assets/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ import * as Sentry from "@sentry/react"
import { LocationSearchResult } from "./models/locationSearchResult"
import {
LocationSearchResultData,
locationSearchResultFromData,
locationSearchResultsFromData,
} from "./models/locationSearchResultData"
import {
LocationSearchSuggestionData,
locationSearchSuggestionsFromData,
} from "./models/locationSearchSuggestionData"
import { LocationSearchSuggestion } from "./models/locationSearchSuggestion"

export interface RouteData {
id: string
Expand Down Expand Up @@ -227,6 +233,29 @@ export const fetchLocationSearchResults = (
defaultResult: [],
})

export const fetchLocationSearchResultById = (
placeId: string
): Promise<LocationSearchResult | null> =>
checkedApiCall<LocationSearchResultData, LocationSearchResult | null>({
url: `api/location_search/place/${placeId}`,
dataStruct: LocationSearchResultData,
parser: nullableParser(locationSearchResultFromData),
defaultResult: null,
})

export const fetchLocationSearchSuggestions = (
searchText: string
): Promise<LocationSearchSuggestion[] | null> =>
checkedApiCall<
LocationSearchSuggestionData[],
LocationSearchSuggestion[] | null
>({
url: `api/location_search/suggest?query=${searchText}`,
dataStruct: array(LocationSearchSuggestionData),
parser: nullableParser(locationSearchSuggestionsFromData),
defaultResult: null,
})

export const putNotificationReadState = (
newReadState: NotificationState,
notificationIds: NotificationId[]
Expand Down
31 changes: 31 additions & 0 deletions assets/src/hooks/useLocationSearchResultById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from "react"
import { fetchLocationSearchResultById } from "../api"
import { LocationSearchResult } from "../models/locationSearchResult"

export const useLocationSearchResultById = (
placeId: string | null
): LocationSearchResult | null => {
const [searchResult, setSearchResult] = useState<LocationSearchResult | null>(
null
)

useEffect(() => {
let shouldUpdate = true

if (placeId) {
fetchLocationSearchResultById(placeId).then((results) => {
if (shouldUpdate) {
setSearchResult(results)
}
})
} else {
setSearchResult(null)
}

return () => {
shouldUpdate = false
}
}, [placeId])

return searchResult
}
31 changes: 31 additions & 0 deletions assets/src/hooks/useLocationSearchSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from "react"
import { fetchLocationSearchSuggestions } from "../api"
import { LocationSearchSuggestion } from "../models/locationSearchSuggestion"

export const useLocationSearchSuggestions = (
text: string | null
): LocationSearchSuggestion[] | null => {
const [searchSuggestions, setSearchSuggestions] = useState<
LocationSearchSuggestion[] | null
>(null)

useEffect(() => {
let shouldUpdate = true

if (text) {
fetchLocationSearchSuggestions(text).then((results) => {
if (shouldUpdate) {
setSearchSuggestions(results)
}
})
} else {
setSearchSuggestions(null)
}

return () => {
shouldUpdate = false
}
}, [text])

return searchSuggestions
}
4 changes: 4 additions & 0 deletions assets/src/models/locationSearchSuggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface LocationSearchSuggestion {
text: string
placeId: string | null
}
23 changes: 23 additions & 0 deletions assets/src/models/locationSearchSuggestionData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Infer, nullable, string, type } from "superstruct"
import { LocationSearchSuggestion } from "./locationSearchSuggestion"

export const LocationSearchSuggestionData = type({
text: string(),
place_id: nullable(string()),
})
export type LocationSearchSuggestionData = Infer<
typeof LocationSearchSuggestionData
>

export const locationSearchSuggestionFromData = ({
text,
place_id,
}: LocationSearchSuggestionData): LocationSearchSuggestion => ({
text,
placeId: place_id,
})

export const locationSearchSuggestionsFromData = (
locationSearchSuggestionsData: LocationSearchSuggestionData[]
): LocationSearchSuggestion[] =>
locationSearchSuggestionsData.map(locationSearchSuggestionFromData)
54 changes: 54 additions & 0 deletions assets/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
fetchStations,
fetchRoutePatterns,
fetchLocationSearchResults,
fetchLocationSearchResultById,
fetchLocationSearchSuggestions,
} from "../src/api"
import routeFactory from "./factories/route"
import routeTabFactory from "./factories/routeTab"
Expand All @@ -25,6 +27,8 @@ import { LocationType } from "../src/models/stopData"
import * as Sentry from "@sentry/react"
import locationSearchResultDataFactory from "./factories/locationSearchResultData"
import locationSearchResultFactory from "./factories/locationSearchResult"
import locationSearchSuggestionDataFactory from "./factories/locationSearchSuggestionData"
import locationSearchSuggestionFactory from "./factories/locationSearchSuggestion"

jest.mock("@sentry/react", () => ({
__esModule: true,
Expand Down Expand Up @@ -764,6 +768,56 @@ describe("fetchLocationSearchResults", () => {
})
})

describe("fetchLocationSearchResultById", () => {
test("parses location returned", (done) => {
const result = locationSearchResultDataFactory.build({
name: "Some Landmark",
address: "123 Test St",
latitude: 1,
longitude: 2,
})

mockFetch(200, {
data: result,
})

fetchLocationSearchResultById("query").then((results) => {
expect(results).toEqual(
locationSearchResultFactory.build({
name: "Some Landmark",
address: "123 Test St",
latitude: 1,
longitude: 2,
})
)
done()
})
})
})

describe("fetchLocationSearchSuggestions", () => {
test("parses location search suggestions", (done) => {
const result = locationSearchSuggestionDataFactory.build({
text: "Some Landmark",
place_id: "test-place",
})

mockFetch(200, {
data: [result],
})

fetchLocationSearchSuggestions("query").then((result) => {
expect(result).toEqual([
locationSearchSuggestionFactory.build({
text: "Some Landmark",
placeId: "test-place",
}),
])
done()
})
})
})

describe("putUserSetting", () => {
test("uses PUT and CSRF token", () => {
mockFetch(200, "")
Expand Down
10 changes: 10 additions & 0 deletions assets/tests/factories/locationSearchSuggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Factory } from "fishery"
import { LocationSearchSuggestion } from "../../src/models/locationSearchSuggestion"

const locationSearchSuggestionFactory =
Factory.define<LocationSearchSuggestion>(({ sequence }) => ({
text: "Some Search Term",
placeId: `${sequence}`,
}))

export default locationSearchSuggestionFactory
10 changes: 10 additions & 0 deletions assets/tests/factories/locationSearchSuggestionData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Factory } from "fishery"
import { LocationSearchSuggestionData } from "../../src/models/locationSearchSuggestionData"

const locationSearchSuggestionDataFactory =
Factory.define<LocationSearchSuggestionData>(({ sequence }) => ({
text: "Some Search Term",
place_id: `${sequence}`,
}))

export default locationSearchSuggestionDataFactory
47 changes: 47 additions & 0 deletions assets/tests/hooks/useLocationSearchResultById.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useLocationSearchResultById } from "../../src/hooks/useLocationSearchResultById"
import { renderHook } from "@testing-library/react"
import * as Api from "../../src/api"
import { instantPromise } from "../testHelpers/mockHelpers"
import locationSearchResultFactory from "../factories/locationSearchResult"

jest.mock("../../src/api", () => ({
__esModule: true,

fetchLocationSearchResultById: jest.fn(() => new Promise(() => {})),
}))

describe("useLocationSearchResultById", () => {
test("returns null if no search query is given", () => {
const mockFetchLocationSearchResultById: jest.Mock =
Api.fetchLocationSearchResultById as jest.Mock

const { result } = renderHook(() => useLocationSearchResultById(null))

expect(result.current).toBeNull()
expect(mockFetchLocationSearchResultById).not.toHaveBeenCalled()
})

test("returns null while loading", () => {
const mockFetchLocationSearchResultById: jest.Mock =
Api.fetchLocationSearchResultById as jest.Mock

const { result } = renderHook(() => useLocationSearchResultById("place_id"))

expect(result.current).toBeNull()
expect(mockFetchLocationSearchResultById).toHaveBeenCalled()
})

test("returns results", () => {
const results = [locationSearchResultFactory.build()]
const mockFetchLocationSearchResultById: jest.Mock =
Api.fetchLocationSearchResultById as jest.Mock
mockFetchLocationSearchResultById.mockImplementationOnce(() =>
instantPromise(results)
)

const { result } = renderHook(() => useLocationSearchResultById("place_id"))

expect(result.current).toEqual(results)
expect(mockFetchLocationSearchResultById).toHaveBeenCalled()
})
})
51 changes: 51 additions & 0 deletions assets/tests/hooks/useLocationSearchSuggestions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useLocationSearchSuggestions } from "../../src/hooks/useLocationSearchSuggestions"
import { renderHook } from "@testing-library/react"
import * as Api from "../../src/api"
import { instantPromise } from "../testHelpers/mockHelpers"
import locationSearchSuggestionFactory from "../factories/locationSearchSuggestion"

jest.mock("../../src/api", () => ({
__esModule: true,

fetchLocationSearchSuggestions: jest.fn(() => new Promise(() => {})),
}))

describe("useLocationSearchSuggestions", () => {
test("returns null if no search query is given", () => {
const mockFetchLocationSearchSuggestions: jest.Mock =
Api.fetchLocationSearchSuggestions as jest.Mock

const { result } = renderHook(() => useLocationSearchSuggestions(null))

expect(result.current).toBeNull()
expect(mockFetchLocationSearchSuggestions).not.toHaveBeenCalled()
})

test("returns null while loading", () => {
const mockFetchLocationSearchSuggestions: jest.Mock =
Api.fetchLocationSearchSuggestions as jest.Mock

const { result } = renderHook(() =>
useLocationSearchSuggestions("search string")
)

expect(result.current).toBeNull()
expect(mockFetchLocationSearchSuggestions).toHaveBeenCalled()
})

test("returns results", () => {
const results = [locationSearchSuggestionFactory.build()]
const mockFetchLocationSearchSuggestions: jest.Mock =
Api.fetchLocationSearchSuggestions as jest.Mock
mockFetchLocationSearchSuggestions.mockImplementationOnce(() =>
instantPromise(results)
)

const { result } = renderHook(() =>
useLocationSearchSuggestions("search string")
)

expect(result.current).toEqual(results)
expect(mockFetchLocationSearchSuggestions).toHaveBeenCalled()
})
})
40 changes: 40 additions & 0 deletions assets/tests/models/locationSearchSuggestionData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
locationSearchSuggestionFromData,
locationSearchSuggestionsFromData,
} from "../../src/models/locationSearchSuggestionData"
import locationSearchSuggestionFactory from "../factories/locationSearchSuggestion"
import locationSearchSuggestionDataFactory from "../factories/locationSearchSuggestionData"

describe("locationSearchSuggestionFromData", () => {
test("passes supplied data through", () => {
const data = locationSearchSuggestionDataFactory.build({
text: "Some Landmark",
place_id: "test-id",
})

expect(locationSearchSuggestionFromData(data)).toEqual(
locationSearchSuggestionFactory.build({
text: "Some Landmark",
placeId: "test-id",
})
)
})
})

describe("locationSearchSuggestionsFromData", () => {
test("passes supplied data through", () => {
const data = [
locationSearchSuggestionDataFactory.build({
text: "Some Landmark",
place_id: "test-id",
}),
]

expect(locationSearchSuggestionsFromData(data)).toEqual([
locationSearchSuggestionFactory.build({
text: "Some Landmark",
placeId: "test-id",
}),
])
})
})
Loading

0 comments on commit bf09ccc

Please sign in to comment.