@assertrx/dino 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,1056 @@
1
+ # Forms and Validation
2
+
3
+ > **When to use**: Testing form filling, submission, validation messages, multi-step wizards, dynamic fields, and auto-complete interactions.
4
+ > **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Text input
10
+ await page.getByLabel('Name').fill('Jane Doe');
11
+
12
+ // Select dropdown
13
+ await page.getByLabel('Country').selectOption('US');
14
+ await page.getByLabel('Country').selectOption({ label: 'United States' });
15
+
16
+ // Checkbox and radio
17
+ await page.getByLabel('Remember me').check();
18
+ await page.getByLabel('Express shipping').click();
19
+
20
+ // Date input
21
+ await page.getByLabel('Start date').fill('2025-03-15');
22
+
23
+ // Clear a field
24
+ await page.getByLabel('Name').clear();
25
+
26
+ // Submit
27
+ await page.getByRole('button', { name: 'Submit' }).click();
28
+
29
+ // Verify validation error
30
+ await expect(page.getByText('Email is required')).toBeVisible();
31
+ ```
32
+
33
+ ## Patterns
34
+
35
+ ### Filling Basic Form Fields
36
+
37
+ **Use when**: Testing any form with standard HTML inputs — text, email, password, number, textarea, select, checkbox, radio.
38
+ **Avoid when**: Never. This is the foundation pattern.
39
+
40
+ **TypeScript**
41
+ ```typescript
42
+ import { test, expect } from '@playwright/test';
43
+
44
+ test('fill and submit a registration form', async ({ page }) => {
45
+ await page.goto('/register');
46
+
47
+ // Text inputs — use fill() which clears first, not type()
48
+ await page.getByLabel('First name').fill('Jane');
49
+ await page.getByLabel('Last name').fill('Doe');
50
+ await page.getByLabel('Email').fill('jane@example.com');
51
+ await page.getByLabel('Password', { exact: true }).fill('S3cureP@ss!');
52
+ await page.getByLabel('Confirm password').fill('S3cureP@ss!');
53
+
54
+ // Textarea
55
+ await page.getByLabel('Bio').fill('Software engineer with 10 years of experience.');
56
+
57
+ // Number input
58
+ await page.getByLabel('Age').fill('32');
59
+
60
+ // Native <select>
61
+ await page.getByLabel('Country').selectOption('US');
62
+
63
+ // Select by visible label text (when value differs from display text)
64
+ await page.getByLabel('State').selectOption({ label: 'California' });
65
+
66
+ // Multi-select
67
+ await page.getByLabel('Interests').selectOption(['coding', 'testing', 'devops']);
68
+
69
+ // Checkbox — use check() not click() (idempotent: won't uncheck if already checked)
70
+ await page.getByLabel('I agree to the terms').check();
71
+ await expect(page.getByLabel('I agree to the terms')).toBeChecked();
72
+
73
+ // Radio button
74
+ await page.getByLabel('Monthly billing').check();
75
+ await expect(page.getByLabel('Monthly billing')).toBeChecked();
76
+
77
+ // Submit
78
+ await page.getByRole('button', { name: 'Create account' }).click();
79
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
80
+ });
81
+ ```
82
+
83
+ **JavaScript**
84
+ ```javascript
85
+ const { test, expect } = require('@playwright/test');
86
+
87
+ test('fill and submit a registration form', async ({ page }) => {
88
+ await page.goto('/register');
89
+
90
+ await page.getByLabel('First name').fill('Jane');
91
+ await page.getByLabel('Last name').fill('Doe');
92
+ await page.getByLabel('Email').fill('jane@example.com');
93
+ await page.getByLabel('Password', { exact: true }).fill('S3cureP@ss!');
94
+ await page.getByLabel('Confirm password').fill('S3cureP@ss!');
95
+
96
+ await page.getByLabel('Bio').fill('Software engineer with 10 years of experience.');
97
+ await page.getByLabel('Age').fill('32');
98
+ await page.getByLabel('Country').selectOption('US');
99
+ await page.getByLabel('State').selectOption({ label: 'California' });
100
+ await page.getByLabel('Interests').selectOption(['coding', 'testing', 'devops']);
101
+
102
+ await page.getByLabel('I agree to the terms').check();
103
+ await expect(page.getByLabel('I agree to the terms')).toBeChecked();
104
+
105
+ await page.getByLabel('Monthly billing').check();
106
+ await expect(page.getByLabel('Monthly billing')).toBeChecked();
107
+
108
+ await page.getByRole('button', { name: 'Create account' }).click();
109
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
110
+ });
111
+ ```
112
+
113
+ ### Date and Time Inputs
114
+
115
+ **Use when**: Testing native `<input type="date">`, `<input type="time">`, `<input type="datetime-local">`, or third-party date pickers.
116
+ **Avoid when**: The date picker is a simple text field with no special input type. Just use `fill()`.
117
+
118
+ **TypeScript**
119
+ ```typescript
120
+ import { test, expect } from '@playwright/test';
121
+
122
+ test('fill native date and time inputs', async ({ page }) => {
123
+ await page.goto('/booking');
124
+
125
+ // Native date input — use ISO format YYYY-MM-DD
126
+ await page.getByLabel('Check-in date').fill('2025-06-15');
127
+ await expect(page.getByLabel('Check-in date')).toHaveValue('2025-06-15');
128
+
129
+ // Native time input — use HH:MM format
130
+ await page.getByLabel('Arrival time').fill('14:30');
131
+
132
+ // datetime-local — use YYYY-MM-DDTHH:MM format
133
+ await page.getByLabel('Event start').fill('2025-06-15T09:00');
134
+ });
135
+
136
+ test('interact with a third-party date picker', async ({ page }) => {
137
+ await page.goto('/booking');
138
+
139
+ // Click to open the date picker
140
+ await page.getByLabel('Departure date').click();
141
+
142
+ // Navigate months if needed
143
+ await page.getByRole('button', { name: 'Next month' }).click();
144
+
145
+ // Select a specific day
146
+ await page.getByRole('gridcell', { name: '20' }).click();
147
+
148
+ // Verify the selected date appears in the input
149
+ await expect(page.getByLabel('Departure date')).toHaveValue(/2025/);
150
+ });
151
+ ```
152
+
153
+ **JavaScript**
154
+ ```javascript
155
+ const { test, expect } = require('@playwright/test');
156
+
157
+ test('fill native date and time inputs', async ({ page }) => {
158
+ await page.goto('/booking');
159
+
160
+ await page.getByLabel('Check-in date').fill('2025-06-15');
161
+ await expect(page.getByLabel('Check-in date')).toHaveValue('2025-06-15');
162
+
163
+ await page.getByLabel('Arrival time').fill('14:30');
164
+ await page.getByLabel('Event start').fill('2025-06-15T09:00');
165
+ });
166
+
167
+ test('interact with a third-party date picker', async ({ page }) => {
168
+ await page.goto('/booking');
169
+
170
+ await page.getByLabel('Departure date').click();
171
+ await page.getByRole('button', { name: 'Next month' }).click();
172
+ await page.getByRole('gridcell', { name: '20' }).click();
173
+
174
+ await expect(page.getByLabel('Departure date')).toHaveValue(/2025/);
175
+ });
176
+ ```
177
+
178
+ ### Required Field Validation
179
+
180
+ **Use when**: Testing that the form shows appropriate error messages when required fields are empty.
181
+ **Avoid when**: You only care about the happy path. Validation tests should complement, not replace, success path tests.
182
+
183
+ **TypeScript**
184
+ ```typescript
185
+ import { test, expect } from '@playwright/test';
186
+
187
+ test('shows validation errors for empty required fields', async ({ page }) => {
188
+ await page.goto('/contact');
189
+
190
+ // Submit without filling anything
191
+ await page.getByRole('button', { name: 'Send message' }).click();
192
+
193
+ // Verify all required field errors appear
194
+ await expect(page.getByText('Name is required')).toBeVisible();
195
+ await expect(page.getByText('Email is required')).toBeVisible();
196
+ await expect(page.getByText('Message is required')).toBeVisible();
197
+
198
+ // Verify the form was NOT submitted (still on the same page)
199
+ await expect(page).toHaveURL(/\/contact/);
200
+ });
201
+
202
+ test('clears validation errors when fields are filled', async ({ page }) => {
203
+ await page.goto('/contact');
204
+
205
+ // Trigger errors
206
+ await page.getByRole('button', { name: 'Send message' }).click();
207
+ await expect(page.getByText('Name is required')).toBeVisible();
208
+
209
+ // Fill the field — error should disappear
210
+ await page.getByLabel('Name').fill('Jane Doe');
211
+
212
+ // Use tab or click away to trigger blur validation
213
+ await page.getByLabel('Email').focus();
214
+
215
+ await expect(page.getByText('Name is required')).not.toBeVisible();
216
+ });
217
+
218
+ test('native HTML5 validation with required attribute', async ({ page }) => {
219
+ await page.goto('/simple-form');
220
+
221
+ await page.getByRole('button', { name: 'Submit' }).click();
222
+
223
+ // Check for native validation message via the :invalid pseudo-class
224
+ const emailInput = page.getByLabel('Email');
225
+ const validationMessage = await emailInput.evaluate(
226
+ (el: HTMLInputElement) => el.validationMessage
227
+ );
228
+ expect(validationMessage).toBeTruthy();
229
+ });
230
+ ```
231
+
232
+ **JavaScript**
233
+ ```javascript
234
+ const { test, expect } = require('@playwright/test');
235
+
236
+ test('shows validation errors for empty required fields', async ({ page }) => {
237
+ await page.goto('/contact');
238
+
239
+ await page.getByRole('button', { name: 'Send message' }).click();
240
+
241
+ await expect(page.getByText('Name is required')).toBeVisible();
242
+ await expect(page.getByText('Email is required')).toBeVisible();
243
+ await expect(page.getByText('Message is required')).toBeVisible();
244
+
245
+ await expect(page).toHaveURL(/\/contact/);
246
+ });
247
+
248
+ test('clears validation errors when fields are filled', async ({ page }) => {
249
+ await page.goto('/contact');
250
+
251
+ await page.getByRole('button', { name: 'Send message' }).click();
252
+ await expect(page.getByText('Name is required')).toBeVisible();
253
+
254
+ await page.getByLabel('Name').fill('Jane Doe');
255
+ await page.getByLabel('Email').focus();
256
+
257
+ await expect(page.getByText('Name is required')).not.toBeVisible();
258
+ });
259
+
260
+ test('native HTML5 validation with required attribute', async ({ page }) => {
261
+ await page.goto('/simple-form');
262
+
263
+ await page.getByRole('button', { name: 'Submit' }).click();
264
+
265
+ const emailInput = page.getByLabel('Email');
266
+ const validationMessage = await emailInput.evaluate(
267
+ (el) => el.validationMessage
268
+ );
269
+ expect(validationMessage).toBeTruthy();
270
+ });
271
+ ```
272
+
273
+ ### Format Validation and Custom Rules
274
+
275
+ **Use when**: Testing email format, phone number format, password strength, and business-specific validation rules.
276
+ **Avoid when**: The validation is purely server-side with no client-side feedback. Test via API instead.
277
+
278
+ **TypeScript**
279
+ ```typescript
280
+ import { test, expect } from '@playwright/test';
281
+
282
+ test('validates email format', async ({ page }) => {
283
+ await page.goto('/register');
284
+
285
+ const emailField = page.getByLabel('Email');
286
+
287
+ // Invalid formats
288
+ const invalidEmails = ['not-an-email', 'missing@', '@no-local.com', 'spaces in@email.com'];
289
+
290
+ for (const email of invalidEmails) {
291
+ await emailField.fill(email);
292
+ await emailField.blur();
293
+ await expect(page.getByText('Please enter a valid email')).toBeVisible();
294
+ }
295
+
296
+ // Valid format clears the error
297
+ await emailField.fill('valid@example.com');
298
+ await emailField.blur();
299
+ await expect(page.getByText('Please enter a valid email')).not.toBeVisible();
300
+ });
301
+
302
+ test('validates password strength rules', async ({ page }) => {
303
+ await page.goto('/register');
304
+
305
+ const passwordField = page.getByLabel('Password', { exact: true });
306
+
307
+ // Too short
308
+ await passwordField.fill('Ab1!');
309
+ await passwordField.blur();
310
+ await expect(page.getByText('At least 8 characters')).toBeVisible();
311
+
312
+ // Missing uppercase
313
+ await passwordField.fill('abcdefg1!');
314
+ await passwordField.blur();
315
+ await expect(page.getByText('At least one uppercase letter')).toBeVisible();
316
+
317
+ // Strong password — all checks pass
318
+ await passwordField.fill('Str0ngP@ss!');
319
+ await passwordField.blur();
320
+ await expect(page.getByText(/At least/)).not.toBeVisible();
321
+ });
322
+
323
+ test('validates custom business rule — age range', async ({ page }) => {
324
+ await page.goto('/insurance/quote');
325
+
326
+ await page.getByLabel('Age').fill('15');
327
+ await page.getByLabel('Age').blur();
328
+ await expect(page.getByText('Must be 18 or older')).toBeVisible();
329
+
330
+ await page.getByLabel('Age').fill('150');
331
+ await page.getByLabel('Age').blur();
332
+ await expect(page.getByText('Please enter a valid age')).toBeVisible();
333
+
334
+ await page.getByLabel('Age').fill('30');
335
+ await page.getByLabel('Age').blur();
336
+ await expect(page.getByText(/Must be|valid age/)).not.toBeVisible();
337
+ });
338
+ ```
339
+
340
+ **JavaScript**
341
+ ```javascript
342
+ const { test, expect } = require('@playwright/test');
343
+
344
+ test('validates email format', async ({ page }) => {
345
+ await page.goto('/register');
346
+
347
+ const emailField = page.getByLabel('Email');
348
+
349
+ const invalidEmails = ['not-an-email', 'missing@', '@no-local.com', 'spaces in@email.com'];
350
+
351
+ for (const email of invalidEmails) {
352
+ await emailField.fill(email);
353
+ await emailField.blur();
354
+ await expect(page.getByText('Please enter a valid email')).toBeVisible();
355
+ }
356
+
357
+ await emailField.fill('valid@example.com');
358
+ await emailField.blur();
359
+ await expect(page.getByText('Please enter a valid email')).not.toBeVisible();
360
+ });
361
+
362
+ test('validates password strength rules', async ({ page }) => {
363
+ await page.goto('/register');
364
+
365
+ const passwordField = page.getByLabel('Password', { exact: true });
366
+
367
+ await passwordField.fill('Ab1!');
368
+ await passwordField.blur();
369
+ await expect(page.getByText('At least 8 characters')).toBeVisible();
370
+
371
+ await passwordField.fill('abcdefg1!');
372
+ await passwordField.blur();
373
+ await expect(page.getByText('At least one uppercase letter')).toBeVisible();
374
+
375
+ await passwordField.fill('Str0ngP@ss!');
376
+ await passwordField.blur();
377
+ await expect(page.getByText(/At least/)).not.toBeVisible();
378
+ });
379
+
380
+ test('validates custom business rule — age range', async ({ page }) => {
381
+ await page.goto('/insurance/quote');
382
+
383
+ await page.getByLabel('Age').fill('15');
384
+ await page.getByLabel('Age').blur();
385
+ await expect(page.getByText('Must be 18 or older')).toBeVisible();
386
+
387
+ await page.getByLabel('Age').fill('150');
388
+ await page.getByLabel('Age').blur();
389
+ await expect(page.getByText('Please enter a valid age')).toBeVisible();
390
+
391
+ await page.getByLabel('Age').fill('30');
392
+ await page.getByLabel('Age').blur();
393
+ await expect(page.getByText(/Must be|valid age/)).not.toBeVisible();
394
+ });
395
+ ```
396
+
397
+ ### Multi-Step Forms and Wizards
398
+
399
+ **Use when**: The form spans multiple pages or steps, with next/previous navigation and per-step validation.
400
+ **Avoid when**: The form is a single page. Use the basic form filling pattern.
401
+
402
+ **TypeScript**
403
+ ```typescript
404
+ import { test, expect } from '@playwright/test';
405
+
406
+ test('complete a multi-step checkout wizard', async ({ page }) => {
407
+ await page.goto('/checkout');
408
+
409
+ // Step 1: Shipping
410
+ await test.step('fill shipping information', async () => {
411
+ await expect(page.getByRole('heading', { name: 'Shipping' })).toBeVisible();
412
+
413
+ await page.getByLabel('Address').fill('123 Main St');
414
+ await page.getByLabel('City').fill('Portland');
415
+ await page.getByLabel('State').selectOption('OR');
416
+ await page.getByLabel('ZIP code').fill('97201');
417
+
418
+ await page.getByRole('button', { name: 'Continue' }).click();
419
+ });
420
+
421
+ // Step 2: Payment
422
+ await test.step('fill payment details', async () => {
423
+ await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
424
+
425
+ await page.getByLabel('Card number').fill('4242424242424242');
426
+ await page.getByLabel('Expiration').fill('12/28');
427
+ await page.getByLabel('CVC').fill('123');
428
+
429
+ await page.getByRole('button', { name: 'Continue' }).click();
430
+ });
431
+
432
+ // Step 3: Review
433
+ await test.step('review and confirm order', async () => {
434
+ await expect(page.getByRole('heading', { name: 'Review' })).toBeVisible();
435
+
436
+ // Verify data from previous steps is shown
437
+ await expect(page.getByText('123 Main St')).toBeVisible();
438
+ await expect(page.getByText('ending in 4242')).toBeVisible();
439
+
440
+ await page.getByRole('button', { name: 'Place order' }).click();
441
+ });
442
+
443
+ // Confirmation
444
+ await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
445
+ });
446
+
447
+ test('wizard validates each step before proceeding', async ({ page }) => {
448
+ await page.goto('/checkout');
449
+
450
+ // Try to skip step 1 without filling required fields
451
+ await page.getByRole('button', { name: 'Continue' }).click();
452
+
453
+ // Should stay on step 1 with validation errors
454
+ await expect(page.getByRole('heading', { name: 'Shipping' })).toBeVisible();
455
+ await expect(page.getByText('Address is required')).toBeVisible();
456
+ });
457
+
458
+ test('wizard supports going back without losing data', async ({ page }) => {
459
+ await page.goto('/checkout');
460
+
461
+ // Fill step 1
462
+ await page.getByLabel('Address').fill('123 Main St');
463
+ await page.getByLabel('City').fill('Portland');
464
+ await page.getByLabel('State').selectOption('OR');
465
+ await page.getByLabel('ZIP code').fill('97201');
466
+ await page.getByRole('button', { name: 'Continue' }).click();
467
+
468
+ // Go back from step 2
469
+ await page.getByRole('button', { name: 'Back' }).click();
470
+
471
+ // Verify step 1 data is preserved
472
+ await expect(page.getByLabel('Address')).toHaveValue('123 Main St');
473
+ await expect(page.getByLabel('City')).toHaveValue('Portland');
474
+ });
475
+ ```
476
+
477
+ **JavaScript**
478
+ ```javascript
479
+ const { test, expect } = require('@playwright/test');
480
+
481
+ test('complete a multi-step checkout wizard', async ({ page }) => {
482
+ await page.goto('/checkout');
483
+
484
+ await test.step('fill shipping information', async () => {
485
+ await expect(page.getByRole('heading', { name: 'Shipping' })).toBeVisible();
486
+
487
+ await page.getByLabel('Address').fill('123 Main St');
488
+ await page.getByLabel('City').fill('Portland');
489
+ await page.getByLabel('State').selectOption('OR');
490
+ await page.getByLabel('ZIP code').fill('97201');
491
+
492
+ await page.getByRole('button', { name: 'Continue' }).click();
493
+ });
494
+
495
+ await test.step('fill payment details', async () => {
496
+ await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
497
+
498
+ await page.getByLabel('Card number').fill('4242424242424242');
499
+ await page.getByLabel('Expiration').fill('12/28');
500
+ await page.getByLabel('CVC').fill('123');
501
+
502
+ await page.getByRole('button', { name: 'Continue' }).click();
503
+ });
504
+
505
+ await test.step('review and confirm order', async () => {
506
+ await expect(page.getByRole('heading', { name: 'Review' })).toBeVisible();
507
+
508
+ await expect(page.getByText('123 Main St')).toBeVisible();
509
+ await expect(page.getByText('ending in 4242')).toBeVisible();
510
+
511
+ await page.getByRole('button', { name: 'Place order' }).click();
512
+ });
513
+
514
+ await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
515
+ });
516
+
517
+ test('wizard validates each step before proceeding', async ({ page }) => {
518
+ await page.goto('/checkout');
519
+
520
+ await page.getByRole('button', { name: 'Continue' }).click();
521
+
522
+ await expect(page.getByRole('heading', { name: 'Shipping' })).toBeVisible();
523
+ await expect(page.getByText('Address is required')).toBeVisible();
524
+ });
525
+
526
+ test('wizard supports going back without losing data', async ({ page }) => {
527
+ await page.goto('/checkout');
528
+
529
+ await page.getByLabel('Address').fill('123 Main St');
530
+ await page.getByLabel('City').fill('Portland');
531
+ await page.getByLabel('State').selectOption('OR');
532
+ await page.getByLabel('ZIP code').fill('97201');
533
+ await page.getByRole('button', { name: 'Continue' }).click();
534
+
535
+ await page.getByRole('button', { name: 'Back' }).click();
536
+
537
+ await expect(page.getByLabel('Address')).toHaveValue('123 Main St');
538
+ await expect(page.getByLabel('City')).toHaveValue('Portland');
539
+ });
540
+ ```
541
+
542
+ ### Auto-Complete and Typeahead Fields
543
+
544
+ **Use when**: Testing search fields, address lookups, mention pickers, or any input that shows suggestions as the user types.
545
+ **Avoid when**: The field is a plain text input with no suggestions.
546
+
547
+ **TypeScript**
548
+ ```typescript
549
+ import { test, expect } from '@playwright/test';
550
+
551
+ test('select from auto-complete suggestions', async ({ page }) => {
552
+ await page.goto('/search');
553
+
554
+ const searchField = page.getByRole('combobox', { name: 'Search' });
555
+
556
+ // Type slowly enough for suggestions to appear
557
+ // pressSequentially simulates real keystrokes (triggers keydown/keyup/input events)
558
+ await searchField.pressSequentially('playw', { delay: 100 });
559
+
560
+ // Wait for the suggestion list to appear
561
+ const suggestions = page.getByRole('listbox');
562
+ await expect(suggestions).toBeVisible();
563
+
564
+ // Select a specific suggestion
565
+ await suggestions.getByRole('option', { name: 'Playwright Testing' }).click();
566
+
567
+ // Verify the selection populated the field
568
+ await expect(searchField).toHaveValue('Playwright Testing');
569
+ });
570
+
571
+ test('auto-complete with API-driven suggestions', async ({ page }) => {
572
+ await page.goto('/address-form');
573
+
574
+ const addressField = page.getByLabel('Address');
575
+ await addressField.pressSequentially('123 Ma', { delay: 50 });
576
+
577
+ // Wait for the API-driven suggestion list
578
+ const responsePromise = page.waitForResponse('**/api/address-suggest*');
579
+ await responsePromise;
580
+
581
+ await page.getByRole('option', { name: /123 Main St/ }).click();
582
+
583
+ // Verify dependent fields were auto-populated
584
+ await expect(page.getByLabel('City')).toHaveValue('Portland');
585
+ await expect(page.getByLabel('State')).toHaveValue('OR');
586
+ await expect(page.getByLabel('ZIP code')).toHaveValue('97201');
587
+ });
588
+
589
+ test('dismiss auto-complete and use custom value', async ({ page }) => {
590
+ await page.goto('/tags');
591
+
592
+ const tagInput = page.getByLabel('Add tag');
593
+ await tagInput.pressSequentially('custom-tag');
594
+
595
+ // Dismiss suggestions with Escape
596
+ await tagInput.press('Escape');
597
+ await expect(page.getByRole('listbox')).not.toBeVisible();
598
+
599
+ // Submit custom value with Enter
600
+ await tagInput.press('Enter');
601
+ await expect(page.getByText('custom-tag')).toBeVisible();
602
+ });
603
+ ```
604
+
605
+ **JavaScript**
606
+ ```javascript
607
+ const { test, expect } = require('@playwright/test');
608
+
609
+ test('select from auto-complete suggestions', async ({ page }) => {
610
+ await page.goto('/search');
611
+
612
+ const searchField = page.getByRole('combobox', { name: 'Search' });
613
+ await searchField.pressSequentially('playw', { delay: 100 });
614
+
615
+ const suggestions = page.getByRole('listbox');
616
+ await expect(suggestions).toBeVisible();
617
+
618
+ await suggestions.getByRole('option', { name: 'Playwright Testing' }).click();
619
+ await expect(searchField).toHaveValue('Playwright Testing');
620
+ });
621
+
622
+ test('auto-complete with API-driven suggestions', async ({ page }) => {
623
+ await page.goto('/address-form');
624
+
625
+ const addressField = page.getByLabel('Address');
626
+ await addressField.pressSequentially('123 Ma', { delay: 50 });
627
+
628
+ const responsePromise = page.waitForResponse('**/api/address-suggest*');
629
+ await responsePromise;
630
+
631
+ await page.getByRole('option', { name: /123 Main St/ }).click();
632
+
633
+ await expect(page.getByLabel('City')).toHaveValue('Portland');
634
+ await expect(page.getByLabel('State')).toHaveValue('OR');
635
+ await expect(page.getByLabel('ZIP code')).toHaveValue('97201');
636
+ });
637
+
638
+ test('dismiss auto-complete and use custom value', async ({ page }) => {
639
+ await page.goto('/tags');
640
+
641
+ const tagInput = page.getByLabel('Add tag');
642
+ await tagInput.pressSequentially('custom-tag');
643
+
644
+ await tagInput.press('Escape');
645
+ await expect(page.getByRole('listbox')).not.toBeVisible();
646
+
647
+ await tagInput.press('Enter');
648
+ await expect(page.getByText('custom-tag')).toBeVisible();
649
+ });
650
+ ```
651
+
652
+ ### Dynamic Forms — Conditional Fields
653
+
654
+ **Use when**: Form fields appear, disappear, or change based on the value of other fields.
655
+ **Avoid when**: All fields are always visible. Use the basic form filling pattern.
656
+
657
+ **TypeScript**
658
+ ```typescript
659
+ import { test, expect } from '@playwright/test';
660
+
661
+ test('conditional fields appear based on selection', async ({ page }) => {
662
+ await page.goto('/insurance/apply');
663
+
664
+ // Selecting "Business" shows additional fields
665
+ await page.getByLabel('Account type').selectOption('business');
666
+
667
+ // Wait for conditional fields to appear
668
+ await expect(page.getByLabel('Company name')).toBeVisible();
669
+ await expect(page.getByLabel('Tax ID')).toBeVisible();
670
+
671
+ await page.getByLabel('Company name').fill('Acme Corp');
672
+ await page.getByLabel('Tax ID').fill('12-3456789');
673
+
674
+ // Switching back to "Personal" hides them
675
+ await page.getByLabel('Account type').selectOption('personal');
676
+ await expect(page.getByLabel('Company name')).not.toBeVisible();
677
+ await expect(page.getByLabel('Tax ID')).not.toBeVisible();
678
+ });
679
+
680
+ test('checkbox toggles additional section', async ({ page }) => {
681
+ await page.goto('/shipping');
682
+
683
+ // "Different billing address" reveals billing fields
684
+ await page.getByLabel('Use different billing address').check();
685
+
686
+ const billingSection = page.getByRole('group', { name: 'Billing address' });
687
+ await expect(billingSection).toBeVisible();
688
+
689
+ await billingSection.getByLabel('Street').fill('456 Oak Ave');
690
+ await billingSection.getByLabel('City').fill('Seattle');
691
+
692
+ // Unchecking hides the section
693
+ await page.getByLabel('Use different billing address').uncheck();
694
+ await expect(billingSection).not.toBeVisible();
695
+ });
696
+
697
+ test('dependent dropdown chains', async ({ page }) => {
698
+ await page.goto('/location-picker');
699
+
700
+ // Country selection populates the state dropdown
701
+ await page.getByLabel('Country').selectOption('US');
702
+
703
+ // Wait for the dependent dropdown to be populated
704
+ const stateDropdown = page.getByLabel('State');
705
+ await expect(stateDropdown.getByRole('option')).not.toHaveCount(0);
706
+
707
+ await stateDropdown.selectOption('CA');
708
+
709
+ // State selection populates the city dropdown
710
+ const cityDropdown = page.getByLabel('City');
711
+ await expect(cityDropdown.getByRole('option')).not.toHaveCount(0);
712
+
713
+ await cityDropdown.selectOption({ label: 'Los Angeles' });
714
+ });
715
+ ```
716
+
717
+ **JavaScript**
718
+ ```javascript
719
+ const { test, expect } = require('@playwright/test');
720
+
721
+ test('conditional fields appear based on selection', async ({ page }) => {
722
+ await page.goto('/insurance/apply');
723
+
724
+ await page.getByLabel('Account type').selectOption('business');
725
+
726
+ await expect(page.getByLabel('Company name')).toBeVisible();
727
+ await expect(page.getByLabel('Tax ID')).toBeVisible();
728
+
729
+ await page.getByLabel('Company name').fill('Acme Corp');
730
+ await page.getByLabel('Tax ID').fill('12-3456789');
731
+
732
+ await page.getByLabel('Account type').selectOption('personal');
733
+ await expect(page.getByLabel('Company name')).not.toBeVisible();
734
+ await expect(page.getByLabel('Tax ID')).not.toBeVisible();
735
+ });
736
+
737
+ test('checkbox toggles additional section', async ({ page }) => {
738
+ await page.goto('/shipping');
739
+
740
+ await page.getByLabel('Use different billing address').check();
741
+
742
+ const billingSection = page.getByRole('group', { name: 'Billing address' });
743
+ await expect(billingSection).toBeVisible();
744
+
745
+ await billingSection.getByLabel('Street').fill('456 Oak Ave');
746
+ await billingSection.getByLabel('City').fill('Seattle');
747
+
748
+ await page.getByLabel('Use different billing address').uncheck();
749
+ await expect(billingSection).not.toBeVisible();
750
+ });
751
+
752
+ test('dependent dropdown chains', async ({ page }) => {
753
+ await page.goto('/location-picker');
754
+
755
+ await page.getByLabel('Country').selectOption('US');
756
+
757
+ const stateDropdown = page.getByLabel('State');
758
+ await expect(stateDropdown.getByRole('option')).not.toHaveCount(0);
759
+
760
+ await stateDropdown.selectOption('CA');
761
+
762
+ const cityDropdown = page.getByLabel('City');
763
+ await expect(cityDropdown.getByRole('option')).not.toHaveCount(0);
764
+
765
+ await cityDropdown.selectOption({ label: 'Los Angeles' });
766
+ });
767
+ ```
768
+
769
+ ### Form Submission and Response Handling
770
+
771
+ **Use when**: Testing what happens after a form is submitted — success messages, redirects, error responses from the server, and loading states during submission.
772
+ **Avoid when**: You only care about client-side validation. Test submission separately from validation.
773
+
774
+ **TypeScript**
775
+ ```typescript
776
+ import { test, expect } from '@playwright/test';
777
+
778
+ test('successful form submission shows confirmation', async ({ page }) => {
779
+ await page.goto('/contact');
780
+
781
+ await page.getByLabel('Name').fill('Jane Doe');
782
+ await page.getByLabel('Email').fill('jane@example.com');
783
+ await page.getByLabel('Message').fill('Hello from Playwright');
784
+
785
+ // Wait for the API response during submission
786
+ const responsePromise = page.waitForResponse('**/api/contact');
787
+ await page.getByRole('button', { name: 'Send message' }).click();
788
+ const response = await responsePromise;
789
+
790
+ expect(response.status()).toBe(200);
791
+ await expect(page.getByText('Message sent successfully')).toBeVisible();
792
+ });
793
+
794
+ test('form submission shows server-side validation errors', async ({ page }) => {
795
+ await page.goto('/register');
796
+
797
+ await page.getByLabel('Email').fill('taken@example.com');
798
+ await page.getByLabel('Password', { exact: true }).fill('ValidP@ss1');
799
+ await page.getByRole('button', { name: 'Register' }).click();
800
+
801
+ // Server responds with a 409 — email already taken
802
+ await expect(page.getByText('An account with this email already exists')).toBeVisible();
803
+ });
804
+
805
+ test('form shows loading state during submission', async ({ page }) => {
806
+ await page.goto('/contact');
807
+
808
+ await page.getByLabel('Name').fill('Jane');
809
+ await page.getByLabel('Email').fill('jane@example.com');
810
+ await page.getByLabel('Message').fill('Test');
811
+
812
+ await page.getByRole('button', { name: 'Send message' }).click();
813
+
814
+ // Button should be disabled during submission
815
+ await expect(page.getByRole('button', { name: /Sending/ })).toBeDisabled();
816
+
817
+ // After completion, button returns to normal
818
+ await expect(page.getByRole('button', { name: 'Send message' })).toBeEnabled();
819
+ });
820
+
821
+ test('form redirects after successful submission', async ({ page }) => {
822
+ await page.goto('/login');
823
+
824
+ await page.getByLabel('Email').fill('user@example.com');
825
+ await page.getByLabel('Password').fill('password123');
826
+ await page.getByRole('button', { name: 'Sign in' }).click();
827
+
828
+ // Verify redirect
829
+ await page.waitForURL('/dashboard');
830
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
831
+ });
832
+ ```
833
+
834
+ **JavaScript**
835
+ ```javascript
836
+ const { test, expect } = require('@playwright/test');
837
+
838
+ test('successful form submission shows confirmation', async ({ page }) => {
839
+ await page.goto('/contact');
840
+
841
+ await page.getByLabel('Name').fill('Jane Doe');
842
+ await page.getByLabel('Email').fill('jane@example.com');
843
+ await page.getByLabel('Message').fill('Hello from Playwright');
844
+
845
+ const responsePromise = page.waitForResponse('**/api/contact');
846
+ await page.getByRole('button', { name: 'Send message' }).click();
847
+ const response = await responsePromise;
848
+
849
+ expect(response.status()).toBe(200);
850
+ await expect(page.getByText('Message sent successfully')).toBeVisible();
851
+ });
852
+
853
+ test('form submission shows server-side validation errors', async ({ page }) => {
854
+ await page.goto('/register');
855
+
856
+ await page.getByLabel('Email').fill('taken@example.com');
857
+ await page.getByLabel('Password', { exact: true }).fill('ValidP@ss1');
858
+ await page.getByRole('button', { name: 'Register' }).click();
859
+
860
+ await expect(page.getByText('An account with this email already exists')).toBeVisible();
861
+ });
862
+
863
+ test('form shows loading state during submission', async ({ page }) => {
864
+ await page.goto('/contact');
865
+
866
+ await page.getByLabel('Name').fill('Jane');
867
+ await page.getByLabel('Email').fill('jane@example.com');
868
+ await page.getByLabel('Message').fill('Test');
869
+
870
+ await page.getByRole('button', { name: 'Send message' }).click();
871
+
872
+ await expect(page.getByRole('button', { name: /Sending/ })).toBeDisabled();
873
+ await expect(page.getByRole('button', { name: 'Send message' })).toBeEnabled();
874
+ });
875
+
876
+ test('form redirects after successful submission', async ({ page }) => {
877
+ await page.goto('/login');
878
+
879
+ await page.getByLabel('Email').fill('user@example.com');
880
+ await page.getByLabel('Password').fill('password123');
881
+ await page.getByRole('button', { name: 'Sign in' }).click();
882
+
883
+ await page.waitForURL('/dashboard');
884
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
885
+ });
886
+ ```
887
+
888
+ ### Form Reset Testing
889
+
890
+ **Use when**: Testing "clear form" or "reset" functionality, verifying that fields return to their default values.
891
+ **Avoid when**: The form has no reset mechanism.
892
+
893
+ **TypeScript**
894
+ ```typescript
895
+ import { test, expect } from '@playwright/test';
896
+
897
+ test('reset button clears all fields to defaults', async ({ page }) => {
898
+ await page.goto('/settings');
899
+
900
+ // Change fields from defaults
901
+ await page.getByLabel('Display name').fill('New Name');
902
+ await page.getByLabel('Theme').selectOption('dark');
903
+ await page.getByLabel('Notifications').uncheck();
904
+
905
+ // Click reset
906
+ await page.getByRole('button', { name: 'Reset' }).click();
907
+
908
+ // Verify fields returned to original values
909
+ await expect(page.getByLabel('Display name')).toHaveValue('');
910
+ await expect(page.getByLabel('Theme')).toHaveValue('light');
911
+ await expect(page.getByLabel('Notifications')).toBeChecked();
912
+ });
913
+
914
+ test('confirmation dialog before resetting a dirty form', async ({ page }) => {
915
+ await page.goto('/editor');
916
+
917
+ await page.getByLabel('Title').fill('Draft post');
918
+
919
+ // Reset triggers a confirmation dialog
920
+ page.on('dialog', (dialog) => dialog.accept());
921
+ await page.getByRole('button', { name: 'Discard changes' }).click();
922
+
923
+ await expect(page.getByLabel('Title')).toHaveValue('');
924
+ });
925
+ ```
926
+
927
+ **JavaScript**
928
+ ```javascript
929
+ const { test, expect } = require('@playwright/test');
930
+
931
+ test('reset button clears all fields to defaults', async ({ page }) => {
932
+ await page.goto('/settings');
933
+
934
+ await page.getByLabel('Display name').fill('New Name');
935
+ await page.getByLabel('Theme').selectOption('dark');
936
+ await page.getByLabel('Notifications').uncheck();
937
+
938
+ await page.getByRole('button', { name: 'Reset' }).click();
939
+
940
+ await expect(page.getByLabel('Display name')).toHaveValue('');
941
+ await expect(page.getByLabel('Theme')).toHaveValue('light');
942
+ await expect(page.getByLabel('Notifications')).toBeChecked();
943
+ });
944
+
945
+ test('confirmation dialog before resetting a dirty form', async ({ page }) => {
946
+ await page.goto('/editor');
947
+
948
+ await page.getByLabel('Title').fill('Draft post');
949
+
950
+ page.on('dialog', (dialog) => dialog.accept());
951
+ await page.getByRole('button', { name: 'Discard changes' }).click();
952
+
953
+ await expect(page.getByLabel('Title')).toHaveValue('');
954
+ });
955
+ ```
956
+
957
+ ## Decision Guide
958
+
959
+ | Scenario | Approach | Key API |
960
+ |---|---|---|
961
+ | Standard text input | `fill()` (clears, then types) | `page.getByLabel('Name').fill('Jane')` |
962
+ | Need keystroke events (autocomplete) | `pressSequentially()` with delay | `locator.pressSequentially('text', { delay: 100 })` |
963
+ | Native `<select>` dropdown | `selectOption()` by value or label | `locator.selectOption('US')` or `{ label: 'United States' }` |
964
+ | Custom dropdown (ARIA listbox) | Click trigger, then select option role | `getByRole('option', { name: '...' }).click()` |
965
+ | Checkbox | `check()` / `uncheck()` (idempotent) | `locator.check()` — safe to call even if already checked |
966
+ | Radio button | `check()` on the target radio | `page.getByLabel('Express').check()` |
967
+ | Date input (native) | `fill()` with ISO format | `locator.fill('2025-03-15')` |
968
+ | Date picker (third-party) | Click to open, navigate, select day | `getByRole('gridcell', { name: '15' }).click()` |
969
+ | Validation errors | Submit, then assert error text | `expect(page.getByText('Required')).toBeVisible()` |
970
+ | Multi-step wizard | `test.step()` per step, assert heading | `await test.step('Step 1', async () => { ... })` |
971
+ | Conditional/dynamic fields | Change trigger field, assert new field visibility | `expect(locator).toBeVisible()` / `.not.toBeVisible()` |
972
+ | Form submission | `waitForResponse` + click submit | Register response listener before click |
973
+ | Auto-complete | `pressSequentially()`, wait for listbox, select option | `getByRole('option', { name }).click()` |
974
+ | Form reset | Click reset, assert default values | `expect(locator).toHaveValue('')` |
975
+
976
+ ## Anti-Patterns
977
+
978
+ | Don't Do This | Problem | Do This Instead |
979
+ |---|---|---|
980
+ | `await page.getByLabel('Name').type('Jane')` | `type()` appends to existing content; does not clear first | `await page.getByLabel('Name').fill('Jane')` |
981
+ | `await page.getByLabel('Agree').click()` | `click()` toggles — if already checked, it unchecks | `await page.getByLabel('Agree').check()` |
982
+ | `await page.fill('#email', 'test@test.com')` | CSS selector is fragile | `await page.getByLabel('Email').fill('test@test.com')` |
983
+ | `await page.selectOption('select', 'US')` without label | Targets first `<select>` on page; ambiguous | `await page.getByLabel('Country').selectOption('US')` |
984
+ | Testing every invalid input in one test | Test becomes huge, slow, and hard to debug | One test per validation rule or group related rules |
985
+ | `expect(await input.inputValue()).toBe('Jane')` | Resolves once — no retry. Race condition. | `await expect(input).toHaveValue('Jane')` |
986
+ | Filling fields with `page.evaluate()` | Bypasses event handlers (no `input`, `change` events fire) | Use `fill()` or `pressSequentially()` |
987
+ | Not waiting for conditional fields before filling | `fill()` fails on hidden/detached elements | `await expect(field).toBeVisible()` first |
988
+ | Hardcoding wait after selecting a dropdown | `waitForTimeout(500)` is flaky and slow | Wait for the dependent element to appear |
989
+ | Skipping server-side validation tests | Client-side validation can be bypassed | Test both client-side UX and server response |
990
+
991
+ ## Troubleshooting
992
+
993
+ ### `fill()` does nothing or clears but doesn't type
994
+
995
+ **Cause**: The input field uses a contenteditable div (rich text editors), not a real `<input>` or `<textarea>`.
996
+
997
+ ```typescript
998
+ // Check if it is contenteditable
999
+ const isContentEditable = await page.getByTestId('editor').evaluate(
1000
+ (el) => el.getAttribute('contenteditable')
1001
+ );
1002
+
1003
+ // For contenteditable, use pressSequentially or type
1004
+ if (isContentEditable) {
1005
+ await page.getByTestId('editor').click();
1006
+ await page.getByTestId('editor').pressSequentially('Hello world');
1007
+ }
1008
+ ```
1009
+
1010
+ ### Date picker does not accept `fill()` value
1011
+
1012
+ **Cause**: Third-party date pickers often render custom UI over a hidden input. `fill()` sets the hidden input but the UI does not update.
1013
+
1014
+ ```typescript
1015
+ // Interact with the date picker UI instead
1016
+ await page.getByLabel('Date').click(); // Opens the picker
1017
+ await page.getByRole('button', { name: 'Next month' }).click();
1018
+ await page.getByRole('gridcell', { name: '15' }).click();
1019
+
1020
+ // Alternatively, if the library reads from the input on change:
1021
+ await page.getByLabel('Date').fill('2025-06-15');
1022
+ await page.getByLabel('Date').dispatchEvent('change');
1023
+ ```
1024
+
1025
+ ### `selectOption()` throws "not a <select> element"
1026
+
1027
+ **Cause**: The dropdown is a custom component (ARIA listbox), not a native `<select>`.
1028
+
1029
+ ```typescript
1030
+ // For custom dropdowns: click to open, then select from the listbox
1031
+ await page.getByRole('combobox', { name: 'Country' }).click();
1032
+ await page.getByRole('option', { name: 'United States' }).click();
1033
+ ```
1034
+
1035
+ ### Validation errors do not appear after `fill()` and submit
1036
+
1037
+ **Cause**: The validation triggers on `blur` (focus leaving the field), but `fill()` does not trigger blur automatically.
1038
+
1039
+ ```typescript
1040
+ // Trigger blur explicitly
1041
+ await page.getByLabel('Email').fill('invalid');
1042
+ await page.getByLabel('Email').blur();
1043
+ await expect(page.getByText('Please enter a valid email')).toBeVisible();
1044
+
1045
+ // Or move focus to the next field
1046
+ await page.getByLabel('Password').focus();
1047
+ ```
1048
+
1049
+ ## Related
1050
+
1051
+ - [core/locators.md](locators.md) -- locator strategies for finding form elements
1052
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- assertion patterns for verifying form state
1053
+ - [core/file-operations.md](file-operations.md) -- file upload fields in forms
1054
+ - [core/error-and-edge-cases.md](error-and-edge-cases.md) -- testing form error states and edge cases
1055
+ - [core/accessibility.md](accessibility.md) -- ensuring forms are accessible (label associations, ARIA attributes)
1056
+ - [core/network-mocking.md](network-mocking.md) -- mocking form submission API responses