Skip to content

Commit c459cb9

Browse files
committed
04/03: add problem and solution texts
1 parent 2d50c8e commit c459cb9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1308
-40
lines changed

exercises/04.debugging/01.problem.dom-snapshots/README.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ While you can observe and analyze certain actions in a test, it's a bit tricker
99

1010
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!
1111

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.
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 <InlineFile file="./src/tic-tac-toe.browser.test.tsx">`tic-tac-toe.browser.test.tsx`</InlineFile> test suite and have the tests passing.
1313

1414
---
1515

exercises/04.debugging/01.solution.dom-snapshots/README.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
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:
44

5-
```tsx filename=debug.browser.test.tsx
5+
```tsx filename=tic-tac-toe.browser.test.tsx
66
test.only('places cross marks in a horizontal line', async () => {
77
```
88
99
Now that the test results represent only this failing test, I can move on with taking a look at what's going on here.
1010
1111
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:
1212
13-
```tsx filename=debug.browser.test.tsx
13+
```tsx filename=tic-tac-toe.browser.test.tsx
1414
await page.getByRole('button', { name: 'left middle' }).click()
1515
await page.getByRole('button', { name: 'middle', exact: true }).click()
1616
await page.getByRole('button', { name: 'right middle' }).click()
@@ -30,7 +30,7 @@ The rightmost mark is missing! But how? The test is clearly marking the left mid
3030
3131
Luckily, I can observe the full state of the rendered DOM at any point in time using the `debug()` utility returned from `render()`:
3232
33-
```tsx filename=debug.browser.test.tsx add=1,7
33+
```tsx filename=tic-tac-toe.browser.test.tsx add=1,7
3434
const { debug } = render(<TicTacToe />)
3535

3636
await page.getByRole('button', { name: 'left middle' }).click()

exercises/04.debugging/02.solution.debugger/README.mdx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Using debugger allows me to step through my test wherever and whenever I need to. It would be really helpful to pause after my test marks a cell in the `<TicTacToe />` component. I can do that by placing a `debugger` statement after the corresponding actions in the test:
44

5-
```tsx filename=debug.browser.test.tsx nonumber nocopy highlight=5,8,11
5+
```tsx filename=tic-tac-toe.browser.test.tsx nonumber nocopy highlight=5,8,11
66
test('places cross marks in a horizontal line', async () => {
77
render(<TicTacToe />)
88

@@ -26,24 +26,11 @@ Running this test via the "Debug Vitest Browser" will now stop the test executio
2626

2727
Every `debugger` statement _creates a breakpoint_. Once the JavaScript engine reaches that breakpoint, its execution will pause until it's resumed. I can inspect the DOM, styles, JavaScript execution, and the test state as if the time has froze now!
2828

29-
## Adding breakpoints
30-
31-
Placing a `debugger` statement anywhere in your code or test is not the only way to create a breakpoint. In fact, you might've noticed that putting the `debugger` in our test makes us _modify that test_!
32-
33-
That isn't nice. Instead, you can add a breakpoint through your IDE.
34-
35-
1. Locate the line of code you want to set a breakpoint at;
36-
1. Either click on the red circle on the gutter next to that line or right-click the gutter and select "Add a breakpoint":
37-
38-
![A screenshot of Visual Studio code showing two red circles on the gutter next to the code, and an open context menu with the debugging options](/assets/04-02-add-breakpoint.png)
39-
40-
> 🦉 Note that adding a breakpoint on a line will pause the execution **before** that line runs, not after. If you want to pause after a certian action, put a breakpoint after it in the code.
41-
4229
## `--inspect-brk` vs `--inspect`
4330

4431
One more noteworthy thing it the flags we are using to create a debugging process, specifically, the `--inspect-brk` flag.
4532

46-
You may provide both `--inspect` and `--inspect-brk` flags to Vitest, but they behave differently. `--inspect-brk` will make Vitest _automatically add a breakpoint on every `render()`_, while `--inspect` will not.
33+
You may provide both `--inspect` and `--inspect-brk` flags to Vitest, but they behave differently. `--inspect-brk` will make Vitest _automatically add a breakpoint before every `render()`_, while `--inspect` will not.
4734

4835
## What's next?
4936

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"type": "node-terminal",
6+
"name": "Run Vitest Browser",
7+
"request": "launch",
8+
"command": "npm test -- --inspect-brk --browser --no-file-parallelism",
9+
"env": {
10+
"DEBUG": "true"
11+
}
12+
},
13+
{
14+
"type": "chrome",
15+
"request": "attach",
16+
"name": "Attach to Vitest Browser",
17+
"port": 9229
18+
}
19+
],
20+
"compounds": [
21+
{
22+
"name": "Debug Vitest Browser",
23+
"configurations": ["Attach to Vitest Browser", "Run Vitest Browser"],
24+
"stopAll": true
25+
}
26+
]
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Breakpoints
2+
3+
Placing a `debugger` statement anywhere in your code or test is not the only way to create a breakpoint. In fact, you might've noticed that putting the `debugger` in our test makes us _modify that test_!
4+
5+
That isn't nice. Instead, you can add a breakpoint through your IDE.
6+
7+
1. Locate the line of code you want to set a breakpoint at;
8+
1. Either click on the red circle on the gutter next to that line or right-click the gutter and select "Add a breakpoint":
9+
10+
![A screenshot of Visual Studio code showing two red circles on the gutter next to the code, and an open context menu with the debugging options](/assets/04-03-add-breakpoint.png)
11+
12+
> 🦉 Note that adding a breakpoint on a line will pause the execution **before** that line runs, not after. If you want to pause after a certian action, put a breakpoint after it in the code.
13+
14+
👨‍💼 In this exercise, I will have a few tasks for you.
15+
16+
1. Replace the `debugger` statements in the <InlineFile file="./src/tic-tac-toe-browser.test.tsx">`./src/`tic-tac-toe-browser.test.tsx`</InlineFile> with breakpoints set in your IDE. Run the tests in the debug mode to verify that breakpoints actually stop the test run where you want them to.
17+
1. Complete the test case in <InlineFile file="./src/main-menu.browser.test.tsx">`./src/`main-menu.browser.test.tsx`</InlineFile>. This is a new component rendering a menu in our app. It looks simple enough but there's something odd about those active links... Once you finish the test, run it in the debug mode and try to see how you can use conditional breakpoints to get to the root cause of the issue.
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_03.problem.breakpoints",
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: 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+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { page } from '@vitest/browser/context'
2+
import { render } from 'vitest-browser-react'
3+
import { MemoryRouter } from 'react-router'
4+
import { MainMenu } from './main-menu.js'
5+
6+
test('renders the currently active menu link', async () => {
7+
// 🐨 Render the <MainMenu /> component.
8+
// Use a custom `wrapper` and `<MemoryRouter />` from "react-router"
9+
// so the rendered component can access routing-related data.
10+
//
11+
// 🐨 Create a variable called `allLinks` and assign it the result
12+
// of locating all elements with the role "link".
13+
// 💰 page.getByRole(role).elements()
14+
//
15+
// 🐨 Create another variable called `currentPageLink`.
16+
// For its value, try to find a link element from `allLinks`
17+
// whose "aria-current" attribute equals to "page".
18+
// 💰 element.getAttribute(attributeName)
19+
//
20+
// 🐨 Finally, write an assertion that the `currentPageLink`
21+
// has acessible name that equals to "Analytics".
22+
// await expect.element(locator).toHaveAccessibleName(name)
23+
//
24+
// Once you run this test, you will notice it's failing.
25+
// 🐨 See what's wrong by adding a conditional breakpoint
26+
// somewhere during the <MenuItemsList /> rendering.
27+
// For the condition, use the title of the wrong active link.
28+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { matchPath, NavLink, useLocation } from 'react-router'
2+
3+
interface MenuItem {
4+
title: string
5+
url: string
6+
children?: Array<MenuItem>
7+
}
8+
9+
const menuItems: Array<MenuItem> = [
10+
{
11+
title: 'Docs',
12+
url: '/docs',
13+
},
14+
{
15+
title: 'Dashboard',
16+
url: '/dashboard',
17+
children: [
18+
{
19+
title: 'Profile',
20+
url: '/dashboard/profile',
21+
},
22+
{
23+
title: 'Analytics',
24+
url: '/dashboard/analytics',
25+
},
26+
{
27+
title: 'Settings',
28+
url: '/dashboard/settings',
29+
},
30+
],
31+
},
32+
] as const
33+
34+
function MenuItemsList(props: { items: Array<MenuItem> }) {
35+
const location = useLocation()
36+
37+
return (
38+
<ul className="ml-4">
39+
{props.items.map((item) => {
40+
const isActive = matchPath(
41+
{ path: item.url, end: false },
42+
location.pathname,
43+
)
44+
45+
return (
46+
<li key={item.url}>
47+
<NavLink
48+
to={item.url}
49+
className={[
50+
'px-2 py-1 hover:text-blue-600 hover:underline',
51+
isActive ? 'font-bold text-black' : 'text-gray-600',
52+
].join(' ')}
53+
>
54+
{item.title}
55+
</NavLink>
56+
{item.children ? <MenuItemsList items={item.children} /> : null}
57+
</li>
58+
)
59+
})}
60+
</ul>
61+
)
62+
}
63+
64+
export function MainMenu() {
65+
return <MenuItemsList items={menuItems} />
66+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import './index.css'
4+
import { App } from './app.jsx'
5+
6+
createRoot(document.getElementById('root')!).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>,
10+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
render(<TicTacToe />)
7+
8+
await page.getByRole('button', { name: 'left middle' }).click()
9+
debugger
10+
11+
await page.getByRole('button', { name: 'middle', exact: true }).click()
12+
debugger
13+
14+
await page.getByRole('button', { name: 'right middle' }).click()
15+
debugger
16+
17+
const squares = page.getByRole('button').elements().slice(3, 6)
18+
expect(squares.map((element) => element.textContent)).toEqual(['✗', '✗', '✗'])
19+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useState } from 'react'
2+
3+
export function TicTacToe() {
4+
return (
5+
<div className="grid grid-cols-3 grid-rows-3">
6+
<Button aria-label="top left" className="border-l-0 border-t-0" />
7+
<Button aria-label="top middle" className="border-t-0" />
8+
<Button aria-label="top right" className="border-r-0 border-t-0" />
9+
<Button aria-label="left middle" className="border-l-0" />
10+
<Button aria-label="middle" />
11+
<Button aria-label="bottom left" className="border-r-0" />
12+
<Button aria-label="right middle" className="border-b-0 border-l-0" />
13+
<Button aria-label="bottom middle" className="border-b-0" />
14+
<Button aria-label="bottom right" className="border-b-0 border-r-0" />
15+
</div>
16+
)
17+
}
18+
19+
function Button(
20+
props: Omit<
21+
React.DetailedHTMLProps<
22+
React.ButtonHTMLAttributes<HTMLButtonElement>,
23+
HTMLButtonElement
24+
>,
25+
'children' | 'onClick'
26+
>,
27+
) {
28+
const [selected, setSelected] = useState(false)
29+
const text = selected ? '✗' : ''
30+
31+
return (
32+
<button
33+
{...props}
34+
className={[
35+
'size-16 border border-slate-400 text-4xl text-blue-500 hover:bg-slate-200',
36+
]
37+
.concat(props.className || '')
38+
.join(' ')}
39+
onClick={() => setSelected(!selected)}
40+
>
41+
{text}
42+
</button>
43+
)
44+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Config } from 'tailwindcss'
2+
3+
export default {
4+
content: ['./index.html', './src/**/*.{ts,tsx}'],
5+
theme: {
6+
extend: {
7+
keyframes: {
8+
'slide-in': {
9+
'0%': { transform: 'translateY(50%)' },
10+
'100%': { transform: 'translateY(0)' },
11+
},
12+
},
13+
animation: {
14+
'slide-in': 'slide-in .4s',
15+
},
16+
},
17+
},
18+
plugins: [],
19+
} satisfies Config

0 commit comments

Comments
 (0)