@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,992 @@
1
+ # Drag and Drop Recipes
2
+
3
+ > **When to use**: You need to test drag-and-drop interactions -- sortable lists, kanban boards, file drop zones, or any element that can be repositioned by dragging.
4
+
5
+ ---
6
+
7
+ ## Recipe 1: Native HTML5 Drag and Drop
8
+
9
+ ### Complete Example
10
+
11
+ **TypeScript**
12
+
13
+ ```typescript
14
+ import { test, expect } from '@playwright/test';
15
+
16
+ test('drags an item from one container to another', async ({ page }) => {
17
+ await page.goto('/drag-demo');
18
+
19
+ const sourceItem = page.getByText('Draggable Item');
20
+ const dropZone = page.locator('#drop-zone');
21
+
22
+ // Verify initial state
23
+ await expect(sourceItem).toBeVisible();
24
+ await expect(dropZone).not.toContainText('Draggable Item');
25
+
26
+ // Perform drag and drop
27
+ await sourceItem.dragTo(dropZone);
28
+
29
+ // Verify the item moved to the drop zone
30
+ await expect(dropZone).toContainText('Draggable Item');
31
+ });
32
+
33
+ test('drags between two drop zones using locators', async ({ page }) => {
34
+ await page.goto('/drag-demo');
35
+
36
+ const item = page.locator('[data-testid="item-1"]');
37
+ const zoneA = page.locator('[data-testid="zone-a"]');
38
+ const zoneB = page.locator('[data-testid="zone-b"]');
39
+
40
+ // Item starts in zone A
41
+ await expect(zoneA).toContainText('Item 1');
42
+
43
+ // Drag to zone B
44
+ await item.dragTo(zoneB);
45
+
46
+ // Item is now in zone B, not zone A
47
+ await expect(zoneB).toContainText('Item 1');
48
+ await expect(zoneA).not.toContainText('Item 1');
49
+
50
+ // Drag back to zone A
51
+ await zoneB.getByText('Item 1').dragTo(zoneA);
52
+
53
+ await expect(zoneA).toContainText('Item 1');
54
+ await expect(zoneB).not.toContainText('Item 1');
55
+ });
56
+
57
+ test('verifies drag visual feedback', async ({ page }) => {
58
+ await page.goto('/drag-demo');
59
+
60
+ const sourceItem = page.getByText('Draggable Item');
61
+ const dropZone = page.locator('#drop-zone');
62
+
63
+ // Start drag manually to check intermediate states
64
+ await sourceItem.hover();
65
+ await page.mouse.down();
66
+
67
+ // Move toward the drop zone
68
+ const dropBox = await dropZone.boundingBox();
69
+ await page.mouse.move(dropBox!.x + dropBox!.width / 2, dropBox!.y + dropBox!.height / 2);
70
+
71
+ // Verify the drop zone shows a visual indicator while hovering
72
+ await expect(dropZone).toHaveClass(/drag-over|highlight/);
73
+
74
+ // Complete the drop
75
+ await page.mouse.up();
76
+
77
+ // Verify drop zone returns to normal styling
78
+ await expect(dropZone).not.toHaveClass(/drag-over|highlight/);
79
+ await expect(dropZone).toContainText('Draggable Item');
80
+ });
81
+ ```
82
+
83
+ **JavaScript**
84
+
85
+ ```javascript
86
+ const { test, expect } = require('@playwright/test');
87
+
88
+ test('drags an item from one container to another', async ({ page }) => {
89
+ await page.goto('/drag-demo');
90
+
91
+ const sourceItem = page.getByText('Draggable Item');
92
+ const dropZone = page.locator('#drop-zone');
93
+
94
+ await expect(sourceItem).toBeVisible();
95
+ await expect(dropZone).not.toContainText('Draggable Item');
96
+
97
+ await sourceItem.dragTo(dropZone);
98
+
99
+ await expect(dropZone).toContainText('Draggable Item');
100
+ });
101
+
102
+ test('drags between two drop zones', async ({ page }) => {
103
+ await page.goto('/drag-demo');
104
+
105
+ const item = page.locator('[data-testid="item-1"]');
106
+ const zoneA = page.locator('[data-testid="zone-a"]');
107
+ const zoneB = page.locator('[data-testid="zone-b"]');
108
+
109
+ await expect(zoneA).toContainText('Item 1');
110
+
111
+ await item.dragTo(zoneB);
112
+
113
+ await expect(zoneB).toContainText('Item 1');
114
+ await expect(zoneA).not.toContainText('Item 1');
115
+ });
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Recipe 2: Sortable Lists (Reordering Items)
121
+
122
+ ### Complete Example
123
+
124
+ **TypeScript**
125
+
126
+ ```typescript
127
+ import { test, expect } from '@playwright/test';
128
+
129
+ test('reorders items in a sortable list', async ({ page }) => {
130
+ await page.goto('/tasks');
131
+
132
+ const list = page.getByRole('list', { name: 'Task list' });
133
+
134
+ // Verify initial order
135
+ const initialItems = await list.getByRole('listitem').allTextContents();
136
+ expect(initialItems[0]).toContain('Task A');
137
+ expect(initialItems[1]).toContain('Task B');
138
+ expect(initialItems[2]).toContain('Task C');
139
+
140
+ // Drag Task C to the top (above Task A)
141
+ const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' });
142
+ const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' });
143
+
144
+ await taskC.dragTo(taskA);
145
+
146
+ // Verify new order
147
+ const reorderedItems = await list.getByRole('listitem').allTextContents();
148
+ expect(reorderedItems[0]).toContain('Task C');
149
+ expect(reorderedItems[1]).toContain('Task A');
150
+ expect(reorderedItems[2]).toContain('Task B');
151
+ });
152
+
153
+ test('reorders using drag handle', async ({ page }) => {
154
+ await page.goto('/tasks');
155
+
156
+ const list = page.getByRole('list', { name: 'Task list' });
157
+
158
+ // Use the drag handle (grip icon) instead of the whole item
159
+ const dragHandle = list
160
+ .getByRole('listitem')
161
+ .filter({ hasText: 'Task C' })
162
+ .getByRole('button', { name: /drag|reorder|grip/i });
163
+
164
+ const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' });
165
+
166
+ await dragHandle.dragTo(targetItem);
167
+
168
+ const reorderedItems = await list.getByRole('listitem').allTextContents();
169
+ expect(reorderedItems[0]).toContain('Task C');
170
+ });
171
+
172
+ test('reorder persists after page reload', async ({ page }) => {
173
+ await page.goto('/tasks');
174
+
175
+ const list = page.getByRole('list', { name: 'Task list' });
176
+
177
+ const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' });
178
+ const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' });
179
+
180
+ await taskC.dragTo(taskA);
181
+
182
+ // Wait for the save API call to complete
183
+ await page.waitForResponse((response) =>
184
+ response.url().includes('/api/tasks/reorder') && response.status() === 200
185
+ );
186
+
187
+ // Reload and verify persistence
188
+ await page.reload();
189
+
190
+ const items = await list.getByRole('listitem').allTextContents();
191
+ expect(items[0]).toContain('Task C');
192
+ expect(items[1]).toContain('Task A');
193
+ expect(items[2]).toContain('Task B');
194
+ });
195
+
196
+ test('reorders with precise mouse movements for libraries that need it', async ({ page }) => {
197
+ await page.goto('/tasks');
198
+
199
+ const list = page.getByRole('list', { name: 'Task list' });
200
+ const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' });
201
+ const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' });
202
+
203
+ const sourceBox = await sourceItem.boundingBox();
204
+ const targetBox = await targetItem.boundingBox();
205
+
206
+ // Some drag libraries (react-beautiful-dnd, dnd-kit) require specific mouse event sequences
207
+ await sourceItem.hover();
208
+ await page.mouse.down();
209
+
210
+ // Move in small steps -- some libraries require incremental movement to register
211
+ const steps = 10;
212
+ for (let i = 1; i <= steps; i++) {
213
+ await page.mouse.move(
214
+ sourceBox!.x + sourceBox!.width / 2,
215
+ sourceBox!.y + (targetBox!.y - sourceBox!.y) * (i / steps),
216
+ { steps: 1 }
217
+ );
218
+ }
219
+
220
+ await page.mouse.up();
221
+
222
+ const items = await list.getByRole('listitem').allTextContents();
223
+ expect(items[0]).toContain('Task C');
224
+ });
225
+ ```
226
+
227
+ **JavaScript**
228
+
229
+ ```javascript
230
+ const { test, expect } = require('@playwright/test');
231
+
232
+ test('reorders items in a sortable list', async ({ page }) => {
233
+ await page.goto('/tasks');
234
+
235
+ const list = page.getByRole('list', { name: 'Task list' });
236
+
237
+ const initialItems = await list.getByRole('listitem').allTextContents();
238
+ expect(initialItems[0]).toContain('Task A');
239
+ expect(initialItems[1]).toContain('Task B');
240
+ expect(initialItems[2]).toContain('Task C');
241
+
242
+ const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' });
243
+ const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' });
244
+
245
+ await taskC.dragTo(taskA);
246
+
247
+ const reorderedItems = await list.getByRole('listitem').allTextContents();
248
+ expect(reorderedItems[0]).toContain('Task C');
249
+ expect(reorderedItems[1]).toContain('Task A');
250
+ expect(reorderedItems[2]).toContain('Task B');
251
+ });
252
+
253
+ test('reorders with precise mouse movements', async ({ page }) => {
254
+ await page.goto('/tasks');
255
+
256
+ const list = page.getByRole('list', { name: 'Task list' });
257
+ const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' });
258
+ const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' });
259
+
260
+ const sourceBox = await sourceItem.boundingBox();
261
+ const targetBox = await targetItem.boundingBox();
262
+
263
+ await sourceItem.hover();
264
+ await page.mouse.down();
265
+
266
+ const steps = 10;
267
+ for (let i = 1; i <= steps; i++) {
268
+ await page.mouse.move(
269
+ sourceBox.x + sourceBox.width / 2,
270
+ sourceBox.y + (targetBox.y - sourceBox.y) * (i / steps),
271
+ { steps: 1 }
272
+ );
273
+ }
274
+
275
+ await page.mouse.up();
276
+
277
+ const items = await list.getByRole('listitem').allTextContents();
278
+ expect(items[0]).toContain('Task C');
279
+ });
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Recipe 3: Kanban Board (Moving Between Columns)
285
+
286
+ ### Complete Example
287
+
288
+ **TypeScript**
289
+
290
+ ```typescript
291
+ import { test, expect } from '@playwright/test';
292
+
293
+ test('moves a card from Todo to In Progress', async ({ page }) => {
294
+ await page.goto('/board');
295
+
296
+ const todoColumn = page.locator('[data-column="todo"]');
297
+ const inProgressColumn = page.locator('[data-column="in-progress"]');
298
+
299
+ // Verify initial state
300
+ const card = todoColumn.getByText('Fix login bug');
301
+ await expect(card).toBeVisible();
302
+
303
+ const todoCountBefore = await todoColumn.getByRole('article').count();
304
+ const inProgressCountBefore = await inProgressColumn.getByRole('article').count();
305
+
306
+ // Drag the card to In Progress
307
+ await card.dragTo(inProgressColumn);
308
+
309
+ // Verify the card moved
310
+ await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible();
311
+ await expect(todoColumn.getByText('Fix login bug')).not.toBeVisible();
312
+
313
+ // Verify counts updated
314
+ await expect(todoColumn.getByRole('article')).toHaveCount(todoCountBefore - 1);
315
+ await expect(inProgressColumn.getByRole('article')).toHaveCount(inProgressCountBefore + 1);
316
+
317
+ // Verify column header count badge updated
318
+ await expect(todoColumn.getByText(`(${todoCountBefore - 1})`)).toBeVisible();
319
+ await expect(inProgressColumn.getByText(`(${inProgressCountBefore + 1})`)).toBeVisible();
320
+ });
321
+
322
+ test('moves a card through all stages', async ({ page }) => {
323
+ await page.goto('/board');
324
+
325
+ const columns = {
326
+ todo: page.locator('[data-column="todo"]'),
327
+ inProgress: page.locator('[data-column="in-progress"]'),
328
+ review: page.locator('[data-column="review"]'),
329
+ done: page.locator('[data-column="done"]'),
330
+ };
331
+
332
+ // Todo -> In Progress
333
+ await columns.todo.getByText('Fix login bug').dragTo(columns.inProgress);
334
+ await expect(columns.inProgress.getByText('Fix login bug')).toBeVisible();
335
+
336
+ // In Progress -> Review
337
+ await columns.inProgress.getByText('Fix login bug').dragTo(columns.review);
338
+ await expect(columns.review.getByText('Fix login bug')).toBeVisible();
339
+
340
+ // Review -> Done
341
+ await columns.review.getByText('Fix login bug').dragTo(columns.done);
342
+ await expect(columns.done.getByText('Fix login bug')).toBeVisible();
343
+
344
+ // Verify the card is only in the Done column
345
+ await expect(columns.todo.getByText('Fix login bug')).not.toBeVisible();
346
+ await expect(columns.inProgress.getByText('Fix login bug')).not.toBeVisible();
347
+ await expect(columns.review.getByText('Fix login bug')).not.toBeVisible();
348
+ });
349
+
350
+ test('reorders cards within the same column', async ({ page }) => {
351
+ await page.goto('/board');
352
+
353
+ const todoColumn = page.locator('[data-column="todo"]');
354
+
355
+ const cardA = todoColumn.getByRole('article').filter({ hasText: 'Task A' });
356
+ const cardC = todoColumn.getByRole('article').filter({ hasText: 'Task C' });
357
+
358
+ // Drag Task C above Task A within the same column
359
+ await cardC.dragTo(cardA);
360
+
361
+ // Verify new order within the column
362
+ const cards = await todoColumn.getByRole('article').allTextContents();
363
+ expect(cards.indexOf('Task C')).toBeLessThan(cards.indexOf('Task A'));
364
+ });
365
+
366
+ test('kanban board state persists via API', async ({ page }) => {
367
+ await page.goto('/board');
368
+
369
+ const todoColumn = page.locator('[data-column="todo"]');
370
+ const inProgressColumn = page.locator('[data-column="in-progress"]');
371
+
372
+ // Wait for the PATCH/PUT to complete after the drag
373
+ const responsePromise = page.waitForResponse(
374
+ (r) => r.url().includes('/api/cards') && r.request().method() === 'PATCH'
375
+ );
376
+
377
+ await todoColumn.getByText('Fix login bug').dragTo(inProgressColumn);
378
+
379
+ const response = await responsePromise;
380
+ expect(response.status()).toBe(200);
381
+
382
+ const body = await response.json();
383
+ expect(body.column).toBe('in-progress');
384
+
385
+ // Reload to confirm persistence
386
+ await page.reload();
387
+ await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible();
388
+ });
389
+ ```
390
+
391
+ **JavaScript**
392
+
393
+ ```javascript
394
+ const { test, expect } = require('@playwright/test');
395
+
396
+ test('moves a card from Todo to In Progress', async ({ page }) => {
397
+ await page.goto('/board');
398
+
399
+ const todoColumn = page.locator('[data-column="todo"]');
400
+ const inProgressColumn = page.locator('[data-column="in-progress"]');
401
+
402
+ const card = todoColumn.getByText('Fix login bug');
403
+ await expect(card).toBeVisible();
404
+
405
+ await card.dragTo(inProgressColumn);
406
+
407
+ await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible();
408
+ await expect(todoColumn.getByText('Fix login bug')).not.toBeVisible();
409
+ });
410
+
411
+ test('moves a card through all stages', async ({ page }) => {
412
+ await page.goto('/board');
413
+
414
+ const columns = {
415
+ todo: page.locator('[data-column="todo"]'),
416
+ inProgress: page.locator('[data-column="in-progress"]'),
417
+ review: page.locator('[data-column="review"]'),
418
+ done: page.locator('[data-column="done"]'),
419
+ };
420
+
421
+ await columns.todo.getByText('Fix login bug').dragTo(columns.inProgress);
422
+ await expect(columns.inProgress.getByText('Fix login bug')).toBeVisible();
423
+
424
+ await columns.inProgress.getByText('Fix login bug').dragTo(columns.review);
425
+ await expect(columns.review.getByText('Fix login bug')).toBeVisible();
426
+
427
+ await columns.review.getByText('Fix login bug').dragTo(columns.done);
428
+ await expect(columns.done.getByText('Fix login bug')).toBeVisible();
429
+ });
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Recipe 4: File Drop Zone
435
+
436
+ > **Playwright 1.60+**: `locator.drop()` now simulates a *native* external file drop — dispatching real `dragenter` / `dragover` / `drop` events with a synthetic `DataTransfer`. This is the preferred approach for drop zones that listen for `drop` events (not just a hidden `input[type=file]`). On earlier versions, fall back to `setInputFiles` on the underlying file input (shown below).
437
+
438
+ ### Recommended: `locator.drop()` (Playwright 1.60+)
439
+
440
+ **Use when**: The drop zone is driven by `drop`/`dragover` DOM events (most modern uploaders, editors that accept dropped images, or zones with no `input[type=file]` fallback).
441
+ **Avoid when**: You're on Playwright < 1.60 — use the `setInputFiles` approach instead.
442
+
443
+ **TypeScript**
444
+
445
+ ```typescript
446
+ import { test, expect } from '@playwright/test';
447
+ import path from 'path';
448
+
449
+ test('drops a file directly onto the drop zone', async ({ page }) => {
450
+ await page.goto('/upload');
451
+
452
+ const dropZone = page.locator('[data-testid="file-drop-zone"]');
453
+ await expect(dropZone).toContainText('Drag files here');
454
+
455
+ // Drop a real file from disk — fires native dragenter/dragover/drop events
456
+ await dropZone.drop({
457
+ files: path.resolve(__dirname, '../fixtures/sample-document.pdf'),
458
+ });
459
+
460
+ await expect(page.getByText('sample-document.pdf')).toBeVisible();
461
+ });
462
+
463
+ test('drops an in-memory file buffer', async ({ page }) => {
464
+ await page.goto('/upload');
465
+
466
+ await page.locator('[data-testid="file-drop-zone"]').drop({
467
+ files: {
468
+ name: 'note.txt',
469
+ mimeType: 'text/plain',
470
+ buffer: Buffer.from('hello from a dropped file'),
471
+ },
472
+ });
473
+
474
+ await expect(page.getByText('note.txt')).toBeVisible();
475
+ });
476
+
477
+ test('drops text/url payloads (not a file)', async ({ page }) => {
478
+ await page.goto('/editor');
479
+
480
+ // `data` maps MIME types to string values — great for dropped links/text
481
+ await page.locator('#canvas').drop({
482
+ data: {
483
+ 'text/plain': 'Hello world',
484
+ 'text/uri-list': 'https://example.com/image.png',
485
+ },
486
+ });
487
+
488
+ await expect(page.getByText('https://example.com/image.png')).toBeVisible();
489
+ });
490
+ ```
491
+
492
+ **JavaScript**
493
+
494
+ ```javascript
495
+ const { test, expect } = require('@playwright/test');
496
+ const path = require('path');
497
+
498
+ test('drops a file directly onto the drop zone', async ({ page }) => {
499
+ await page.goto('/upload');
500
+
501
+ const dropZone = page.locator('[data-testid="file-drop-zone"]');
502
+ await dropZone.drop({
503
+ files: path.resolve(__dirname, '../fixtures/sample-document.pdf'),
504
+ });
505
+
506
+ await expect(page.getByText('sample-document.pdf')).toBeVisible();
507
+ });
508
+ ```
509
+
510
+ ### Legacy / fallback: file input + dispatched events
511
+
512
+ **TypeScript**
513
+
514
+ ```typescript
515
+ import { test, expect } from '@playwright/test';
516
+ import path from 'path';
517
+
518
+ test('uploads a file via the underlying input (Playwright < 1.60)', async ({ page }) => {
519
+ await page.goto('/upload');
520
+
521
+ const dropZone = page.locator('[data-testid="file-drop-zone"]');
522
+
523
+ // Verify initial state
524
+ await expect(dropZone).toContainText('Drag files here');
525
+
526
+ // On older Playwright, native OS file drops can't be simulated — drive the
527
+ // underlying input[type=file] that most drop zones fall back to instead.
528
+ const fileInput = page.locator('input[type="file"]');
529
+
530
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/sample-document.pdf'));
531
+
532
+ // Verify file appears in the upload list
533
+ await expect(page.getByText('sample-document.pdf')).toBeVisible();
534
+ await expect(page.getByText(/\d+ KB/)).toBeVisible();
535
+ });
536
+
537
+ test('simulates drag-over visual feedback via JavaScript dispatch', async ({ page }) => {
538
+ await page.goto('/upload');
539
+
540
+ const dropZone = page.locator('[data-testid="file-drop-zone"]');
541
+
542
+ // Dispatch dragenter/dragover events to test visual feedback
543
+ await dropZone.dispatchEvent('dragenter', {
544
+ dataTransfer: { types: ['Files'] },
545
+ });
546
+
547
+ // Verify the drop zone shows the active/hover state
548
+ await expect(dropZone).toHaveClass(/drag-active|drop-highlight/);
549
+ await expect(dropZone).toContainText(/drop.*here|release.*upload/i);
550
+
551
+ // Dispatch dragleave to reset
552
+ await dropZone.dispatchEvent('dragleave');
553
+
554
+ await expect(dropZone).not.toHaveClass(/drag-active|drop-highlight/);
555
+ });
556
+
557
+ test('rejects invalid file types in drop zone', async ({ page }) => {
558
+ await page.goto('/upload');
559
+
560
+ const fileInput = page.locator('input[type="file"]');
561
+
562
+ // Try to upload a file type that is not allowed
563
+ await fileInput.setInputFiles({
564
+ name: 'malware.exe',
565
+ mimeType: 'application/x-msdownload',
566
+ buffer: Buffer.from('fake-content'),
567
+ });
568
+
569
+ await expect(page.getByRole('alert')).toContainText(/not allowed|invalid file type/i);
570
+ await expect(page.getByText('malware.exe')).not.toBeVisible();
571
+ });
572
+ ```
573
+
574
+ **JavaScript**
575
+
576
+ ```javascript
577
+ const { test, expect } = require('@playwright/test');
578
+ const path = require('path');
579
+
580
+ test('uploads a file by dropping it on the drop zone', async ({ page }) => {
581
+ await page.goto('/upload');
582
+
583
+ const dropZone = page.locator('[data-testid="file-drop-zone"]');
584
+ await expect(dropZone).toContainText('Drag files here');
585
+
586
+ const fileInput = page.locator('input[type="file"]');
587
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/sample-document.pdf'));
588
+
589
+ await expect(page.getByText('sample-document.pdf')).toBeVisible();
590
+ });
591
+
592
+ test('rejects invalid file types in drop zone', async ({ page }) => {
593
+ await page.goto('/upload');
594
+
595
+ const fileInput = page.locator('input[type="file"]');
596
+
597
+ await fileInput.setInputFiles({
598
+ name: 'malware.exe',
599
+ mimeType: 'application/x-msdownload',
600
+ buffer: Buffer.from('fake-content'),
601
+ });
602
+
603
+ await expect(page.getByRole('alert')).toContainText(/not allowed|invalid file type/i);
604
+ });
605
+ ```
606
+
607
+ ---
608
+
609
+ ## Recipe 5: Drag with Custom Preview / Ghost Image
610
+
611
+ ### Complete Example
612
+
613
+ **TypeScript**
614
+
615
+ ```typescript
616
+ import { test, expect } from '@playwright/test';
617
+
618
+ test('shows custom drag preview during drag operation', async ({ page }) => {
619
+ await page.goto('/board');
620
+
621
+ const card = page.locator('[data-testid="card-1"]');
622
+ const targetColumn = page.locator('[data-column="in-progress"]');
623
+
624
+ const cardBox = await card.boundingBox();
625
+ const targetBox = await targetColumn.boundingBox();
626
+
627
+ // Start dragging
628
+ await card.hover();
629
+ await page.mouse.down();
630
+
631
+ // Move partially toward the target
632
+ const midX = (cardBox!.x + targetBox!.x) / 2;
633
+ const midY = (cardBox!.y + targetBox!.y) / 2;
634
+ await page.mouse.move(midX, midY, { steps: 5 });
635
+
636
+ // Take a screenshot to visually verify the drag preview
637
+ // The custom preview element should be visible during drag
638
+ await expect(page.locator('.drag-preview')).toBeVisible();
639
+
640
+ // Verify the original card shows a placeholder/ghost
641
+ await expect(card).toHaveClass(/dragging|placeholder/);
642
+
643
+ // Complete the drag
644
+ await page.mouse.move(
645
+ targetBox!.x + targetBox!.width / 2,
646
+ targetBox!.y + targetBox!.height / 2,
647
+ { steps: 5 }
648
+ );
649
+ await page.mouse.up();
650
+
651
+ // Preview should disappear after drop
652
+ await expect(page.locator('.drag-preview')).not.toBeVisible();
653
+ });
654
+
655
+ test('drag preview shows item count for multi-select drag', async ({ page }) => {
656
+ await page.goto('/board');
657
+
658
+ // Select multiple cards
659
+ await page.locator('[data-testid="card-1"]').click();
660
+ await page.locator('[data-testid="card-2"]').click({ modifiers: ['Shift'] });
661
+ await page.locator('[data-testid="card-3"]').click({ modifiers: ['Shift'] });
662
+
663
+ // Start dragging one of the selected cards
664
+ const card = page.locator('[data-testid="card-1"]');
665
+ const targetColumn = page.locator('[data-column="done"]');
666
+
667
+ await card.hover();
668
+ await page.mouse.down();
669
+
670
+ const targetBox = await targetColumn.boundingBox();
671
+ await page.mouse.move(targetBox!.x + 50, targetBox!.y + 50, { steps: 5 });
672
+
673
+ // Verify the preview shows the count of selected items
674
+ await expect(page.locator('.drag-preview')).toContainText('3 items');
675
+
676
+ await page.mouse.up();
677
+
678
+ // All three cards should be in the target column
679
+ await expect(targetColumn.locator('[data-testid="card-1"]')).toBeVisible();
680
+ await expect(targetColumn.locator('[data-testid="card-2"]')).toBeVisible();
681
+ await expect(targetColumn.locator('[data-testid="card-3"]')).toBeVisible();
682
+ });
683
+ ```
684
+
685
+ **JavaScript**
686
+
687
+ ```javascript
688
+ const { test, expect } = require('@playwright/test');
689
+
690
+ test('shows custom drag preview during drag operation', async ({ page }) => {
691
+ await page.goto('/board');
692
+
693
+ const card = page.locator('[data-testid="card-1"]');
694
+ const targetColumn = page.locator('[data-column="in-progress"]');
695
+
696
+ const cardBox = await card.boundingBox();
697
+ const targetBox = await targetColumn.boundingBox();
698
+
699
+ await card.hover();
700
+ await page.mouse.down();
701
+
702
+ const midX = (cardBox.x + targetBox.x) / 2;
703
+ const midY = (cardBox.y + targetBox.y) / 2;
704
+ await page.mouse.move(midX, midY, { steps: 5 });
705
+
706
+ await expect(page.locator('.drag-preview')).toBeVisible();
707
+ await expect(card).toHaveClass(/dragging|placeholder/);
708
+
709
+ await page.mouse.move(
710
+ targetBox.x + targetBox.width / 2,
711
+ targetBox.y + targetBox.height / 2,
712
+ { steps: 5 }
713
+ );
714
+ await page.mouse.up();
715
+
716
+ await expect(page.locator('.drag-preview')).not.toBeVisible();
717
+ });
718
+ ```
719
+
720
+ ---
721
+
722
+ ## Recipe 6: Testing Drag Position and Coordinates
723
+
724
+ ### Complete Example
725
+
726
+ **TypeScript**
727
+
728
+ ```typescript
729
+ import { test, expect } from '@playwright/test';
730
+
731
+ test('drags an element to a specific coordinate on a canvas', async ({ page }) => {
732
+ await page.goto('/canvas-editor');
733
+
734
+ const canvas = page.locator('#design-canvas');
735
+ const element = page.locator('[data-testid="shape-1"]');
736
+
737
+ // Get the initial position
738
+ const initialBox = await element.boundingBox();
739
+ expect(initialBox).toBeTruthy();
740
+
741
+ // Define target position (absolute coordinates within the canvas)
742
+ const canvasBox = await canvas.boundingBox();
743
+ const targetX = canvasBox!.x + 300;
744
+ const targetY = canvasBox!.y + 200;
745
+
746
+ // Drag to specific coordinates
747
+ await element.hover();
748
+ await page.mouse.down();
749
+ await page.mouse.move(targetX, targetY, { steps: 10 });
750
+ await page.mouse.up();
751
+
752
+ // Verify element moved to approximately the right position
753
+ const newBox = await element.boundingBox();
754
+ expect(newBox!.x).toBeCloseTo(targetX - newBox!.width / 2, -1);
755
+ expect(newBox!.y).toBeCloseTo(targetY - newBox!.height / 2, -1);
756
+ });
757
+
758
+ test('snaps element to grid when dropped', async ({ page }) => {
759
+ await page.goto('/canvas-editor');
760
+
761
+ const element = page.locator('[data-testid="shape-1"]');
762
+ const canvas = page.locator('#design-canvas');
763
+
764
+ const canvasBox = await canvas.boundingBox();
765
+
766
+ // Drag to a position that is not on the grid
767
+ await element.hover();
768
+ await page.mouse.down();
769
+ await page.mouse.move(canvasBox!.x + 147, canvasBox!.y + 83, { steps: 10 });
770
+ await page.mouse.up();
771
+
772
+ // With a 20px grid, position should snap to nearest grid point
773
+ const snappedBox = await element.boundingBox();
774
+ expect(snappedBox!.x % 20).toBeCloseTo(0, 0);
775
+ expect(snappedBox!.y % 20).toBeCloseTo(0, 0);
776
+ });
777
+
778
+ test('constrains drag within boundaries', async ({ page }) => {
779
+ await page.goto('/canvas-editor');
780
+
781
+ const element = page.locator('[data-testid="constrained-element"]');
782
+ const container = page.locator('#constraint-box');
783
+
784
+ const containerBox = await container.boundingBox();
785
+
786
+ // Try to drag far outside the container
787
+ await element.hover();
788
+ await page.mouse.down();
789
+ await page.mouse.move(containerBox!.x + containerBox!.width + 500, containerBox!.y - 200, {
790
+ steps: 10,
791
+ });
792
+ await page.mouse.up();
793
+
794
+ // Element should be clamped within the container
795
+ const elementBox = await element.boundingBox();
796
+
797
+ expect(elementBox!.x).toBeGreaterThanOrEqual(containerBox!.x);
798
+ expect(elementBox!.y).toBeGreaterThanOrEqual(containerBox!.y);
799
+ expect(elementBox!.x + elementBox!.width).toBeLessThanOrEqual(
800
+ containerBox!.x + containerBox!.width
801
+ );
802
+ expect(elementBox!.y + elementBox!.height).toBeLessThanOrEqual(
803
+ containerBox!.y + containerBox!.height
804
+ );
805
+ });
806
+
807
+ test('resizes an element by dragging its handle', async ({ page }) => {
808
+ await page.goto('/canvas-editor');
809
+
810
+ const element = page.locator('[data-testid="shape-1"]');
811
+ await element.click(); // Select to show resize handles
812
+
813
+ const resizeHandle = element.locator('.resize-handle-se'); // south-east corner
814
+ const handleBox = await resizeHandle.boundingBox();
815
+
816
+ const initialBox = await element.boundingBox();
817
+
818
+ // Drag the resize handle to make the element larger
819
+ await resizeHandle.hover();
820
+ await page.mouse.down();
821
+ await page.mouse.move(handleBox!.x + 100, handleBox!.y + 80, { steps: 5 });
822
+ await page.mouse.up();
823
+
824
+ const newBox = await element.boundingBox();
825
+ expect(newBox!.width).toBeCloseTo(initialBox!.width + 100, -1);
826
+ expect(newBox!.height).toBeCloseTo(initialBox!.height + 80, -1);
827
+ });
828
+ ```
829
+
830
+ **JavaScript**
831
+
832
+ ```javascript
833
+ const { test, expect } = require('@playwright/test');
834
+
835
+ test('drags an element to a specific coordinate on a canvas', async ({ page }) => {
836
+ await page.goto('/canvas-editor');
837
+
838
+ const canvas = page.locator('#design-canvas');
839
+ const element = page.locator('[data-testid="shape-1"]');
840
+
841
+ const canvasBox = await canvas.boundingBox();
842
+ const targetX = canvasBox.x + 300;
843
+ const targetY = canvasBox.y + 200;
844
+
845
+ await element.hover();
846
+ await page.mouse.down();
847
+ await page.mouse.move(targetX, targetY, { steps: 10 });
848
+ await page.mouse.up();
849
+
850
+ const newBox = await element.boundingBox();
851
+ expect(newBox.x).toBeCloseTo(targetX - newBox.width / 2, -1);
852
+ expect(newBox.y).toBeCloseTo(targetY - newBox.height / 2, -1);
853
+ });
854
+
855
+ test('constrains drag within boundaries', async ({ page }) => {
856
+ await page.goto('/canvas-editor');
857
+
858
+ const element = page.locator('[data-testid="constrained-element"]');
859
+ const container = page.locator('#constraint-box');
860
+
861
+ const containerBox = await container.boundingBox();
862
+
863
+ await element.hover();
864
+ await page.mouse.down();
865
+ await page.mouse.move(containerBox.x + containerBox.width + 500, containerBox.y - 200, {
866
+ steps: 10,
867
+ });
868
+ await page.mouse.up();
869
+
870
+ const elementBox = await element.boundingBox();
871
+ expect(elementBox.x).toBeGreaterThanOrEqual(containerBox.x);
872
+ expect(elementBox.y).toBeGreaterThanOrEqual(containerBox.y);
873
+ });
874
+ ```
875
+
876
+ ---
877
+
878
+ ## Variations
879
+
880
+ ### Drag and Drop with Keyboard Accessibility
881
+
882
+ ```typescript
883
+ test('reorders items using keyboard', async ({ page }) => {
884
+ await page.goto('/tasks');
885
+
886
+ const list = page.getByRole('list', { name: 'Task list' });
887
+ const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' });
888
+
889
+ // Focus the item
890
+ await taskC.focus();
891
+
892
+ // Use keyboard shortcut to pick up the item
893
+ await page.keyboard.press('Space');
894
+
895
+ // Move up twice
896
+ await page.keyboard.press('ArrowUp');
897
+ await page.keyboard.press('ArrowUp');
898
+
899
+ // Drop the item
900
+ await page.keyboard.press('Space');
901
+
902
+ const items = await list.getByRole('listitem').allTextContents();
903
+ expect(items[0]).toContain('Task C');
904
+ });
905
+ ```
906
+
907
+ ### Drag and Drop Between iframes
908
+
909
+ ```typescript
910
+ test('drags between main page and iframe', async ({ page }) => {
911
+ await page.goto('/editor');
912
+
913
+ const sourceItem = page.getByText('Widget A');
914
+ const iframe = page.frameLocator('#preview-frame');
915
+ const dropTarget = iframe.locator('#content-area');
916
+
917
+ // Cross-frame drag requires coordinates since dragTo does not work across frames
918
+ const sourceBox = await sourceItem.boundingBox();
919
+ const iframeElement = page.locator('#preview-frame');
920
+ const iframeBox = await iframeElement.boundingBox();
921
+
922
+ // Calculate target position within the iframe
923
+ const targetX = iframeBox!.x + 100;
924
+ const targetY = iframeBox!.y + 100;
925
+
926
+ await sourceItem.hover();
927
+ await page.mouse.down();
928
+ await page.mouse.move(targetX, targetY, { steps: 20 });
929
+ await page.mouse.up();
930
+
931
+ await expect(iframe.getByText('Widget A')).toBeVisible();
932
+ });
933
+ ```
934
+
935
+ ### Touch-Based Drag on Mobile
936
+
937
+ ```typescript
938
+ test('drags items on mobile via touch events', async ({ page }) => {
939
+ await page.goto('/tasks');
940
+
941
+ const list = page.getByRole('list', { name: 'Task list' });
942
+ const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' });
943
+ const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' });
944
+
945
+ const sourceBox = await sourceItem.boundingBox();
946
+ const targetBox = await targetItem.boundingBox();
947
+
948
+ // Simulate long-press then drag using touch
949
+ await page.touchscreen.tap(sourceBox!.x + sourceBox!.width / 2, sourceBox!.y + sourceBox!.height / 2);
950
+
951
+ // Dispatch touchstart, touchmove, touchend for libraries that use touch events
952
+ await sourceItem.dispatchEvent('touchstart', {
953
+ touches: [{ clientX: sourceBox!.x + 10, clientY: sourceBox!.y + 10 }],
954
+ });
955
+
956
+ // Move in steps
957
+ for (let i = 1; i <= 5; i++) {
958
+ const y = sourceBox!.y + (targetBox!.y - sourceBox!.y) * (i / 5);
959
+ await sourceItem.dispatchEvent('touchmove', {
960
+ touches: [{ clientX: sourceBox!.x + 10, clientY: y }],
961
+ });
962
+ }
963
+
964
+ await sourceItem.dispatchEvent('touchend');
965
+
966
+ const items = await list.getByRole('listitem').allTextContents();
967
+ expect(items[0]).toContain('Task C');
968
+ });
969
+ ```
970
+
971
+ ---
972
+
973
+ ## Tips
974
+
975
+ 1. **Use `dragTo()` first, then fall back to manual mouse events**. Playwright's built-in `dragTo()` handles most native HTML5 drag and drop. Only use `page.mouse.down()` / `move()` / `up()` sequences for custom drag libraries (react-beautiful-dnd, dnd-kit, SortableJS) that need specific event sequences.
976
+
977
+ 2. **Add intermediate mouse move steps for drag libraries**. Libraries like `react-beautiful-dnd` require multiple `mousemove` events with small increments to detect a drag. Use `{ steps: 10 }` or a manual loop. A single jump from source to target often fails silently.
978
+
979
+ 3. **Always assert the final state, not just the drop event**. After a drag-and-drop, verify that the DOM actually reflects the change -- item order, column contents, position coordinates. Visual feedback during drag is nice to test but the final persisted state matters most.
980
+
981
+ 4. **Use `boundingBox()` for coordinate-based assertions**. When testing canvas editors, grid layouts, or position-sensitive drops, capture the bounding box after the operation and compare against expected coordinates. Use `toBeCloseTo()` for tolerance.
982
+
983
+ 5. **Test undo after drag operations**. If your app supports Ctrl+Z to undo a reorder or move, test that the drag operation is reversible. This catches state management bugs that only appear on undo.
984
+
985
+ ---
986
+
987
+ ## Related
988
+
989
+ - [Playwright Drag and Drop Docs](https://playwright.dev/docs/input#drag-and-drop)
990
+ - `recipes/file-upload-download.md` -- File drop zones specifically
991
+ - `foundations/actions.md` -- Mouse and keyboard interaction basics
992
+ - `patterns/page-objects.md` -- Encapsulate complex drag flows