Skip to content

Commit 3f1dbce

Browse files
committed
03/02: add problem and solution text
1 parent 622662b commit 3f1dbce

22 files changed

+449
-4
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# User events
2+
3+
The way your test interacts with UI elements is just as important as the way it locates them. Naturally, your users don't dispatch events on elements or programmatically access them. They hover, click, type, drag. They do what their input devices allow them to do.
4+
5+
So, how do you bring your tests to do the same? For a long while, integration testing revolved around _simulating_ user events. That was mostly due to the environmental limitations as you cannot really interact with a UI element in Node.js.
6+
7+
**But your component tests aren't running in Node.js anymore**. Being in the real browser also means utlizing real user events. Actually hovering, clicking, typing, or dragging. One more benefit to reap from testing the code where it's supposed to run.
8+
9+
👨‍💼 This exercises has the following task for you in store: complete the automated test at <InlineFile file="./src/discount-code-form.browser.test.tsx">`discount-code-form.browser.test.tsx`</InlineFile> to enter a discount code, submit it, and assert that it has actually been applied (i.e. became visible as applied in the UI).
10+
11+
Give it your best and see you in the solution to this exercise!
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_03.best-practices_02.problem.user-events",
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+
"@testing-library/dom": "^10.4.0",
14+
"@testing-library/react": "^16.1.0",
15+
"@types/node": "^22.10.6",
16+
"@types/react": "^19.0.6",
17+
"@types/react-dom": "^19.0.3",
18+
"@vitejs/plugin-react": "^4.3.4",
19+
"@vitest/browser": "^3.0.3",
20+
"autoprefixer": "^10.4.20",
21+
"playwright": "^1.49.1",
22+
"postcss": "^8.4.49",
23+
"tailwindcss": "^3.4.17",
24+
"vite": "^6.0.7",
25+
"vitest": "^3.0.3"
26+
}
27+
}
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 { DiscountCodeForm } from './discount-code-form.js'
2+
3+
export function App() {
4+
return <DiscountCodeForm />
5+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { page } from '@vitest/browser/context'
2+
import { render } from 'vitest-browser-react'
3+
import { DiscountCodeForm } from './discount-code-form.js'
4+
5+
test('renders the discount form', async () => {
6+
render(<DiscountCodeForm />)
7+
8+
const discountInput = page.getByLabelText('Discount code')
9+
// 💣 Remove this visibility assertion.
10+
// If you are going to interact with an element, you don't have
11+
// to assert that it's visible/enabled/etc. Its interactive state
12+
// is implied by the interaction.
13+
await expect.element(discountInput).toBeVisible()
14+
15+
// 🐨 Fill the discount code "EPIC2025" into the discount code input.
16+
// 💰 await element.fill(value)
17+
18+
const applyDiscountButton = page.getByRole('button', {
19+
name: 'Apply discount',
20+
})
21+
// 💣 Similarly, remove this redundant assertion.
22+
await expect.element(applyDiscountButton).toBeVisible()
23+
24+
// 🐨 Submit the discount form by clicking on the button.
25+
// 💰 await element.click()
26+
//
27+
// 🐨 Finally, add an assertion that the element with the text
28+
// "Discount: EPIC2025 (-20%)" is visibile on the page.
29+
// 💰 await expect.element(locator).toBeVisible()
30+
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState, type FormEventHandler } from 'react'
2+
3+
export interface Discount {
4+
code: string
5+
amount: number
6+
}
7+
8+
export function DiscountCodeForm() {
9+
const [appliedDiscount, setAppliedDiscount] = useState<Discount>()
10+
11+
const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
12+
event.preventDefault()
13+
14+
const data = new FormData(event.currentTarget)
15+
const code = data.get('discountCode') as string
16+
17+
setAppliedDiscount({
18+
code,
19+
amount: 20,
20+
})
21+
}
22+
23+
return (
24+
<section className="w-96 rounded-lg border bg-white p-10">
25+
{appliedDiscount ? (
26+
<p>
27+
Discount: <strong>{appliedDiscount.code}</strong> (-
28+
{appliedDiscount.amount}%)
29+
</p>
30+
) : (
31+
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
32+
<div>
33+
<label htmlFor="discountCode" className="mb-1 block">
34+
Discount code
35+
</label>
36+
<input
37+
id="discountCode"
38+
name="discountCode"
39+
className="w-full rounded-md border px-2 py-1 focus:ring-4"
40+
placeholder="ABCD1234"
41+
pattern="[A-Z]{4}[0-9]{4}"
42+
autoComplete="off"
43+
required
44+
/>
45+
</div>
46+
47+
<button
48+
type="submit"
49+
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-500 focus:ring-4"
50+
>
51+
Apply discount
52+
</button>
53+
</form>
54+
)}
55+
</section>
56+
)
57+
}
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: 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+
)

0 commit comments

Comments
 (0)