Skip to content

Commit 14cf89f

Browse files
committed
04/01: solution
1 parent 48e1eee commit 14cf89f

23 files changed

+506
-18
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# DOM snapshots
22

3+
Your component tests consist of two parts:
4+
5+
- A rendered UI;
6+
- Test actions interacting with that UI.
7+
8+
While you can observe and analyze certain actions in a test, it's a bit tricker when it comes to debugging the rendered markup. For once, your UI components are not a 1-1 representation of the final DOM. There can be side effects and user actions that result in the elements appearing or disappearing on the page. Looking at the component's code alone won't cut it.
9+
10+
Sometimes you just need to take a look at the current state of the DOM _during_ the test. This is where DOM snapshots come in!
11+
12+
👨‍💼 In this exercise, your task is to **track down and fix a nasty bug** that found its way in the code. To do that, you will observe the rendered markup as it changes from the test actions, using a `debug()` utility function. Complete the instructions in the `debug.browser.test.tsx` test suite and have the tests passing.
13+
14+
---
15+
16+
This is where taking _spapshots_ comes in handy. At any point of your test, you can take a look at the current state of the DOM to see if it's what you expect it to be. You can do that by calling the `debug()` utility function returned from `render()`:
17+
18+
```tsx
19+
const { debug } = render(<p>Hello world</p>)
20+
debug()
21+
```
22+
23+
```html
24+
<body>
25+
<p>Hello world</p>
26+
</body>
27+
```
28+
29+
---
30+
331
- How to use `debug()` from `render()`.
432
- You can debug the entire rendered tree by calling `debug()`.
533
- You can debug a subportion of the tree but calling `debug(locator)` or even an array of locators.

exercises/04.debugging/01.problem.dom-snapshots/src/debug.browser.test.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,26 @@ test('places cross marks in a horizontal line', async () => {
2020
expect(squares.map((element) => element.textContent)).toEqual(['✗', '✗', '✗'])
2121
})
2222

