@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,945 @@
1
+ # CRUD Testing Recipes
2
+
3
+ > **When to use**: You need to test create, read, update, or delete operations on any resource -- forms, tables, lists, cards, inline edits, or bulk actions.
4
+
5
+ ---
6
+
7
+ ## Recipe 1: Creating a Resource (Fill Form, Submit, Verify)
8
+
9
+ ### Complete Example
10
+
11
+ **TypeScript**
12
+
13
+ ```typescript
14
+ import { test, expect } from '@playwright/test';
15
+
16
+ test('creates a new product via form', async ({ page }) => {
17
+ await page.goto('/products');
18
+
19
+ // Click the create button
20
+ await page.getByRole('button', { name: 'Add product' }).click();
21
+
22
+ // Fill out the creation form
23
+ await page.getByLabel('Product name').fill('Wireless Keyboard');
24
+ await page.getByLabel('SKU').fill('KB-WIRELESS-001');
25
+ await page.getByLabel('Price').fill('79.99');
26
+ await page.getByLabel('Description').fill('Ergonomic wireless keyboard with backlit keys');
27
+ await page.getByLabel('Category').selectOption('Electronics');
28
+ await page.getByLabel('In stock').check();
29
+
30
+ // Submit
31
+ await page.getByRole('button', { name: 'Save product' }).click();
32
+
33
+ // Verify success notification
34
+ await expect(page.getByRole('alert')).toContainText('Product created successfully');
35
+
36
+ // Verify the new item appears in the list
37
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
38
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toContainText('$79.99');
39
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toContainText('KB-WIRELESS-001');
40
+ });
41
+
42
+ test('shows validation errors for invalid form data', async ({ page }) => {
43
+ await page.goto('/products');
44
+ await page.getByRole('button', { name: 'Add product' }).click();
45
+
46
+ // Submit empty form
47
+ await page.getByRole('button', { name: 'Save product' }).click();
48
+
49
+ // Verify field-level validation messages
50
+ await expect(page.getByText('Product name is required')).toBeVisible();
51
+ await expect(page.getByText('Price is required')).toBeVisible();
52
+
53
+ // Fill in invalid data
54
+ await page.getByLabel('Product name').fill('A'); // too short
55
+ await page.getByLabel('Price').fill('-5');
56
+
57
+ await page.getByRole('button', { name: 'Save product' }).click();
58
+
59
+ await expect(page.getByText('Name must be at least 3 characters')).toBeVisible();
60
+ await expect(page.getByText('Price must be a positive number')).toBeVisible();
61
+ });
62
+
63
+ test('prevents duplicate resource creation', async ({ page }) => {
64
+ await page.goto('/products');
65
+ await page.getByRole('button', { name: 'Add product' }).click();
66
+
67
+ // Try to create a product with a SKU that already exists
68
+ await page.getByLabel('Product name').fill('Duplicate Product');
69
+ await page.getByLabel('SKU').fill('EXISTING-SKU-001');
70
+ await page.getByLabel('Price').fill('29.99');
71
+ await page.getByRole('button', { name: 'Save product' }).click();
72
+
73
+ await expect(page.getByRole('alert')).toContainText(/already exists|duplicate/i);
74
+ });
75
+ ```
76
+
77
+ **JavaScript**
78
+
79
+ ```javascript
80
+ const { test, expect } = require('@playwright/test');
81
+
82
+ test('creates a new product via form', async ({ page }) => {
83
+ await page.goto('/products');
84
+
85
+ await page.getByRole('button', { name: 'Add product' }).click();
86
+
87
+ await page.getByLabel('Product name').fill('Wireless Keyboard');
88
+ await page.getByLabel('SKU').fill('KB-WIRELESS-001');
89
+ await page.getByLabel('Price').fill('79.99');
90
+ await page.getByLabel('Description').fill('Ergonomic wireless keyboard with backlit keys');
91
+ await page.getByLabel('Category').selectOption('Electronics');
92
+ await page.getByLabel('In stock').check();
93
+
94
+ await page.getByRole('button', { name: 'Save product' }).click();
95
+
96
+ await expect(page.getByRole('alert')).toContainText('Product created successfully');
97
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
98
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toContainText('$79.99');
99
+ });
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Recipe 2: Reading / Listing Resources (Table, Cards, Pagination)
105
+
106
+ ### Complete Example
107
+
108
+ **TypeScript**
109
+
110
+ ```typescript
111
+ import { test, expect } from '@playwright/test';
112
+
113
+ test.describe('reading resources in a table', () => {
114
+ test('displays resources in a table with correct columns', async ({ page }) => {
115
+ await page.goto('/products');
116
+
117
+ // Verify table headers
118
+ const headers = page.getByRole('columnheader');
119
+ await expect(headers).toContainText(['Name', 'SKU', 'Price', 'Category', 'Status']);
120
+
121
+ // Verify at least one row of data exists
122
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
123
+ await expect(rows.first()).toBeVisible();
124
+
125
+ // Verify specific data in a row
126
+ const firstRow = rows.first();
127
+ await expect(firstRow.getByRole('cell').nth(0)).not.toBeEmpty();
128
+ await expect(firstRow.getByRole('cell').nth(2)).toContainText('$');
129
+ });
130
+
131
+ test('sorts table by column', async ({ page }) => {
132
+ await page.goto('/products');
133
+
134
+ // Click the Price header to sort
135
+ await page.getByRole('columnheader', { name: 'Price' }).click();
136
+
137
+ // Get all prices from the table
138
+ const priceCells = page.getByRole('row')
139
+ .filter({ hasNot: page.getByRole('columnheader') })
140
+ .getByRole('cell')
141
+ .nth(2);
142
+
143
+ const prices = await priceCells.allTextContents();
144
+ const numericPrices = prices.map((p) => parseFloat(p.replace(/[$,]/g, '')));
145
+
146
+ // Verify sorted ascending
147
+ for (let i = 1; i < numericPrices.length; i++) {
148
+ expect(numericPrices[i]).toBeGreaterThanOrEqual(numericPrices[i - 1]);
149
+ }
150
+
151
+ // Click again for descending
152
+ await page.getByRole('columnheader', { name: 'Price' }).click();
153
+
154
+ const pricesDesc = await priceCells.allTextContents();
155
+ const numericDesc = pricesDesc.map((p) => parseFloat(p.replace(/[$,]/g, '')));
156
+
157
+ for (let i = 1; i < numericDesc.length; i++) {
158
+ expect(numericDesc[i]).toBeLessThanOrEqual(numericDesc[i - 1]);
159
+ }
160
+ });
161
+
162
+ test('paginates through results', async ({ page }) => {
163
+ await page.goto('/products');
164
+
165
+ // Verify pagination controls are visible
166
+ const pagination = page.getByRole('navigation', { name: /pagination/i });
167
+ await expect(pagination).toBeVisible();
168
+
169
+ // Get first page content
170
+ const firstPageFirstRow = await page
171
+ .getByRole('row')
172
+ .filter({ hasNot: page.getByRole('columnheader') })
173
+ .first()
174
+ .textContent();
175
+
176
+ // Go to page 2
177
+ await pagination.getByRole('button', { name: '2' }).click();
178
+
179
+ // Wait for data to load
180
+ await expect(page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') }).first())
181
+ .not.toHaveText(firstPageFirstRow!);
182
+
183
+ // Verify page 2 is active
184
+ await expect(pagination.getByRole('button', { name: '2' })).toHaveAttribute(
185
+ 'aria-current',
186
+ 'page'
187
+ );
188
+
189
+ // Verify URL updated with page parameter
190
+ await expect(page).toHaveURL(/[?&]page=2/);
191
+ });
192
+
193
+ test('shows correct item count', async ({ page }) => {
194
+ await page.goto('/products');
195
+
196
+ // Verify the total count display
197
+ await expect(page.getByText(/showing \d+.+of \d+/i)).toBeVisible();
198
+ });
199
+ });
200
+
201
+ test.describe('reading resources as cards', () => {
202
+ test('displays resources in a card grid', async ({ page }) => {
203
+ await page.goto('/products?view=grid');
204
+
205
+ // Verify cards are displayed
206
+ const cards = page.getByRole('article');
207
+ await expect(cards.first()).toBeVisible();
208
+
209
+ // Verify card content
210
+ const firstCard = cards.first();
211
+ await expect(firstCard.getByRole('heading')).toBeVisible();
212
+ await expect(firstCard.getByText('$')).toBeVisible();
213
+ await expect(firstCard.getByRole('img')).toBeVisible();
214
+ });
215
+
216
+ test('switches between list and grid view', async ({ page }) => {
217
+ await page.goto('/products');
218
+
219
+ // Start in table/list view
220
+ await expect(page.getByRole('table')).toBeVisible();
221
+
222
+ // Switch to grid
223
+ await page.getByRole('button', { name: /grid view/i }).click();
224
+ await expect(page.getByRole('article').first()).toBeVisible();
225
+ await expect(page.getByRole('table')).not.toBeVisible();
226
+
227
+ // Switch back to list
228
+ await page.getByRole('button', { name: /list view/i }).click();
229
+ await expect(page.getByRole('table')).toBeVisible();
230
+ });
231
+ });
232
+ ```
233
+
234
+ **JavaScript**
235
+
236
+ ```javascript
237
+ const { test, expect } = require('@playwright/test');
238
+
239
+ test('displays resources in a table with correct columns', async ({ page }) => {
240
+ await page.goto('/products');
241
+
242
+ const headers = page.getByRole('columnheader');
243
+ await expect(headers).toContainText(['Name', 'SKU', 'Price', 'Category', 'Status']);
244
+
245
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
246
+ await expect(rows.first()).toBeVisible();
247
+ });
248
+
249
+ test('paginates through results', async ({ page }) => {
250
+ await page.goto('/products');
251
+
252
+ const pagination = page.getByRole('navigation', { name: /pagination/i });
253
+ await expect(pagination).toBeVisible();
254
+
255
+ const firstPageFirstRow = await page
256
+ .getByRole('row')
257
+ .filter({ hasNot: page.getByRole('columnheader') })
258
+ .first()
259
+ .textContent();
260
+
261
+ await pagination.getByRole('button', { name: '2' }).click();
262
+
263
+ await expect(page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') }).first())
264
+ .not.toHaveText(firstPageFirstRow);
265
+
266
+ await expect(page).toHaveURL(/[?&]page=2/);
267
+ });
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Recipe 3: Updating a Resource (Edit Form, Save, Verify Changes)
273
+
274
+ ### Complete Example
275
+
276
+ **TypeScript**
277
+
278
+ ```typescript
279
+ import { test, expect } from '@playwright/test';
280
+
281
+ test('updates an existing product', async ({ page }) => {
282
+ await page.goto('/products');
283
+
284
+ // Find the target row and click edit
285
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
286
+ await row.getByRole('button', { name: 'Edit' }).click();
287
+
288
+ // Verify the edit form is pre-filled with existing data
289
+ await expect(page.getByLabel('Product name')).toHaveValue('Wireless Keyboard');
290
+ await expect(page.getByLabel('Price')).toHaveValue('79.99');
291
+
292
+ // Update the fields
293
+ await page.getByLabel('Product name').clear();
294
+ await page.getByLabel('Product name').fill('Wireless Keyboard Pro');
295
+ await page.getByLabel('Price').clear();
296
+ await page.getByLabel('Price').fill('99.99');
297
+
298
+ // Save changes
299
+ await page.getByRole('button', { name: 'Save changes' }).click();
300
+
301
+ // Verify success notification
302
+ await expect(page.getByRole('alert')).toContainText('Product updated successfully');
303
+
304
+ // Verify the list reflects the changes
305
+ await expect(page.getByRole('row', { name: /Wireless Keyboard Pro/ })).toBeVisible();
306
+ await expect(page.getByRole('row', { name: /Wireless Keyboard Pro/ })).toContainText('$99.99');
307
+
308
+ // Verify old name is gone
309
+ await expect(page.getByRole('row', { name: /^Wireless Keyboard$/ })).not.toBeVisible();
310
+ });
311
+
312
+ test('edit form preserves data when navigating away and back', async ({ page }) => {
313
+ await page.goto('/products');
314
+
315
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
316
+ await row.getByRole('button', { name: 'Edit' }).click();
317
+
318
+ // Make changes but do not save
319
+ await page.getByLabel('Product name').clear();
320
+ await page.getByLabel('Product name').fill('Modified Name');
321
+
322
+ // Try navigating away
323
+ await page.getByRole('link', { name: 'Dashboard' }).click();
324
+
325
+ // Expect unsaved changes warning
326
+ page.on('dialog', async (dialog) => {
327
+ expect(dialog.message()).toContain('unsaved changes');
328
+ await dialog.dismiss(); // Stay on page
329
+ });
330
+
331
+ // Verify data is still there after dismissing navigation
332
+ await expect(page.getByLabel('Product name')).toHaveValue('Modified Name');
333
+ });
334
+
335
+ test('cancelling edit discards changes', async ({ page }) => {
336
+ await page.goto('/products');
337
+
338
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
339
+ await row.getByRole('button', { name: 'Edit' }).click();
340
+
341
+ // Make changes
342
+ await page.getByLabel('Product name').clear();
343
+ await page.getByLabel('Product name').fill('Should Not Save');
344
+
345
+ // Cancel
346
+ await page.getByRole('button', { name: 'Cancel' }).click();
347
+
348
+ // Verify original data is preserved
349
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
350
+ await expect(page.getByRole('row', { name: /Should Not Save/ })).not.toBeVisible();
351
+ });
352
+ ```
353
+
354
+ **JavaScript**
355
+
356
+ ```javascript
357
+ const { test, expect } = require('@playwright/test');
358
+
359
+ test('updates an existing product', async ({ page }) => {
360
+ await page.goto('/products');
361
+
362
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
363
+ await row.getByRole('button', { name: 'Edit' }).click();
364
+
365
+ await expect(page.getByLabel('Product name')).toHaveValue('Wireless Keyboard');
366
+ await expect(page.getByLabel('Price')).toHaveValue('79.99');
367
+
368
+ await page.getByLabel('Product name').clear();
369
+ await page.getByLabel('Product name').fill('Wireless Keyboard Pro');
370
+ await page.getByLabel('Price').clear();
371
+ await page.getByLabel('Price').fill('99.99');
372
+
373
+ await page.getByRole('button', { name: 'Save changes' }).click();
374
+
375
+ await expect(page.getByRole('alert')).toContainText('Product updated successfully');
376
+ await expect(page.getByRole('row', { name: /Wireless Keyboard Pro/ })).toBeVisible();
377
+ await expect(page.getByRole('row', { name: /Wireless Keyboard Pro/ })).toContainText('$99.99');
378
+ });
379
+
380
+ test('cancelling edit discards changes', async ({ page }) => {
381
+ await page.goto('/products');
382
+
383
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
384
+ await row.getByRole('button', { name: 'Edit' }).click();
385
+
386
+ await page.getByLabel('Product name').clear();
387
+ await page.getByLabel('Product name').fill('Should Not Save');
388
+
389
+ await page.getByRole('button', { name: 'Cancel' }).click();
390
+
391
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
392
+ await expect(page.getByRole('row', { name: /Should Not Save/ })).not.toBeVisible();
393
+ });
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Recipe 4: Deleting a Resource (Delete, Confirm Dialog, Verify Removal)
399
+
400
+ ### Complete Example
401
+
402
+ **TypeScript**
403
+
404
+ ```typescript
405
+ import { test, expect } from '@playwright/test';
406
+
407
+ test('deletes a product with confirmation dialog', async ({ page }) => {
408
+ await page.goto('/products');
409
+
410
+ // Count items before deletion
411
+ const rowsBefore = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
412
+ const countBefore = await rowsBefore.count();
413
+
414
+ // Click delete on a specific product
415
+ const targetRow = page.getByRole('row', { name: /Wireless Keyboard/ });
416
+ await expect(targetRow).toBeVisible();
417
+ await targetRow.getByRole('button', { name: 'Delete' }).click();
418
+
419
+ // Confirmation dialog should appear
420
+ const dialog = page.getByRole('dialog');
421
+ await expect(dialog).toBeVisible();
422
+ await expect(dialog).toContainText('Are you sure you want to delete');
423
+ await expect(dialog).toContainText('Wireless Keyboard');
424
+ await expect(dialog.getByText(/cannot be undone/i)).toBeVisible();
425
+
426
+ // Confirm the deletion
427
+ await dialog.getByRole('button', { name: 'Delete' }).click();
428
+
429
+ // Verify the dialog closes
430
+ await expect(dialog).not.toBeVisible();
431
+
432
+ // Verify success notification
433
+ await expect(page.getByRole('alert')).toContainText('Product deleted');
434
+
435
+ // Verify the item is removed from the list
436
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).not.toBeVisible();
437
+
438
+ // Verify count decreased
439
+ const rowsAfter = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
440
+ await expect(rowsAfter).toHaveCount(countBefore - 1);
441
+ });
442
+
443
+ test('cancel delete preserves the resource', async ({ page }) => {
444
+ await page.goto('/products');
445
+
446
+ const targetRow = page.getByRole('row', { name: /Wireless Keyboard/ });
447
+ await targetRow.getByRole('button', { name: 'Delete' }).click();
448
+
449
+ const dialog = page.getByRole('dialog');
450
+ await expect(dialog).toBeVisible();
451
+
452
+ // Cancel the deletion
453
+ await dialog.getByRole('button', { name: 'Cancel' }).click();
454
+
455
+ // Dialog closes, item is still present
456
+ await expect(dialog).not.toBeVisible();
457
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
458
+ });
459
+
460
+ test('handles delete error gracefully', async ({ page }) => {
461
+ // Mock the delete endpoint to fail
462
+ await page.route('**/api/products/*', async (route) => {
463
+ if (route.request().method() === 'DELETE') {
464
+ await route.fulfill({
465
+ status: 409,
466
+ contentType: 'application/json',
467
+ body: JSON.stringify({
468
+ error: 'Cannot delete product with active orders',
469
+ }),
470
+ });
471
+ } else {
472
+ await route.continue();
473
+ }
474
+ });
475
+
476
+ await page.goto('/products');
477
+
478
+ const targetRow = page.getByRole('row', { name: /Wireless Keyboard/ });
479
+ await targetRow.getByRole('button', { name: 'Delete' }).click();
480
+
481
+ const dialog = page.getByRole('dialog');
482
+ await dialog.getByRole('button', { name: 'Delete' }).click();
483
+
484
+ // Verify error is shown to user
485
+ await expect(page.getByRole('alert')).toContainText('Cannot delete product with active orders');
486
+
487
+ // Item should still be in the list
488
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
489
+ });
490
+ ```
491
+
492
+ **JavaScript**
493
+
494
+ ```javascript
495
+ const { test, expect } = require('@playwright/test');
496
+
497
+ test('deletes a product with confirmation dialog', async ({ page }) => {
498
+ await page.goto('/products');
499
+
500
+ const rowsBefore = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
501
+ const countBefore = await rowsBefore.count();
502
+
503
+ const targetRow = page.getByRole('row', { name: /Wireless Keyboard/ });
504
+ await targetRow.getByRole('button', { name: 'Delete' }).click();
505
+
506
+ const dialog = page.getByRole('dialog');
507
+ await expect(dialog).toBeVisible();
508
+ await expect(dialog).toContainText('Wireless Keyboard');
509
+
510
+ await dialog.getByRole('button', { name: 'Delete' }).click();
511
+
512
+ await expect(dialog).not.toBeVisible();
513
+ await expect(page.getByRole('alert')).toContainText('Product deleted');
514
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).not.toBeVisible();
515
+
516
+ const rowsAfter = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
517
+ await expect(rowsAfter).toHaveCount(countBefore - 1);
518
+ });
519
+
520
+ test('cancel delete preserves the resource', async ({ page }) => {
521
+ await page.goto('/products');
522
+
523
+ const targetRow = page.getByRole('row', { name: /Wireless Keyboard/ });
524
+ await targetRow.getByRole('button', { name: 'Delete' }).click();
525
+
526
+ const dialog = page.getByRole('dialog');
527
+ await dialog.getByRole('button', { name: 'Cancel' }).click();
528
+
529
+ await expect(dialog).not.toBeVisible();
530
+ await expect(page.getByRole('row', { name: /Wireless Keyboard/ })).toBeVisible();
531
+ });
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Recipe 5: Inline Editing
537
+
538
+ ### Complete Example
539
+
540
+ **TypeScript**
541
+
542
+ ```typescript
543
+ import { test, expect } from '@playwright/test';
544
+
545
+ test('edits a field inline by double-clicking', async ({ page }) => {
546
+ await page.goto('/products');
547
+
548
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
549
+ const nameCell = row.getByRole('cell').first();
550
+
551
+ // Double-click to enter edit mode
552
+ await nameCell.dblclick();
553
+
554
+ // The cell should now contain an input
555
+ const inlineInput = nameCell.getByRole('textbox');
556
+ await expect(inlineInput).toBeVisible();
557
+ await expect(inlineInput).toHaveValue('Wireless Keyboard');
558
+
559
+ // Edit the value
560
+ await inlineInput.clear();
561
+ await inlineInput.fill('Wireless Keyboard v2');
562
+
563
+ // Press Enter to save
564
+ await inlineInput.press('Enter');
565
+
566
+ // Verify the cell shows the updated value (no longer an input)
567
+ await expect(nameCell.getByRole('textbox')).not.toBeVisible();
568
+ await expect(nameCell).toHaveText('Wireless Keyboard v2');
569
+
570
+ // Verify a success indicator appears
571
+ await expect(page.getByRole('alert')).toContainText(/saved|updated/i);
572
+ });
573
+
574
+ test('cancels inline edit with Escape key', async ({ page }) => {
575
+ await page.goto('/products');
576
+
577
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
578
+ const nameCell = row.getByRole('cell').first();
579
+
580
+ await nameCell.dblclick();
581
+
582
+ const inlineInput = nameCell.getByRole('textbox');
583
+ await inlineInput.clear();
584
+ await inlineInput.fill('Temporary Value');
585
+
586
+ // Press Escape to cancel
587
+ await inlineInput.press('Escape');
588
+
589
+ // Verify original value is restored
590
+ await expect(nameCell).toHaveText('Wireless Keyboard');
591
+ await expect(nameCell.getByRole('textbox')).not.toBeVisible();
592
+ });
593
+
594
+ test('inline edit with click-away saves changes', async ({ page }) => {
595
+ await page.goto('/products');
596
+
597
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
598
+ const nameCell = row.getByRole('cell').first();
599
+
600
+ await nameCell.dblclick();
601
+
602
+ const inlineInput = nameCell.getByRole('textbox');
603
+ await inlineInput.clear();
604
+ await inlineInput.fill('Keyboard Updated');
605
+
606
+ // Click somewhere else on the page to trigger save
607
+ await page.getByRole('heading').first().click();
608
+
609
+ await expect(nameCell).toHaveText('Keyboard Updated');
610
+ });
611
+
612
+ test('inline edit shows validation error', async ({ page }) => {
613
+ await page.goto('/products');
614
+
615
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
616
+ const priceCell = row.getByRole('cell').nth(2);
617
+
618
+ await priceCell.dblclick();
619
+
620
+ const inlineInput = priceCell.getByRole('textbox');
621
+ await inlineInput.clear();
622
+ await inlineInput.fill('-10');
623
+ await inlineInput.press('Enter');
624
+
625
+ // Validation error should appear inline
626
+ await expect(priceCell.getByText(/positive number|invalid/i)).toBeVisible();
627
+
628
+ // Input should remain visible for correction
629
+ await expect(inlineInput).toBeVisible();
630
+ });
631
+ ```
632
+
633
+ **JavaScript**
634
+
635
+ ```javascript
636
+ const { test, expect } = require('@playwright/test');
637
+
638
+ test('edits a field inline by double-clicking', async ({ page }) => {
639
+ await page.goto('/products');
640
+
641
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
642
+ const nameCell = row.getByRole('cell').first();
643
+
644
+ await nameCell.dblclick();
645
+
646
+ const inlineInput = nameCell.getByRole('textbox');
647
+ await expect(inlineInput).toBeVisible();
648
+ await expect(inlineInput).toHaveValue('Wireless Keyboard');
649
+
650
+ await inlineInput.clear();
651
+ await inlineInput.fill('Wireless Keyboard v2');
652
+ await inlineInput.press('Enter');
653
+
654
+ await expect(nameCell.getByRole('textbox')).not.toBeVisible();
655
+ await expect(nameCell).toHaveText('Wireless Keyboard v2');
656
+ });
657
+
658
+ test('cancels inline edit with Escape key', async ({ page }) => {
659
+ await page.goto('/products');
660
+
661
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
662
+ const nameCell = row.getByRole('cell').first();
663
+
664
+ await nameCell.dblclick();
665
+ const inlineInput = nameCell.getByRole('textbox');
666
+ await inlineInput.clear();
667
+ await inlineInput.fill('Temporary Value');
668
+ await inlineInput.press('Escape');
669
+
670
+ await expect(nameCell).toHaveText('Wireless Keyboard');
671
+ });
672
+ ```
673
+
674
+ ---
675
+
676
+ ## Recipe 6: Bulk Operations
677
+
678
+ ### Complete Example
679
+
680
+ **TypeScript**
681
+
682
+ ```typescript
683
+ import { test, expect } from '@playwright/test';
684
+
685
+ test('selects multiple items and performs bulk delete', async ({ page }) => {
686
+ await page.goto('/products');
687
+
688
+ // Select multiple items via checkboxes
689
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
690
+
691
+ await rows.nth(0).getByRole('checkbox').check();
692
+ await rows.nth(1).getByRole('checkbox').check();
693
+ await rows.nth(2).getByRole('checkbox').check();
694
+
695
+ // Verify selection count is shown
696
+ await expect(page.getByText('3 items selected')).toBeVisible();
697
+
698
+ // Verify bulk action toolbar appears
699
+ const bulkToolbar = page.getByRole('toolbar', { name: /bulk actions/i });
700
+ await expect(bulkToolbar).toBeVisible();
701
+
702
+ // Click bulk delete
703
+ await bulkToolbar.getByRole('button', { name: 'Delete selected' }).click();
704
+
705
+ // Confirm in dialog
706
+ const dialog = page.getByRole('dialog');
707
+ await expect(dialog).toContainText('Delete 3 products');
708
+ await dialog.getByRole('button', { name: 'Delete' }).click();
709
+
710
+ // Verify items removed
711
+ await expect(page.getByRole('alert')).toContainText('3 products deleted');
712
+
713
+ // Verify selection count is cleared
714
+ await expect(page.getByText('items selected')).not.toBeVisible();
715
+ await expect(bulkToolbar).not.toBeVisible();
716
+ });
717
+
718
+ test('select all and deselect all', async ({ page }) => {
719
+ await page.goto('/products');
720
+
721
+ // Click "Select all" checkbox in the header
722
+ const selectAllCheckbox = page
723
+ .getByRole('row')
724
+ .filter({ has: page.getByRole('columnheader') })
725
+ .getByRole('checkbox');
726
+
727
+ await selectAllCheckbox.check();
728
+
729
+ // All row checkboxes should be checked
730
+ const rowCheckboxes = page
731
+ .getByRole('row')
732
+ .filter({ hasNot: page.getByRole('columnheader') })
733
+ .getByRole('checkbox');
734
+
735
+ const count = await rowCheckboxes.count();
736
+ for (let i = 0; i < count; i++) {
737
+ await expect(rowCheckboxes.nth(i)).toBeChecked();
738
+ }
739
+
740
+ await expect(page.getByText(`${count} items selected`)).toBeVisible();
741
+
742
+ // Deselect all
743
+ await selectAllCheckbox.uncheck();
744
+
745
+ for (let i = 0; i < count; i++) {
746
+ await expect(rowCheckboxes.nth(i)).not.toBeChecked();
747
+ }
748
+
749
+ await expect(page.getByText('items selected')).not.toBeVisible();
750
+ });
751
+
752
+ test('bulk status change', async ({ page }) => {
753
+ await page.goto('/products');
754
+
755
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
756
+ await rows.nth(0).getByRole('checkbox').check();
757
+ await rows.nth(1).getByRole('checkbox').check();
758
+
759
+ const bulkToolbar = page.getByRole('toolbar', { name: /bulk actions/i });
760
+ await bulkToolbar.getByRole('button', { name: 'Change status' }).click();
761
+
762
+ // Select new status from dropdown
763
+ await page.getByRole('menuitem', { name: 'Archived' }).click();
764
+
765
+ await expect(page.getByRole('alert')).toContainText('2 products archived');
766
+
767
+ // Verify status changed in the rows
768
+ await expect(rows.nth(0)).toContainText('Archived');
769
+ await expect(rows.nth(1)).toContainText('Archived');
770
+ });
771
+
772
+ test('bulk export selected items', async ({ page }) => {
773
+ await page.goto('/products');
774
+
775
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
776
+ await rows.nth(0).getByRole('checkbox').check();
777
+ await rows.nth(1).getByRole('checkbox').check();
778
+
779
+ const bulkToolbar = page.getByRole('toolbar', { name: /bulk actions/i });
780
+
781
+ // Start waiting for download before clicking
782
+ const downloadPromise = page.waitForEvent('download');
783
+ await bulkToolbar.getByRole('button', { name: 'Export' }).click();
784
+
785
+ const download = await downloadPromise;
786
+ expect(download.suggestedFilename()).toMatch(/products.*\.csv$/);
787
+ });
788
+ ```
789
+
790
+ **JavaScript**
791
+
792
+ ```javascript
793
+ const { test, expect } = require('@playwright/test');
794
+
795
+ test('selects multiple items and performs bulk delete', async ({ page }) => {
796
+ await page.goto('/products');
797
+
798
+ const rows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') });
799
+ await rows.nth(0).getByRole('checkbox').check();
800
+ await rows.nth(1).getByRole('checkbox').check();
801
+ await rows.nth(2).getByRole('checkbox').check();
802
+
803
+ await expect(page.getByText('3 items selected')).toBeVisible();
804
+
805
+ const bulkToolbar = page.getByRole('toolbar', { name: /bulk actions/i });
806
+ await bulkToolbar.getByRole('button', { name: 'Delete selected' }).click();
807
+
808
+ const dialog = page.getByRole('dialog');
809
+ await expect(dialog).toContainText('Delete 3 products');
810
+ await dialog.getByRole('button', { name: 'Delete' }).click();
811
+
812
+ await expect(page.getByRole('alert')).toContainText('3 products deleted');
813
+ });
814
+
815
+ test('select all and deselect all', async ({ page }) => {
816
+ await page.goto('/products');
817
+
818
+ const selectAllCheckbox = page
819
+ .getByRole('row')
820
+ .filter({ has: page.getByRole('columnheader') })
821
+ .getByRole('checkbox');
822
+
823
+ await selectAllCheckbox.check();
824
+
825
+ const rowCheckboxes = page
826
+ .getByRole('row')
827
+ .filter({ hasNot: page.getByRole('columnheader') })
828
+ .getByRole('checkbox');
829
+
830
+ const count = await rowCheckboxes.count();
831
+ for (let i = 0; i < count; i++) {
832
+ await expect(rowCheckboxes.nth(i)).toBeChecked();
833
+ }
834
+ });
835
+ ```
836
+
837
+ ---
838
+
839
+ ## Variations
840
+
841
+ ### Create with Multi-Step Wizard
842
+
843
+ ```typescript
844
+ test('creates a resource through a multi-step wizard', async ({ page }) => {
845
+ await page.goto('/products');
846
+ await page.getByRole('button', { name: 'Add product' }).click();
847
+
848
+ // Step 1: Basic info
849
+ await expect(page.getByText('Step 1 of 3')).toBeVisible();
850
+ await page.getByLabel('Product name').fill('Wireless Keyboard');
851
+ await page.getByLabel('Category').selectOption('Electronics');
852
+ await page.getByRole('button', { name: 'Next' }).click();
853
+
854
+ // Step 2: Pricing
855
+ await expect(page.getByText('Step 2 of 3')).toBeVisible();
856
+ await page.getByLabel('Price').fill('79.99');
857
+ await page.getByLabel('Tax rate').selectOption('Standard (20%)');
858
+ await page.getByRole('button', { name: 'Next' }).click();
859
+
860
+ // Step 3: Review and confirm
861
+ await expect(page.getByText('Step 3 of 3')).toBeVisible();
862
+ await expect(page.getByText('Wireless Keyboard')).toBeVisible();
863
+ await expect(page.getByText('$79.99')).toBeVisible();
864
+ await page.getByRole('button', { name: 'Create product' }).click();
865
+
866
+ await expect(page.getByRole('alert')).toContainText('Product created');
867
+ });
868
+ ```
869
+
870
+ ### CRUD with API Verification
871
+
872
+ ```typescript
873
+ test('create and verify via API', async ({ page, request }) => {
874
+ await page.goto('/products');
875
+ await page.getByRole('button', { name: 'Add product' }).click();
876
+
877
+ await page.getByLabel('Product name').fill('API Verified Product');
878
+ await page.getByLabel('Price').fill('49.99');
879
+ await page.getByRole('button', { name: 'Save product' }).click();
880
+
881
+ await expect(page.getByRole('alert')).toContainText('Product created');
882
+
883
+ // Also verify via API that the data was persisted correctly
884
+ const response = await request.get('/api/products?search=API+Verified+Product');
885
+ const data = await response.json();
886
+
887
+ expect(data.products).toHaveLength(1);
888
+ expect(data.products[0].name).toBe('API Verified Product');
889
+ expect(data.products[0].price).toBe(49.99);
890
+ });
891
+ ```
892
+
893
+ ### Optimistic UI Updates
894
+
895
+ ```typescript
896
+ test('shows optimistic update then confirms', async ({ page }) => {
897
+ // Slow down the API response to observe optimistic behavior
898
+ await page.route('**/api/products/*', async (route) => {
899
+ if (route.request().method() === 'PATCH') {
900
+ await new Promise((resolve) => setTimeout(resolve, 2000));
901
+ await route.continue();
902
+ } else {
903
+ await route.continue();
904
+ }
905
+ });
906
+
907
+ await page.goto('/products');
908
+ const row = page.getByRole('row', { name: /Wireless Keyboard/ });
909
+
910
+ await row.getByRole('button', { name: 'Toggle status' }).click();
911
+
912
+ // Verify UI updates immediately (optimistic)
913
+ await expect(row.getByText('Active')).toBeVisible();
914
+
915
+ // Verify loading indicator while API catches up
916
+ await expect(row.getByRole('progressbar')).toBeVisible();
917
+
918
+ // After API responds, loading indicator disappears
919
+ await expect(row.getByRole('progressbar')).not.toBeVisible({ timeout: 5000 });
920
+ await expect(row.getByText('Active')).toBeVisible();
921
+ });
922
+ ```
923
+
924
+ ---
925
+
926
+ ## Tips
927
+
928
+ 1. **Always verify state after mutations**. After a create, update, or delete, assert that the UI reflects the change. Do not assume the success notification alone means the operation worked. Check that the list, table, or card actually changed.
929
+
930
+ 2. **Use unique identifiers in test data**. Include timestamps or random strings in test data to prevent collisions: `Keyboard-${Date.now()}`. This avoids flaky tests from leftover data.
931
+
932
+ 3. **Test the full lifecycle**. Write a single test that creates, reads, updates, and then deletes a resource. This catches integration issues between operations that isolated tests miss.
933
+
934
+ 4. **Clean up test data via API**. Use `test.afterEach` or `test.afterAll` with the `request` fixture to delete resources created during tests, so tests remain independent and repeatable.
935
+
936
+ 5. **Prefer `getByRole('row', { name: ... })` for table operations**. This targets accessible row content and is resilient to column reordering. Avoid relying on `nth()` indices for specific data rows since ordering can change.
937
+
938
+ ---
939
+
940
+ ## Related
941
+
942
+ - `recipes/search-and-filter.md` -- Filter and search within resource lists
943
+ - `recipes/file-upload-download.md` -- Upload files as part of resource creation
944
+ - `patterns/page-objects.md` -- Encapsulate CRUD forms in page objects
945
+ - `foundations/selectors.md` -- Best practices for selecting table cells and form fields