Skip to content

Commit ea52fe9

Browse files
committed
03/01: add newsletter form
1 parent bb6ff28 commit ea52fe9

23 files changed

+565
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ workspace/
88
data.db
99
/playground
1010
**/tsconfig.tsbuildinfo
11+
__screenshots__
1112

1213
# in a real app you'd want to not commit the .env
1314
# file as well, but since this is for a workshop
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
# Accessibility selectors
22

33
Problem: don't riddle your code with implementation details like `data-testId`. Select elements on the page like the user would actually see them—using the ARIA roles.
4+
5+
- We've already used `page.getByText('file.txt')` in our test, which is great! Let's explore more accessible selectors you can use when testing your React components.
6+
7+
A **form** should work nicely:
8+
9+
- `.getByRole('heading')` for the form section heading on the page.
10+
- `.getByLabelText()` for inputs.
11+
- `.getByRole('button')` (again) for the submit button, but this time the button's text is broken by a visual element (an icon).
12+
13+
Best practices:
14+
15+
- Avoid `.getByTestId()`
16+
- Prefer `.getByLabelText()` vs `.getByPlaceholder()`
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_01.solution.accessibility-selectors",
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.2",
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.2"
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 { NewsletterForm } from './newsletter-form.js'
2+
3+
export function App() {
4+
return <NewsletterForm />
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: 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { test, expect } from 'vitest'
2+
import { page, userEvent } from '@vitest/browser/context'
3+
import { render } from 'vitest-browser-react'
4+
import { NewsletterForm } from './newsletter-form.js'
5+
6+
userEvent.setup()
7+
8+
test('submits the newsletter form', async () => {
9+
render(<NewsletterForm />)
10+
11+
await expect
12+
.element(page.getByRole('heading', { name: 'Newsletter' }))
13+
.toBeVisible()
14+
15+
const emailInput = page.getByLabelText('Your email')
16+
await userEvent.fill(emailInput, '[email protected]')
17+
18+
/**
19+
* @note Vitest doesn't support the `normalize` options
20+
* you'd normally use to sanitize an element with a broken text.
21+
*/
22+
const submitButton = page.getByRole('button', {
23+
name: 'Join the newsletter',
24+
})
25+
await userEvent.click(submitButton)
26+
27+
/** @todo Assert */
28+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function NewsletterForm() {
2+
return (
3+
<section className="w-96 rounded-lg border bg-white p-10">
4+
<h2 className="mb-6 border-b pb-2.5 text-2xl font-bold">Newsletter</h2>
5+
<form className="flex flex-col gap-5">
6+
<div>
7+
<label htmlFor="email" className="mb-1 block">
8+
Your email
9+
</label>
10+
<input
11+
id="email"
12+
name="email"
13+
type="email"
14+
className="w-full rounded-md border px-2 py-1 focus:ring-4"
15+
placeholder="[email protected]"
16+
autoComplete="off"
17+
required
18+
/>
19+
</div>
20+
21+
<button
22+
type="submit"
23+
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-500 focus:ring-4"
24+
>
25+
Join the 📨 newsletter
26+
</button>
27+
</form>
28+
</section>
29+
)
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Config } from 'tailwindcss'
2+
3+
export default {
4+
content: ['./index.html', './src/**/*.{ts,tsx}'],
5+
theme: {
6+
extend: {},
7+
},
8+
plugins: [],
9+
} satisfies Config
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"module": "ESNext"
6+
},
7+
"include": ["src"],
8+
"exclude": ["**/*.test.ts*"]
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"useDefineForClassFields": true,
4+
"skipLibCheck": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
7+
/* Bundler mode */
8+
"moduleResolution": "bundler",
9+
"allowImportingTsExtensions": true,
10+
"isolatedModules": true,
11+
"moduleDetection": "force",
12+
"noEmit": true,
13+
"jsx": "react-jsx",
14+
"verbatimModuleSyntax": true,
15+
16+
/* Linting */
17+
"strict": true,
18+
"noUnusedLocals": true,
19+
"noUnusedParameters": true,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedSideEffectImports": true
22+
}
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"files": [],
3+
"references": [
4+
{ "path": "./tsconfig.app.json" },
5+
{ "path": "./tsconfig.node.json" },
6+
{ "path": "./tsconfig.test.unit.json" },
7+
{ "path": "./tsconfig.test.browser.json" }
8+
]
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"lib": ["ES2023"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
8+
/* Bundler mode */
9+
"moduleResolution": "bundler",
10+
"allowImportingTsExtensions": true,
11+
"isolatedModules": true,
12+
"moduleDetection": "force",
13+
"noEmit": true,
14+
15+
/* Linting */
16+
"strict": true,
17+
"noUnusedLocals": true,
18+
"noUnusedParameters": true,
19+
"noFallthroughCasesInSwitch": true,
20+
"noUncheckedSideEffectImports": true
21+
},
22+
"include": ["vite.config.ts", "vitest.browser.setup.ts"]
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["vite.config.ts", "**/*.browser.test.ts*"],
4+
"compilerOptions": {
5+
"target": "esnext",
6+
"module": "preserve",
7+
"types": ["vitest/globals", "@vitest/browser/providers/playwright"]
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["**/*.test.ts*"],
4+
"exclude": ["**/*.browser.test.ts*"],
5+
"compilerOptions": {
6+
"target": "esnext",
7+
"module": "preserve",
8+
"types": ["node", "vitest/globals"]
9+
}
10+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// <reference types="vitest" />
2+
import { defineConfig } from 'vite'
3+
import react from '@vitejs/plugin-react'
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
globals: true,
9+
workspace: [
10+
{
11+
test: {
12+
name: 'unit',
13+
include: ['**/*.test.ts'],
14+
exclude: ['**/*.browser.test.ts(x)?'],
15+
environment: 'node',
16+
},
17+
},
18+
{
19+
test: {
20+
name: 'browser',
21+
include: ['**/*.browser.test.ts(x)?'],
22+
browser: {
23+
enabled: true,
24+
headless: true,
25+
provider: 'playwright',
26+
instances: [
27+
{
28+
browser: 'chromium',
29+
setupFiles: ['./vitest.browser.setup.ts'],
30+
},
31+
],
32+
},
33+
},
34+
},
35+
],
36+
},
37+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference path="./src/vite-env.d.ts" />
2+
import './src/index.css'

0 commit comments

Comments
 (0)