23-
test('...', async () => {
24-
// A bunch of video thumbnails that render a lot of UI
25-
// but you are interested only in certain elements, like "Watch now" links.
26-
//
27-
// OR any kind of list with interactive elements like buttons next to each element.
28-
const { debug } = render(
29-
<VideoFeed>
30-
<Video />
31-
<Video />
32-
<Video />
33-
<Video />
34-
<Video />
35-
</VideoFeed>,
36-
)
23+
// test('...', async () => {
24+
// /**
25+
// * @fixme @todo
26+
// */
3727

38-
// A large component tree and we can print all elements
39-
// matching this role.
40-
// debug(page.getByRole('button').all())
41-
})
28+
// // A bunch of video thumbnails that render a lot of UI
29+
// // but you are interested only in certain elements, like "Watch now" links.
30+
// //
31+
// // OR any kind of list with interactive elements like buttons next to each element.
32+
// const { debug } = render(
33+
// <VideoFeed>
34+
// <Video />
35+
// <Video />
36+
// <Video />
37+
// <Video />
38+
// <Video />
39+
// </VideoFeed>,
40+
// )
41+
42+
// // A large component tree and we can print all elements
43+
// // matching this role.
44+
// // debug(page.getByRole('button').all())
45+
// })
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# DOM snapshots
2+
3+
I've got two failing tests on my hands, and a good place to start is to isolate the one I'm going to address first:
4+
5+
```tsx filename=debug.browser.test.tsx
6+
test.only('places cross marks in a horizontal line', async () => {
7+
```
8+
9+
Now that the test results represent only this failing test, I can move on with taking a look at what's going on here.
10+
11+
The goal of this test is to validate one of the scenarios of interacting with the `<TicTacToe />` component. If I place three marks in a horizontal line, they should form a winning line. This is precisely what the test actions do:
12+
13+
```tsx filename=debug.browser.test.tsx
14+
await page.getByRole('button', { name: 'left middle' }).click()
15+
await page.getByRole('button', { name: 'middle', exact: true }).click()
16+
await page.getByRole('button', { name: 'right middle' }).click()
17+
```
18+
19+
> 🦉 Notice the usage of `exact: true` in the second action. It is there so that `name: 'middle'` matches the element with the accessible name `'middle'` exactly, ignoring the `'left middle'` and `'right middle'` elements that would otherwise match, too.
20+
21+
The usage seems to be in order, but the test still fails:
22+
23+
```
24+
AssertionError: expected [ '', '', '' ] to deeply equal [ '', '', '' ]
25+
```
26+
27+
The rightmost mark is missing! But how? The test is clearly marking the left middle, middle, and the right middle squares of the game. Something is certainly off here...
28+
29+
## Printing the entire DOM
30+
31+
Luckily, I can observe the full state of the rendered DOM at any point in time using the `debug()` utility returned from `render()`:
32+
33+
```tsx filename=debug.browser.test.tsx add=1,7
34+
const { debug } = render(<TicTacToe />)
35+
36+
await page.getByRole('button', { name: 'left middle' }).click()
37+
await page.getByRole('button', { name: 'middle', exact: true }).click()
38+
await page.getByRole('button', { name: 'right middle' }).click()
39+
40+
debug()
41+
```
42+
43+
Calling the `debug()` function here will print out the entire DOM tree into the console after the test is done interacting with the `<TicTacToe />` component. This should, hopefully, give me some clue as to why that last check mark is missing.
44+
45+
```html highlight=18-29,34-39
46+
<body>
47+
<div>
48+
<div
49+
class="grid grid-cols-3 grid-rows-3"
50+
>
51+
<button
52+
aria-label="top left"
53+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-l-0 border-t-0"
54+
/>
55+
<button
56+
aria-label="top middle"
57+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-t-0"
58+
/>
59+
<button
60+
aria-label="top right"
61+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-r-0 border-t-0"
62+
/>
63+
<button
64+
aria-label="left middle"
65+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-l-0"
66+
>
67+
68+
</button>
69+
<button
70+
aria-label="middle"
71+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 "
72+
>
73+
74+
</button>
75+
<button
76+
aria-label="bottom left"
77+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-r-0"
78+
/>
79+
<button
80+
aria-label="right middle"
81+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-b-0 border-l-0"
82+
>
83+
84+
</button>
85+
<button
86+
aria-label="bottom middle"
87+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-b-0"
88+
/>
89+
<button
90+
aria-label="bottom right"
91+
class="size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200 border-b-0 border-r-0"
92+
/>
93+
</div>
94+
</div>
95+
</body
96+
```
97+
98+
Having a bird's eye view on the DOM lets me see whether everything is rendered correctly and . . . What do you know! The last check mark indeed gets placed, and even in the correct square, but the square itself is at the wrong place! Looks like the `'bottom left'` and `'right middle'` sections of the game got swapped by accident the other day 😅.
99+
100+
Hopefully, it's much easier to fix the bug than it was to find it:
101+
102+
```tsx filename=tic-tac-toe.tsx remove=3 add=5
103+
<Button aria-label="left middle" className="border-l-0" />
104+
<Button aria-label="middle" />
105+
<Button aria-label="bottom left" className="border-r-0" />
106+
<Button aria-label="right middle" className="border-b-0 border-l-0" />
107+
<Button aria-label="bottom left" className="border-r-0" />
108+
```
109+
110+
Alright! The first test is back to green. But what about the second one?
111+
112+
## Printing selected elements
113+
114+
Sometimes the rendered element tree is too large to digest all at once. This is where isolating it to a particular element or a set of elements is a huge time-saver to get to the bottom of the issue.
115+
116+
As it turns out, this is exactly the case for the second failing test.
117+
118+
> TODO: Complete the solution once the second failing test is there.
119+
120+
---
121+
122+
This is where taking _spapshots_ comes in handy. At any point of your test, you can take a look at the current state of the DOM to see if it's what you expect it to be. You can do that by calling the `debug()` utility function returned from `render()`:
123+
124+
```tsx
125+
const { debug } = render(<p>Hello world</p>)
126+
debug()
127+
```
128+
129+
```html
130+
<body>
131+
<p>Hello world</p>
132+
</body>
133+
```
134+
135+
---
136+
137+
- How to use `debug()` from `render()`.
138+
- You can debug the entire rendered tree by calling `debug()`.
139+
- You can debug a subportion of the tree but calling `debug(locator)` or even an array of locators.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite App</title>
8+
<link rel="preconnect" href="https://fonts.googleapis.com" />
9+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10+
</head>
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_04.debugging_01.solution.dom-snapshots",
4+
"scripts": {
5+
"dev": "vite",
6+
"test": "vitest"
7+
},
8+
"dependencies": {
9+
"react": "^19.0.0",
10+
"react-dom": "^19.0.0"
11+
},
12+
"devDependencies": {
13+
"@types/node": "^22.10.6",
14+
"@types/react": "^19.0.6",
15+
"@types/react-dom": "^19.0.3",
16+
"@vitejs/plugin-react": "^4.3.4",
17+
"@vitest/browser": "^3.0.4",
18+
"autoprefixer": "^10.4.20",
19+
"playwright": "^1.49.1",
20+
"postcss": "^8.4.49",
21+
"tailwindcss": "^3.4.17",
22+
"vite": "^6.0.7",
23+
"vitest": "^3.0.4"
24+
}
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TicTacToe } from './tic-tac-toe.js'
2+
3+
export function App() {
4+
return <TicTacToe />
5+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { page } from '@vitest/browser/context'
2+
import { render } from 'vitest-browser-react'
3+
import { TicTacToe } from './tic-tac-toe.js'
4+
5+
test('places cross marks in a horizontal line', async () => {
6+
const { debug } = render(<TicTacToe />)
7+
8+
await page.getByRole('button', { name: 'left middle' }).click()
9+
await page.getByRole('button', { name: 'middle', exact: true }).click()
10+
await page.getByRole('button', { name: 'right middle' }).click()
11+
12+
debug()
13+
14+
const squares = page.getByRole('button').elements().slice(3, 6)
15+
expect(squares.map((element) => element.textContent)).toEqual(['✗', '✗', '✗'])
16+
})
17+
18+
test('...', async () => {
19+
/**
20+
* @fixme @todo
21+
*/
22+
// A bunch of video thumbnails that render a lot of UI
23+
// but you are interested only in certain elements, like "Watch now" links.
24+
//
25+
// OR any kind of list with interactive elements like buttons next to each element.
26+
const { debug } = render(
27+
<VideoFeed>
28+
<Video />
29+
<Video />
30+
<Video />
31+
<Video />
32+
<Video />
33+
</VideoFeed>,
34+
)
35+
36+
// A large component tree and we can print all elements
37+
// matching this role.
38+
// debug(page.getByRole('button').all())
39+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap');
2+
3+
@tailwind base;
4+
@tailwind components;
5+
@tailwind utilities;
6+
7+
html {
8+
@apply flex size-full items-center justify-center bg-slate-100 p-10;
9+
10+
font-family:
11+
'DM Sans',
12+
system-ui,
13+
-apple-system,
14+
BlinkMacSystemFont,
15+
'Segoe UI',
16+
Roboto,
17+
Oxygen,
18+
Ubuntu,
19+
Cantarell,
20+
'Open Sans',
21+
'Helvetica Neue',
22+
sans-serif;
23+
font-style: normal;
24+
font-optical-sizing: auto;
25+
}

0 commit comments

Comments
 (0)