Skip to content

Commit 72af473

Browse files
committed
Create playwright-friendly-html.md
1 parent 9af4ec4 commit 72af473

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

docs/playwright-friendly-html.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Building Playwright-Friendly Web Pages
2+
3+
Tips for writing HTML that is easy to test and resilient to CSS/styling changes.
4+
5+
## Use `data-testid` attributes
6+
7+
The single most impactful thing you can do. Test IDs survive class renames, component refactors, and redesigns.
8+
9+
```html
10+
<!-- Good: stable test anchor -->
11+
<button data-testid="submit-registration">Register</button>
12+
13+
<!-- Fragile: breaks if class or text changes -->
14+
<button class="btn-primary text-sm rounded-lg">Register</button>
15+
```
16+
17+
```ts
18+
// In Playwright
19+
page.getByTestId('submit-registration')
20+
```
21+
22+
Playwright has built-in support via `getByTestId()`. You can configure the attribute name in `playwright.config.ts` if you prefer something like `data-test` or `data-cy`.
23+
24+
## Use semantic HTML and ARIA attributes
25+
26+
Semantic elements and ARIA roles give tests meaningful anchors that also improve accessibility.
27+
28+
```html
29+
<form aria-label="registration">
30+
<input name="username" aria-label="Username" />
31+
<button type="submit" aria-label="submit">Register</button>
32+
</form>
33+
```
34+
35+
```ts
36+
page.getByRole('form', { name: 'registration' })
37+
page.getByRole('textbox', { name: 'Username' })
38+
page.getByRole('button', { name: 'submit' })
39+
```
40+
41+
These selectors are independent of CSS classes, tag nesting, and visual layout.
42+
43+
## Use `name` attributes on form inputs
44+
45+
The `name` attribute is stable, functional (needed for form submission), and unlikely to change for cosmetic reasons.
46+
47+
```html
48+
<input name="first_name" />
49+
<input name="email" />
50+
```
51+
52+
```ts
53+
page.locator('input[name="first_name"]')
54+
page.locator('input[name="email"]')
55+
```
56+
57+
## Avoid selectors tied to styling
58+
59+
| Fragile | Why | Better alternative |
60+
|---|---|---|
61+
| `.btn-primary` | Styling class, changes with redesigns | `data-testid="submit-btn"` |
62+
| `.flex.items-center.space-x-3` | Layout utility classes | `data-testid="provider-row"` |
63+
| `div > div > button` | Structural coupling, breaks with refactors | `button[data-testid="..."]` |
64+
| `:nth-child(2)` | Position-dependent, breaks if order changes | `data-testid` on each item |
65+
66+
## Give distinct identities to repeated items
67+
68+
When you have lists of similar elements, give each one a unique test ID.
69+
70+
```html
71+
<!-- Good: each document row is identifiable -->
72+
<div data-testid="legal-doc-terms-of-service">
73+
<span>Terms of Service</span>
74+
<button data-testid="accept-terms-of-service">Read & Accept</button>
75+
</div>
76+
<div data-testid="legal-doc-privacy-policy">
77+
<span>Privacy Policy</span>
78+
<button data-testid="accept-privacy-policy">Read & Accept</button>
79+
</div>
80+
```
81+
82+
```ts
83+
// Direct, no ambiguity
84+
page.getByTestId('accept-privacy-policy').click()
85+
```
86+
87+
Without this, tests resort to fragile text matching or positional selectors to distinguish items.
88+
89+
## Mark key states with data attributes
90+
91+
Expose UI state in the DOM so tests can assert on it directly.
92+
93+
```html
94+
<div data-testid="role-checker" data-state="collapsed">...</div>
95+
<div data-testid="role-checker" data-state="expanded">...</div>
96+
```
97+
98+
```ts
99+
await expect(page.getByTestId('role-checker')).toHaveAttribute('data-state', 'expanded')
100+
```
101+
102+
This is more reliable than checking for CSS classes like `.expanded` which may be renamed.
103+
104+
## Summary
105+
106+
| Principle | Effect |
107+
|---|---|
108+
| Add `data-testid` to interactive and assertable elements | Tests don't break on styling changes |
109+
| Use semantic HTML + ARIA | Tests read like user intent, not DOM spelunking |
110+
| Use `name` on form fields | Stable, functional anchors |
111+
| Avoid class-based and structural selectors | Decouples tests from CSS and layout |
112+
| Give unique IDs to repeated items | Eliminates ambiguous selectors |
113+
| Expose state via data attributes | Clean assertions without class sniffing |

0 commit comments

Comments
 (0)