|
| 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