Skip to content

Commit e7dc460

Browse files
committed
03/04: migrate from google maps to nominatim api
1 parent 0f2aed7 commit e7dc460

File tree

9 files changed

+178
-54
lines changed

9 files changed

+178
-54
lines changed

exercises/03.guides/03.problem.recording-tests/app/components/user-dropdown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function UserDropdown() {
2828
>
2929
<Img
3030
className="size-8 rounded-full object-cover"
31+
role="presentation"
3132
alt={user.name ?? user.username}
3233
src={getUserImgSrc(user.image?.objectKey)}
3334
width={256}

exercises/03.guides/03.solution.recording-tests/README.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,31 @@
22

33
## Summary
44

5-
1.
5+
1. Install the Playwright extension in VS Code. Show where and how.
6+
2. Showcase the extension in the sidebar once it's installed. See how it merges with the same extension from Vitest. Neat!
7+
8+
---
9+
10+
1. Before recording the tests, check the "Show browser" option in the "Settings" of the extension.
11+
2. Run the test. You can see that PW spawned in a headful mode, but the browser window doesn't close once the test is done. I will use that to record!
12+
3. Right-click on the test case in the list of tests and "Go to Test". The test case opens. Put the cursor at the blank space before the test's end.
13+
4. Finally, in the extension sidebar, click "Record at cursor".
14+
15+
---
16+
17+
1. In the browser window, notice how an UI overlay appeared. It has the controls to help with recording my interactions with the app.
18+
2. I scroll to the "Name" field on the page and change the user's name to "John Doe".
19+
3. I click "Save changes" the same way the user would.
20+
4. Now, let's verify that the change has been applied. I click on the user profile at the top right corner, then go to the "Profile" page.
21+
5. I can see the updated user name, but now I need to help Playwright see it also. In the overlay panel, I choose "Assert visibility" and click on the heading element with the updated user name.
22+
23+
---
24+
25+
1. Let's take a look at the recorded test. See how all my actions have been recorded, and the visibility assertion has been translated to the `expect` call.
26+
2. I'm done with the recording, so all I have to do is close the browser window.
27+
3. Now, let's run this test. Here's the result.
28+
29+
---
30+
31+
1. The role of the recording. It's a tool to make the process faster not to _replace_ the process. Playwright won't always output the best locators, and sometimes your app will be to blame. If you lack proper accessibility attributes, Playwright will have no choice but to resort to locating the elements by their DOM order, for example, which is less than ideal.
32+
2. Use the recording as a draft of the test. Polish it (or the app), introduce additional behaviors, like assertion messages or retryable assertions, and have yourself a great test done much faster.

exercises/03.guides/04.solution.api-mocking/README.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# API mocking
22

3-
Good job! 👏
3+
1. Install `msw` and `@msw/playwright` dependencies.
4+
2. Head to `tests/text-extend.ts` and in the `Fixtures` interface, describe a new fixture called `network`. Use the type from the `@msw/playwright` package to annotate it.
5+
3. Implement the `network` fixture. Use `defineNetworkFixture()` from the package, provide it with the handlers and the browser `context`. Start, use, and stop, awaited. Then, set `{ auto: true }` because I want for API mocking to always be applied, even if this fixture isn't explicitly used in a test.
6+
4. To make this fixture more flexible, I will create an option fixture called `handlers`. Annotate it as `Array<AnyHandler>`, then implement it as `[[], { option: true }]`. This way, individual tests and even entire test projects can define their own list of request handlers to affect the network.
47

5-
Please wait for the others to finish so we could go through the solution to this exercise together.
8+
---
9+
10+
1. Now, let's use `network` in the test. Reference it and use `network.use()` to apply request handlers for the Google API requests I want to intercept.
11+
2. Respond with a mocked response (use the `HttpResponse` helper from `msw`).
12+
13+
---
614

715
## Related materials
816

exercises/03.guides/04.solution.api-mocking/app/components/forms.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export function ComboboxField({
213213
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
214214
inputProps: React.TextareaHTMLAttributes<HTMLInputElement> & { key: any }
215215
errors?: ListOfErrors
216-
options: Array<{ label: string; value: string }>
216+
options: Array<{ id: string | number; label: string; value: string }>
217217
className?: string
218218
}) {
219219
const fallbackId = useId()
@@ -230,7 +230,7 @@ export function ComboboxField({
230230
const [query, setQuery] = useState<string>('')
231231

232232
const [filtered, setFiltered] = useState<
233-
Array<{ label: string; value: string }>
233+
Array<{ id: string | number; label: string; value: string }>
234234
>([])
235235

236236
const filterOptions = useCallback(
@@ -285,7 +285,7 @@ export function ComboboxField({
285285
<CommandList>
286286
{filtered.map((option) => (
287287
<CommandItem
288-
key={option.value}
288+
key={option.id}
289289
value={option.value}
290290
onSelect={() => {
291291
control.change(option.value)

exercises/03.guides/04.solution.api-mocking/app/routes/users+/$username_+/__note-editor.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,11 @@ export const NoteEditorSchema = z.object({
5757
location: z.string().optional(),
5858
})
5959

60-
export type GoogleFindPlaceApiResponse = {
61-
candidates: Array<{
62-
place_id: string
63-
formatted_address: string
64-
}>
65-
}
60+
export type NominatimSearchResponse = Array<{
61+
addresstype: 'country' | 'city'
62+
place_id: number
63+
display_name: string
64+
}>
6665

6766
export function NoteEditor({
6867
note,
@@ -89,22 +88,23 @@ export function NoteEditor({
8988
const imageList = fields.images.getFieldList()
9089

9190
const { data: locationSuggestions } = useQuery<
92-
GoogleFindPlaceApiResponse | undefined
91+
NominatimSearchResponse | undefined
9392
>({
94-
queryKey: ['location-suggestions'],
93+
queryKey: ['location-suggestions', fields.location.value],
9594
queryFn: async ({ signal }) => {
9695
await new Promise((resolve) => setTimeout(resolve, 300))
9796

9897
if (!signal.aborted && fields.location.value) {
99-
const url = new URL(
100-
'https://maps.googleapis.com/maps/api/place/findplacefromtext/json',
101-
)
102-
url.searchParams.set('key', 'FOO')
103-
url.searchParams.set('input', fields.location.value)
104-
url.searchParams.set('inputtype', 'textquery')
105-
url.searchParams.set('fields', 'place_id,formatted_address')
98+
const url = new URL('https://nominatim.openstreetmap.org/search')
99+
url.searchParams.set('q', fields.location.value)
100+
url.searchParams.set('format', 'jsonv2')
106101
const response = await fetch(url)
107-
return response.json() as Promise<GoogleFindPlaceApiResponse>
102+
const locations = (await response.json()) as NominatimSearchResponse
103+
return locations.filter(
104+
(location) =>
105+
location['addresstype'] === 'city' ||
106+
location['addresstype'] === 'country',
107+
)
108108
}
109109
},
110110
enabled: !!fields.location.value,
@@ -144,9 +144,10 @@ export function NoteEditor({
144144
/>
145145
<ComboboxField
146146
options={
147-
locationSuggestions?.candidates.map((candidate) => ({
148-
label: candidate.formatted_address,
149-
value: candidate.formatted_address,
147+
locationSuggestions?.map((location) => ({
148+
id: location.place_id,
149+
label: location.display_name,
150+
value: location.display_name,
150151
})) || []
151152
}
152153
labelProps={{ children: 'Location' }}

exercises/03.guides/04.solution.api-mocking/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
"devDependencies": {
121121
"@epic-web/config": "^1.20.1",
122122
"@faker-js/faker": "^9.7.0",
123-
"@msw/playwright": "^0.4.2",
123+
"@msw/playwright": "^0.6.4",
124124
"@playwright/test": "^1.57.0",
125125
"@react-router/dev": "^7.5.3",
126126
"@sly-cli/sly": "^2.1.1",
@@ -149,7 +149,7 @@
149149
"eslint": "^9.26.0",
150150
"fs-extra": "^11.3.0",
151151
"jsdom": "^25.0.1",
152-
"msw": "^2.7.6",
152+
"msw": "^2.12.10",
153153
"npm-run-all": "^4.1.5",
154154
"playwright-persona": "^0.2.8",
155155
"prettier": "^3.5.3",

exercises/03.guides/04.solution.api-mocking/tests/e2e/notes-create.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { http, HttpResponse } from 'msw'
2-
import { type GoogleFindPlaceApiResponse } from '#app/routes/users+/$username_+/__note-editor.tsx'
2+
import { type NominatimSearchResponse } from '#app/routes/users+/$username_+/__note-editor.tsx'
33
import { test, expect } from '#tests/test-extend.ts'
44

55
test('displays location suggestions when creating a new note', async ({
@@ -9,21 +9,21 @@ test('displays location suggestions when creating a new note', async ({
99
page,
1010
}) => {
1111
network.use(
12-
http.get<never, never, GoogleFindPlaceApiResponse>(
13-
'https://maps.googleapis.com/maps/api/place/findplacefromtext/json',
12+
http.get<never, never, NominatimSearchResponse>(
13+
'https://nominatim.openstreetmap.org/search',
1414
() => {
15-
return HttpResponse.json({
16-
candidates: [
17-
{
18-
place_id: 'f02cda9e-d6d0-437b-9f82-1c5652e12af2',
19-
formatted_address: 'San Francisco',
20-
},
21-
{
22-
place_id: '77886276-dc77-4399-a39e-037378ac4c34',
23-
formatted_address: 'San Jose',
24-
},
25-
],
26-
})
15+
return HttpResponse.json([
16+
{
17+
place_id: 1,
18+
addresstype: 'city',
19+
display_name: 'San Francisco',
20+
},
21+
{
22+
place_id: 2,
23+
addresstype: 'city',
24+
display_name: 'San Jose',
25+
},
26+
])
2727
},
2828
),
2929
)

exercises/03.guides/04.solution.api-mocking/tests/test-extend.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { createNetworkFixture, type NetworkFixture } from '@msw/playwright'
1+
import { defineNetworkFixture, type NetworkFixture } from '@msw/playwright'
22
import { test as testBase, expect } from '@playwright/test'
3+
import { type AnyHandler } from 'msw'
34
import {
45
definePersona,
56
combinePersonas,
@@ -15,6 +16,7 @@ interface Fixtures {
1516
...args: Parameters<typeof href<T>>
1617
) => Promise<void>
1718
authenticate: AuthenticateFunction<[typeof user]>
19+
handlers: Array<AnyHandler>
1820
network: NetworkFixture
1921
}
2022

@@ -54,7 +56,19 @@ export const test = testBase.extend<Fixtures>({
5456
})
5557
},
5658
authenticate: combinePersonas(user),
57-
network: createNetworkFixture(),
59+
handlers: [[], { option: true }],
60+
network: [
61+
async ({ context, handlers }, use) => {
62+
const network = defineNetworkFixture({
63+
context,
64+
handlers,
65+
})
66+
await network.enable()
67+
await use(network)
68+
await network.disable()
69+
},
70+
{ auto: true },
71+
],
5872
})
5973

6074
export { expect }

0 commit comments

Comments
 (0)