@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,1162 @@
1
+ # Testing Vue Apps with Playwright
2
+
3
+ > **When to use**: Testing Vue 3 applications, including composition API components, Pinia stores, Vue Router navigation, Nuxt.js apps, Teleport portals, and transitions. Covers E2E testing and experimental component testing with `@playwright/experimental-ct-vue`.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md), [core/locators.md](locators.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```bash
9
+ # Install Playwright in a Vue project
10
+ npm init playwright@latest
11
+
12
+ # Install component testing (experimental)
13
+ npm install -D @playwright/experimental-ct-vue
14
+
15
+ # Run E2E tests
16
+ npx playwright test
17
+
18
+ # Run component tests
19
+ npx playwright test -c playwright-ct.config.ts
20
+
21
+ # Debug a test with headed browser and inspector
22
+ npx playwright test tests/home.spec.ts --headed --debug
23
+ ```
24
+
25
+ ## Setup
26
+
27
+ ### Playwright Config for Vue (Vite)
28
+
29
+ **TypeScript**
30
+ ```typescript
31
+ // playwright.config.ts
32
+ import { defineConfig, devices } from '@playwright/test';
33
+
34
+ export default defineConfig({
35
+ testDir: './tests/e2e',
36
+ testMatch: '**/*.spec.ts',
37
+ fullyParallel: true,
38
+ forbidOnly: !!process.env.CI,
39
+ retries: process.env.CI ? 2 : 0,
40
+ workers: process.env.CI ? '50%' : undefined,
41
+
42
+ use: {
43
+ baseURL: 'http://localhost:5173',
44
+ trace: 'on-first-retry',
45
+ screenshot: 'only-on-failure',
46
+ },
47
+
48
+ projects: [
49
+ {
50
+ name: 'chromium',
51
+ use: { ...devices['Desktop Chrome'] },
52
+ },
53
+ {
54
+ name: 'firefox',
55
+ use: { ...devices['Desktop Firefox'] },
56
+ },
57
+ {
58
+ name: 'mobile',
59
+ use: { ...devices['iPhone 14'] },
60
+ },
61
+ ],
62
+
63
+ webServer: {
64
+ command: process.env.CI
65
+ ? 'npm run build && npx vite preview --port 5173'
66
+ : 'npm run dev',
67
+ url: 'http://localhost:5173',
68
+ reuseExistingServer: !process.env.CI,
69
+ timeout: 120_000,
70
+ },
71
+ });
72
+ ```
73
+
74
+ **JavaScript**
75
+ ```javascript
76
+ // playwright.config.js
77
+ const { defineConfig, devices } = require('@playwright/test');
78
+
79
+ module.exports = defineConfig({
80
+ testDir: './tests/e2e',
81
+ testMatch: '**/*.spec.js',
82
+ fullyParallel: true,
83
+ forbidOnly: !!process.env.CI,
84
+ retries: process.env.CI ? 2 : 0,
85
+ workers: process.env.CI ? '50%' : undefined,
86
+
87
+ use: {
88
+ baseURL: 'http://localhost:5173',
89
+ trace: 'on-first-retry',
90
+ screenshot: 'only-on-failure',
91
+ },
92
+
93
+ projects: [
94
+ {
95
+ name: 'chromium',
96
+ use: { ...devices['Desktop Chrome'] },
97
+ },
98
+ {
99
+ name: 'firefox',
100
+ use: { ...devices['Desktop Firefox'] },
101
+ },
102
+ {
103
+ name: 'mobile',
104
+ use: { ...devices['iPhone 14'] },
105
+ },
106
+ ],
107
+
108
+ webServer: {
109
+ command: process.env.CI
110
+ ? 'npm run build && npx vite preview --port 5173'
111
+ : 'npm run dev',
112
+ url: 'http://localhost:5173',
113
+ reuseExistingServer: !process.env.CI,
114
+ timeout: 120_000,
115
+ },
116
+ });
117
+ ```
118
+
119
+ ### Component Testing Config
120
+
121
+ **TypeScript**
122
+ ```typescript
123
+ // playwright-ct.config.ts
124
+ import { defineConfig, devices } from '@playwright/experimental-ct-vue';
125
+
126
+ export default defineConfig({
127
+ testDir: './tests/components',
128
+ testMatch: '**/*.ct.ts',
129
+
130
+ use: {
131
+ trace: 'on-first-retry',
132
+ ctPort: 3100,
133
+ },
134
+
135
+ projects: [
136
+ {
137
+ name: 'chromium',
138
+ use: { ...devices['Desktop Chrome'] },
139
+ },
140
+ ],
141
+ });
142
+ ```
143
+
144
+ **JavaScript**
145
+ ```javascript
146
+ // playwright-ct.config.js
147
+ const { defineConfig, devices } = require('@playwright/experimental-ct-vue');
148
+
149
+ module.exports = defineConfig({
150
+ testDir: './tests/components',
151
+ testMatch: '**/*.ct.js',
152
+
153
+ use: {
154
+ trace: 'on-first-retry',
155
+ ctPort: 3100,
156
+ },
157
+
158
+ projects: [
159
+ {
160
+ name: 'chromium',
161
+ use: { ...devices['Desktop Chrome'] },
162
+ },
163
+ ],
164
+ });
165
+ ```
166
+
167
+ ### Nuxt.js Setup
168
+
169
+ Nuxt requires a build step before testing and uses port 3000 by default.
170
+
171
+ **TypeScript**
172
+ ```typescript
173
+ // playwright.config.ts
174
+ import { defineConfig, devices } from '@playwright/test';
175
+
176
+ export default defineConfig({
177
+ testDir: './tests/e2e',
178
+ testMatch: '**/*.spec.ts',
179
+ fullyParallel: true,
180
+ forbidOnly: !!process.env.CI,
181
+ retries: process.env.CI ? 2 : 0,
182
+
183
+ use: {
184
+ baseURL: 'http://localhost:3000',
185
+ trace: 'on-first-retry',
186
+ screenshot: 'only-on-failure',
187
+ },
188
+
189
+ projects: [
190
+ {
191
+ name: 'chromium',
192
+ use: { ...devices['Desktop Chrome'] },
193
+ },
194
+ ],
195
+
196
+ webServer: {
197
+ command: process.env.CI
198
+ ? 'npx nuxi build && npx nuxi preview'
199
+ : 'npx nuxi dev',
200
+ url: 'http://localhost:3000',
201
+ reuseExistingServer: !process.env.CI,
202
+ timeout: 120_000,
203
+ env: {
204
+ NUXT_PUBLIC_API_BASE: 'http://localhost:3000/api',
205
+ },
206
+ },
207
+ });
208
+ ```
209
+
210
+ **JavaScript**
211
+ ```javascript
212
+ // playwright.config.js
213
+ const { defineConfig, devices } = require('@playwright/test');
214
+
215
+ module.exports = defineConfig({
216
+ testDir: './tests/e2e',
217
+ testMatch: '**/*.spec.js',
218
+ fullyParallel: true,
219
+ forbidOnly: !!process.env.CI,
220
+ retries: process.env.CI ? 2 : 0,
221
+
222
+ use: {
223
+ baseURL: 'http://localhost:3000',
224
+ trace: 'on-first-retry',
225
+ screenshot: 'only-on-failure',
226
+ },
227
+
228
+ projects: [
229
+ {
230
+ name: 'chromium',
231
+ use: { ...devices['Desktop Chrome'] },
232
+ },
233
+ ],
234
+
235
+ webServer: {
236
+ command: process.env.CI
237
+ ? 'npx nuxi build && npx nuxi preview'
238
+ : 'npx nuxi dev',
239
+ url: 'http://localhost:3000',
240
+ reuseExistingServer: !process.env.CI,
241
+ timeout: 120_000,
242
+ env: {
243
+ NUXT_PUBLIC_API_BASE: 'http://localhost:3000/api',
244
+ },
245
+ },
246
+ });
247
+ ```
248
+
249
+ ## Patterns
250
+
251
+ ### Component Testing with `@playwright/experimental-ct-vue`
252
+
253
+ **Use when**: Testing complex interactive Vue components in isolation -- data tables, form components, custom select dropdowns, rich editors. The component needs a real browser but not a full application.
254
+ **Avoid when**: The component depends heavily on Pinia stores, Vue Router, or backend data. Use E2E tests instead, or provide the dependencies in your component test setup.
255
+
256
+ **TypeScript**
257
+ ```typescript
258
+ // tests/components/Counter.ct.ts
259
+ import { test, expect } from '@playwright/experimental-ct-vue';
260
+ import Counter from '../../src/components/Counter.vue';
261
+
262
+ test('renders with initial count and increments', async ({ mount }) => {
263
+ const component = await mount(Counter, {
264
+ props: {
265
+ initialCount: 0,
266
+ },
267
+ });
268
+
269
+ await expect(component.getByText('Count: 0')).toBeVisible();
270
+ await component.getByRole('button', { name: 'Increment' }).click();
271
+ await expect(component.getByText('Count: 1')).toBeVisible();
272
+ });
273
+
274
+ test('emits update event when count changes', async ({ mount }) => {
275
+ const emittedValues: number[] = [];
276
+ const component = await mount(Counter, {
277
+ props: {
278
+ initialCount: 5,
279
+ },
280
+ on: {
281
+ update: (value: number) => emittedValues.push(value),
282
+ },
283
+ });
284
+
285
+ await component.getByRole('button', { name: 'Increment' }).click();
286
+ await component.getByRole('button', { name: 'Increment' }).click();
287
+
288
+ expect(emittedValues).toEqual([6, 7]);
289
+ });
290
+
291
+ test('respects max prop', async ({ mount }) => {
292
+ const component = await mount(Counter, {
293
+ props: {
294
+ initialCount: 9,
295
+ max: 10,
296
+ },
297
+ });
298
+
299
+ await component.getByRole('button', { name: 'Increment' }).click();
300
+ await expect(component.getByText('Count: 10')).toBeVisible();
301
+
302
+ // At max -- button should be disabled
303
+ await expect(component.getByRole('button', { name: 'Increment' })).toBeDisabled();
304
+ });
305
+
306
+ test('renders with slot content', async ({ mount }) => {
307
+ const component = await mount(Counter, {
308
+ props: { initialCount: 0 },
309
+ slots: {
310
+ default: '<span class="custom-label">Items in cart</span>',
311
+ },
312
+ });
313
+
314
+ await expect(component.getByText('Items in cart')).toBeVisible();
315
+ });
316
+ ```
317
+
318
+ **JavaScript**
319
+ ```javascript
320
+ // tests/components/Counter.ct.js
321
+ const { test, expect } = require('@playwright/experimental-ct-vue');
322
+ const Counter = require('../../src/components/Counter.vue');
323
+
324
+ test('renders with initial count and increments', async ({ mount }) => {
325
+ const component = await mount(Counter, {
326
+ props: {
327
+ initialCount: 0,
328
+ },
329
+ });
330
+
331
+ await expect(component.getByText('Count: 0')).toBeVisible();
332
+ await component.getByRole('button', { name: 'Increment' }).click();
333
+ await expect(component.getByText('Count: 1')).toBeVisible();
334
+ });
335
+
336
+ test('emits update event when count changes', async ({ mount }) => {
337
+ const emittedValues = [];
338
+ const component = await mount(Counter, {
339
+ props: {
340
+ initialCount: 5,
341
+ },
342
+ on: {
343
+ update: (value) => emittedValues.push(value),
344
+ },
345
+ });
346
+
347
+ await component.getByRole('button', { name: 'Increment' }).click();
348
+ await component.getByRole('button', { name: 'Increment' }).click();
349
+
350
+ expect(emittedValues).toEqual([6, 7]);
351
+ });
352
+
353
+ test('renders with slot content', async ({ mount }) => {
354
+ const component = await mount(Counter, {
355
+ props: { initialCount: 0 },
356
+ slots: {
357
+ default: '<span class="custom-label">Items in cart</span>',
358
+ },
359
+ });
360
+
361
+ await expect(component.getByText('Items in cart')).toBeVisible();
362
+ });
363
+ ```
364
+
365
+ ### Testing Pinia Stores Through the UI
366
+
367
+ **Use when**: Verifying that Pinia stores produce the correct UI behavior. Playwright tests the rendered output, not the store directly. If the UI is correct, the store is correct.
368
+ **Avoid when**: Testing pure store logic (getters, actions with no UI side effect) -- use unit tests with Vitest for that.
369
+
370
+ **TypeScript**
371
+ ```typescript
372
+ import { test, expect } from '@playwright/test';
373
+
374
+ test.describe('Pinia cart store (tested through UI)', () => {
375
+ test('adding items updates the cart across the app', async ({ page }) => {
376
+ await page.goto('/products');
377
+
378
+ const cartBadge = page.getByTestId('cart-count');
379
+
380
+ // Initially empty
381
+ await expect(cartBadge).toHaveText('0');
382
+
383
+ // Add first item -- Pinia store action is triggered
384
+ await page.getByRole('listitem')
385
+ .filter({ hasText: 'Vue T-Shirt' })
386
+ .getByRole('button', { name: 'Add to cart' })
387
+ .click();
388
+ await expect(cartBadge).toHaveText('1');
389
+
390
+ // Add second item
391
+ await page.getByRole('listitem')
392
+ .filter({ hasText: 'Pinia Sticker Pack' })
393
+ .getByRole('button', { name: 'Add to cart' })
394
+ .click();
395
+ await expect(cartBadge).toHaveText('2');
396
+
397
+ // Navigate to cart page -- store state persists
398
+ await page.getByRole('link', { name: 'Cart' }).click();
399
+ await page.waitForURL('/cart');
400
+
401
+ await expect(page.getByText('Vue T-Shirt')).toBeVisible();
402
+ await expect(page.getByText('Pinia Sticker Pack')).toBeVisible();
403
+ await expect(page.getByText('Total: $29.98')).toBeVisible();
404
+ });
405
+
406
+ test('removing items from cart updates total', async ({ page }) => {
407
+ // Navigate to a pre-populated cart (seed via API or navigate through adding items)
408
+ await page.goto('/products');
409
+ await page.getByRole('listitem')
410
+ .filter({ hasText: 'Vue T-Shirt' })
411
+ .getByRole('button', { name: 'Add to cart' })
412
+ .click();
413
+
414
+ await page.getByRole('link', { name: 'Cart' }).click();
415
+ await page.waitForURL('/cart');
416
+
417
+ await page.getByRole('button', { name: 'Remove Vue T-Shirt' }).click();
418
+
419
+ await expect(page.getByText('Your cart is empty')).toBeVisible();
420
+ await expect(page.getByTestId('cart-count')).toHaveText('0');
421
+ });
422
+
423
+ test('store persists after page reload when using pinia-plugin-persistedstate', async ({ page }) => {
424
+ await page.goto('/products');
425
+
426
+ await page.getByRole('listitem')
427
+ .filter({ hasText: 'Vue T-Shirt' })
428
+ .getByRole('button', { name: 'Add to cart' })
429
+ .click();
430
+
431
+ // Reload the page -- persisted store should restore state
432
+ await page.reload();
433
+
434
+ await expect(page.getByTestId('cart-count')).toHaveText('1');
435
+ });
436
+ });
437
+
438
+ test.describe('Pinia auth store (tested through UI)', () => {
439
+ test('login updates auth state across all components', async ({ page }) => {
440
+ await page.goto('/');
441
+
442
+ await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible();
443
+
444
+ await page.getByRole('link', { name: 'Sign in' }).click();
445
+ await page.getByLabel('Email').fill('user@example.com');
446
+ await page.getByLabel('Password').fill('password123');
447
+ await page.getByRole('button', { name: 'Sign in' }).click();
448
+
449
+ await page.waitForURL('/dashboard');
450
+
451
+ // Header updates (uses auth store)
452
+ await expect(page.getByText('user@example.com')).toBeVisible();
453
+ // Navigation updates (uses auth store for conditional routes)
454
+ await expect(page.getByRole('link', { name: 'Admin' })).toBeVisible();
455
+ });
456
+ });
457
+ ```
458
+
459
+ **JavaScript**
460
+ ```javascript
461
+ const { test, expect } = require('@playwright/test');
462
+
463
+ test.describe('Pinia cart store (tested through UI)', () => {
464
+ test('adding items updates the cart across the app', async ({ page }) => {
465
+ await page.goto('/products');
466
+
467
+ const cartBadge = page.getByTestId('cart-count');
468
+ await expect(cartBadge).toHaveText('0');
469
+
470
+ await page.getByRole('listitem')
471
+ .filter({ hasText: 'Vue T-Shirt' })
472
+ .getByRole('button', { name: 'Add to cart' })
473
+ .click();
474
+ await expect(cartBadge).toHaveText('1');
475
+
476
+ await page.getByRole('listitem')
477
+ .filter({ hasText: 'Pinia Sticker Pack' })
478
+ .getByRole('button', { name: 'Add to cart' })
479
+ .click();
480
+ await expect(cartBadge).toHaveText('2');
481
+
482
+ await page.getByRole('link', { name: 'Cart' }).click();
483
+ await page.waitForURL('/cart');
484
+
485
+ await expect(page.getByText('Vue T-Shirt')).toBeVisible();
486
+ await expect(page.getByText('Pinia Sticker Pack')).toBeVisible();
487
+ });
488
+
489
+ test('store persists after page reload when using pinia-plugin-persistedstate', async ({ page }) => {
490
+ await page.goto('/products');
491
+
492
+ await page.getByRole('listitem')
493
+ .filter({ hasText: 'Vue T-Shirt' })
494
+ .getByRole('button', { name: 'Add to cart' })
495
+ .click();
496
+
497
+ await page.reload();
498
+ await expect(page.getByTestId('cart-count')).toHaveText('1');
499
+ });
500
+ });
501
+ ```
502
+
503
+ ### Testing Vue Router Navigation
504
+
505
+ **Use when**: Testing client-side routing with Vue Router. Verify route transitions, navigation guards, URL parameters, and browser history behavior.
506
+ **Avoid when**: Testing Nuxt.js file-based routing -- the patterns are similar but Nuxt has additional server-side concerns.
507
+
508
+ **TypeScript**
509
+ ```typescript
510
+ import { test, expect } from '@playwright/test';
511
+
512
+ test.describe('Vue Router navigation', () => {
513
+ test('client-side navigation does not cause full page reload', async ({ page }) => {
514
+ await page.goto('/');
515
+
516
+ await page.evaluate(() => {
517
+ (window as any).__testMarker = 'no-reload';
518
+ });
519
+
520
+ await page.getByRole('link', { name: 'Products' }).click();
521
+ await page.waitForURL('/products');
522
+
523
+ const marker = await page.evaluate(() => (window as any).__testMarker);
524
+ expect(marker).toBe('no-reload');
525
+ });
526
+
527
+ test('dynamic route params render correct content', async ({ page }) => {
528
+ await page.goto('/products/42');
529
+
530
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
531
+ await expect(page.getByText('Product #42')).toBeVisible();
532
+ });
533
+
534
+ test('navigation guard redirects unauthenticated users', async ({ page }) => {
535
+ // Visit a route guarded by router.beforeEach
536
+ await page.goto('/admin/users');
537
+
538
+ // Guard should redirect to login
539
+ await expect(page).toHaveURL(/\/login/);
540
+ await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
541
+ });
542
+
543
+ test('named routes and nested views render correctly', async ({ page }) => {
544
+ await page.goto('/settings');
545
+
546
+ // Parent layout
547
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
548
+ await expect(page.getByRole('navigation', { name: 'Settings' })).toBeVisible();
549
+
550
+ // Default child route
551
+ await expect(page.getByRole('heading', { name: 'General', level: 2 })).toBeVisible();
552
+
553
+ // Navigate to sibling child route
554
+ await page.getByRole('link', { name: 'Notifications' }).click();
555
+ await page.waitForURL('/settings/notifications');
556
+
557
+ // Parent layout persists, child content changes
558
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
559
+ await expect(page.getByRole('heading', { name: 'Notifications', level: 2 })).toBeVisible();
560
+ });
561
+
562
+ test('browser back/forward works with Vue Router', async ({ page }) => {
563
+ await page.goto('/');
564
+ await page.getByRole('link', { name: 'Products' }).click();
565
+ await page.waitForURL('/products');
566
+ await page.getByRole('link', { name: 'About' }).click();
567
+ await page.waitForURL('/about');
568
+
569
+ await page.goBack();
570
+ await expect(page).toHaveURL(/\/products/);
571
+
572
+ await page.goBack();
573
+ await expect(page).toHaveURL(/\/$/);
574
+
575
+ await page.goForward();
576
+ await expect(page).toHaveURL(/\/products/);
577
+ });
578
+
579
+ test('query parameters update reactive computed properties', async ({ page }) => {
580
+ await page.goto('/products?sort=price-asc&category=electronics');
581
+
582
+ await expect(page.getByRole('heading', { name: 'Electronics' })).toBeVisible();
583
+
584
+ // Change sort via UI (updates route query params)
585
+ await page.getByRole('combobox', { name: 'Sort by' }).selectOption('name-asc');
586
+ await expect(page).toHaveURL(/sort=name-asc/);
587
+ });
588
+
589
+ test('404 catch-all route displays not found page', async ({ page }) => {
590
+ await page.goto('/this-page-does-not-exist');
591
+
592
+ await expect(page.getByRole('heading', { name: 'Page Not Found' })).toBeVisible();
593
+ await expect(page.getByRole('link', { name: 'Go home' })).toBeVisible();
594
+ });
595
+ });
596
+ ```
597
+
598
+ **JavaScript**
599
+ ```javascript
600
+ const { test, expect } = require('@playwright/test');
601
+
602
+ test.describe('Vue Router navigation', () => {
603
+ test('client-side navigation does not cause full page reload', async ({ page }) => {
604
+ await page.goto('/');
605
+
606
+ await page.evaluate(() => {
607
+ window.__testMarker = 'no-reload';
608
+ });
609
+
610
+ await page.getByRole('link', { name: 'Products' }).click();
611
+ await page.waitForURL('/products');
612
+
613
+ const marker = await page.evaluate(() => window.__testMarker);
614
+ expect(marker).toBe('no-reload');
615
+ });
616
+
617
+ test('dynamic route params render correct content', async ({ page }) => {
618
+ await page.goto('/products/42');
619
+
620
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
621
+ await expect(page.getByText('Product #42')).toBeVisible();
622
+ });
623
+
624
+ test('navigation guard redirects unauthenticated users', async ({ page }) => {
625
+ await page.goto('/admin/users');
626
+
627
+ await expect(page).toHaveURL(/\/login/);
628
+ await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
629
+ });
630
+
631
+ test('browser back/forward works with Vue Router', async ({ page }) => {
632
+ await page.goto('/');
633
+ await page.getByRole('link', { name: 'Products' }).click();
634
+ await page.waitForURL('/products');
635
+ await page.getByRole('link', { name: 'About' }).click();
636
+ await page.waitForURL('/about');
637
+
638
+ await page.goBack();
639
+ await expect(page).toHaveURL(/\/products/);
640
+
641
+ await page.goBack();
642
+ await expect(page).toHaveURL(/\/$/);
643
+ });
644
+ });
645
+ ```
646
+
647
+ ### Testing Teleport (Portals)
648
+
649
+ **Use when**: Testing components rendered via Vue's `<Teleport>` -- modals, notifications, overlay menus. Teleport moves DOM elements to a different location (often `body`), but Playwright sees the entire document.
650
+ **Avoid when**: The component is not teleported -- it renders inline in its parent.
651
+
652
+ **TypeScript**
653
+ ```typescript
654
+ import { test, expect } from '@playwright/test';
655
+
656
+ test.describe('Teleport components', () => {
657
+ test('modal teleported to body is visible and interactive', async ({ page }) => {
658
+ await page.goto('/products');
659
+
660
+ await page.getByRole('button', { name: 'Delete' }).first().click();
661
+
662
+ // Modal is teleported to <body> with <Teleport to="body">
663
+ // Playwright sees it like any other element
664
+ const modal = page.getByRole('dialog', { name: 'Confirm deletion' });
665
+ await expect(modal).toBeVisible();
666
+
667
+ await modal.getByRole('button', { name: 'Cancel' }).click();
668
+ await expect(modal).toBeHidden();
669
+ });
670
+
671
+ test('teleported notification appears above all content', async ({ page }) => {
672
+ await page.goto('/settings');
673
+
674
+ await page.getByRole('button', { name: 'Save' }).click();
675
+
676
+ // Notification teleported to #notifications container
677
+ const notification = page.getByRole('alert');
678
+ await expect(notification).toBeVisible();
679
+ await expect(notification).toContainText('Settings saved');
680
+
681
+ // Auto-dismiss
682
+ await expect(notification).toBeHidden({ timeout: 10_000 });
683
+ });
684
+
685
+ test('teleported dropdown closes on outside click', async ({ page }) => {
686
+ await page.goto('/dashboard');
687
+
688
+ await page.getByRole('button', { name: 'User menu' }).click();
689
+
690
+ const menu = page.getByRole('menu');
691
+ await expect(menu).toBeVisible();
692
+
693
+ // Click outside the menu
694
+ await page.locator('body').click({ position: { x: 10, y: 10 } });
695
+ await expect(menu).toBeHidden();
696
+ });
697
+ });
698
+ ```
699
+
700
+ **JavaScript**
701
+ ```javascript
702
+ const { test, expect } = require('@playwright/test');
703
+
704
+ test.describe('Teleport components', () => {
705
+ test('modal teleported to body is visible and interactive', async ({ page }) => {
706
+ await page.goto('/products');
707
+
708
+ await page.getByRole('button', { name: 'Delete' }).first().click();
709
+
710
+ const modal = page.getByRole('dialog', { name: 'Confirm deletion' });
711
+ await expect(modal).toBeVisible();
712
+
713
+ await modal.getByRole('button', { name: 'Cancel' }).click();
714
+ await expect(modal).toBeHidden();
715
+ });
716
+
717
+ test('teleported notification appears above all content', async ({ page }) => {
718
+ await page.goto('/settings');
719
+
720
+ await page.getByRole('button', { name: 'Save' }).click();
721
+
722
+ const notification = page.getByRole('alert');
723
+ await expect(notification).toBeVisible();
724
+ await expect(notification).toContainText('Settings saved');
725
+
726
+ await expect(notification).toBeHidden({ timeout: 10_000 });
727
+ });
728
+
729
+ test('teleported dropdown closes on outside click', async ({ page }) => {
730
+ await page.goto('/dashboard');
731
+
732
+ await page.getByRole('button', { name: 'User menu' }).click();
733
+
734
+ const menu = page.getByRole('menu');
735
+ await expect(menu).toBeVisible();
736
+
737
+ await page.locator('body').click({ position: { x: 10, y: 10 } });
738
+ await expect(menu).toBeHidden();
739
+ });
740
+ });
741
+ ```
742
+
743
+ ### Testing Transitions and Animations
744
+
745
+ **Use when**: Verifying that Vue `<Transition>` and `<TransitionGroup>` components work correctly -- elements appear, disappear, and reorder with expected behavior. Focus on the end state, not the animation itself.
746
+ **Avoid when**: Testing the exact CSS animation keyframes -- visual regression testing is better for pixel-level validation.
747
+
748
+ **TypeScript**
749
+ ```typescript
750
+ import { test, expect } from '@playwright/test';
751
+
752
+ test.describe('transitions', () => {
753
+ test('list items animate in when added', async ({ page }) => {
754
+ await page.goto('/todo');
755
+
756
+ await page.getByRole('textbox', { name: 'New task' }).fill('Buy groceries');
757
+ await page.getByRole('button', { name: 'Add' }).click();
758
+
759
+ // The item should be visible after the enter transition completes
760
+ // Playwright auto-waits for visibility, handling the transition automatically
761
+ await expect(page.getByText('Buy groceries')).toBeVisible();
762
+ });
763
+
764
+ test('deleted items animate out and are removed from DOM', async ({ page }) => {
765
+ await page.goto('/todo');
766
+
767
+ // Add an item first
768
+ await page.getByRole('textbox', { name: 'New task' }).fill('Temporary task');
769
+ await page.getByRole('button', { name: 'Add' }).click();
770
+ await expect(page.getByText('Temporary task')).toBeVisible();
771
+
772
+ // Delete it
773
+ await page.getByRole('listitem')
774
+ .filter({ hasText: 'Temporary task' })
775
+ .getByRole('button', { name: 'Delete' })
776
+ .click();
777
+
778
+ // After the leave transition, the element should be gone
779
+ await expect(page.getByText('Temporary task')).toBeHidden();
780
+ });
781
+
782
+ test('page transition completes between routes', async ({ page }) => {
783
+ await page.goto('/');
784
+
785
+ await page.getByRole('link', { name: 'About' }).click();
786
+ await page.waitForURL('/about');
787
+
788
+ // After the route transition animation, content should be visible
789
+ await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
790
+ });
791
+
792
+ test('disable animations for faster, more reliable tests', async ({ page }) => {
793
+ // Disable all CSS animations and transitions for deterministic testing
794
+ await page.addStyleTag({
795
+ content: `
796
+ *, *::before, *::after {
797
+ animation-duration: 0s !important;
798
+ animation-delay: 0s !important;
799
+ transition-duration: 0s !important;
800
+ transition-delay: 0s !important;
801
+ }
802
+ `,
803
+ });
804
+
805
+ await page.goto('/todo');
806
+
807
+ await page.getByRole('textbox', { name: 'New task' }).fill('Instant task');
808
+ await page.getByRole('button', { name: 'Add' }).click();
809
+
810
+ // With animations disabled, elements appear/disappear instantly
811
+ await expect(page.getByText('Instant task')).toBeVisible();
812
+ });
813
+ });
814
+ ```
815
+
816
+ **JavaScript**
817
+ ```javascript
818
+ const { test, expect } = require('@playwright/test');
819
+
820
+ test.describe('transitions', () => {
821
+ test('list items animate in when added', async ({ page }) => {
822
+ await page.goto('/todo');
823
+
824
+ await page.getByRole('textbox', { name: 'New task' }).fill('Buy groceries');
825
+ await page.getByRole('button', { name: 'Add' }).click();
826
+
827
+ await expect(page.getByText('Buy groceries')).toBeVisible();
828
+ });
829
+
830
+ test('deleted items animate out and are removed from DOM', async ({ page }) => {
831
+ await page.goto('/todo');
832
+
833
+ await page.getByRole('textbox', { name: 'New task' }).fill('Temporary task');
834
+ await page.getByRole('button', { name: 'Add' }).click();
835
+ await expect(page.getByText('Temporary task')).toBeVisible();
836
+
837
+ await page.getByRole('listitem')
838
+ .filter({ hasText: 'Temporary task' })
839
+ .getByRole('button', { name: 'Delete' })
840
+ .click();
841
+
842
+ await expect(page.getByText('Temporary task')).toBeHidden();
843
+ });
844
+
845
+ test('disable animations for faster, more reliable tests', async ({ page }) => {
846
+ await page.addStyleTag({
847
+ content: `
848
+ *, *::before, *::after {
849
+ animation-duration: 0s !important;
850
+ animation-delay: 0s !important;
851
+ transition-duration: 0s !important;
852
+ transition-delay: 0s !important;
853
+ }
854
+ `,
855
+ });
856
+
857
+ await page.goto('/todo');
858
+
859
+ await page.getByRole('textbox', { name: 'New task' }).fill('Instant task');
860
+ await page.getByRole('button', { name: 'Add' }).click();
861
+
862
+ await expect(page.getByText('Instant task')).toBeVisible();
863
+ });
864
+ });
865
+ ```
866
+
867
+ ### Testing Composition API Components
868
+
869
+ **Use when**: Testing components built with the Vue 3 Composition API (`<script setup>`, `setup()` function). From Playwright's perspective, Composition API and Options API components are identical -- you test the rendered output.
870
+ **Avoid when**: You want to test composable functions in isolation -- use Vitest for that.
871
+
872
+ **TypeScript**
873
+ ```typescript
874
+ import { test, expect } from '@playwright/test';
875
+
876
+ test.describe('composition API patterns', () => {
877
+ test('computed properties update the UI reactively', async ({ page }) => {
878
+ await page.goto('/calculator');
879
+
880
+ await page.getByLabel('Price').fill('100');
881
+ await page.getByLabel('Quantity').fill('3');
882
+
883
+ // Computed total should update reactively
884
+ await expect(page.getByTestId('total')).toHaveText('$300.00');
885
+
886
+ // Apply discount
887
+ await page.getByLabel('Discount (%)').fill('10');
888
+ await expect(page.getByTestId('total')).toHaveText('$270.00');
889
+ });
890
+
891
+ test('watcher triggers side effect on value change', async ({ page }) => {
892
+ await page.goto('/settings');
893
+
894
+ // Changing the language triggers a watcher that reloads translations
895
+ await page.getByRole('combobox', { name: 'Language' }).selectOption('fr');
896
+
897
+ // Watcher fetches French translations -- UI updates
898
+ await expect(page.getByRole('heading', { name: 'Parametres' })).toBeVisible();
899
+ });
900
+
901
+ test('composable hook (useSearch) provides debounced search', async ({ page }) => {
902
+ await page.goto('/products');
903
+
904
+ const searchInput = page.getByRole('textbox', { name: 'Search products' });
905
+ await searchInput.pressSequentially('vue stickers', { delay: 50 });
906
+
907
+ // useSearch composable debounces and fetches results
908
+ await expect(page.getByRole('listitem')).toHaveCount(3);
909
+ await expect(page.getByText('Vue Sticker Pack')).toBeVisible();
910
+ });
911
+
912
+ test('provide/inject context works across component tree', async ({ page }) => {
913
+ await page.goto('/dashboard');
914
+
915
+ // Theme is provided at the app root and injected in child components
916
+ await page.getByRole('switch', { name: 'Dark mode' }).click();
917
+
918
+ // All components using inject('theme') should update
919
+ await expect(page.locator('body')).toHaveClass(/dark/);
920
+ await expect(page.getByRole('navigation')).toHaveCSS('background-color', /rgb\(30/);
921
+ });
922
+ });
923
+ ```
924
+
925
+ **JavaScript**
926
+ ```javascript
927
+ const { test, expect } = require('@playwright/test');
928
+
929
+ test.describe('composition API patterns', () => {
930
+ test('computed properties update the UI reactively', async ({ page }) => {
931
+ await page.goto('/calculator');
932
+
933
+ await page.getByLabel('Price').fill('100');
934
+ await page.getByLabel('Quantity').fill('3');
935
+
936
+ await expect(page.getByTestId('total')).toHaveText('$300.00');
937
+
938
+ await page.getByLabel('Discount (%)').fill('10');
939
+ await expect(page.getByTestId('total')).toHaveText('$270.00');
940
+ });
941
+
942
+ test('watcher triggers side effect on value change', async ({ page }) => {
943
+ await page.goto('/settings');
944
+
945
+ await page.getByRole('combobox', { name: 'Language' }).selectOption('fr');
946
+ await expect(page.getByRole('heading', { name: 'Parametres' })).toBeVisible();
947
+ });
948
+
949
+ test('composable hook (useSearch) provides debounced search', async ({ page }) => {
950
+ await page.goto('/products');
951
+
952
+ const searchInput = page.getByRole('textbox', { name: 'Search products' });
953
+ await searchInput.pressSequentially('vue stickers', { delay: 50 });
954
+
955
+ await expect(page.getByRole('listitem')).toHaveCount(3);
956
+ await expect(page.getByText('Vue Sticker Pack')).toBeVisible();
957
+ });
958
+ });
959
+ ```
960
+
961
+ ### Testing Nuxt.js Specific Patterns
962
+
963
+ **Use when**: Testing Nuxt 3 applications with server-side rendering, auto-imports, server routes (`/server/api/`), and middleware.
964
+ **Avoid when**: Testing a plain Vue SPA without Nuxt.
965
+
966
+ **TypeScript**
967
+ ```typescript
968
+ import { test, expect } from '@playwright/test';
969
+
970
+ test.describe('Nuxt-specific patterns', () => {
971
+ test('SSR page renders server-fetched data', async ({ page }) => {
972
+ await page.goto('/blog');
973
+
974
+ // useFetch/useAsyncData runs on the server -- content is in the initial HTML
975
+ await expect(page.getByRole('article')).toHaveCount(10);
976
+ await expect(page.getByRole('article').first()).toContainText(/\w+/);
977
+ });
978
+
979
+ test('Nuxt server route returns correct data', async ({ request }) => {
980
+ const response = await request.get('/api/products');
981
+
982
+ expect(response.ok()).toBeTruthy();
983
+ const body = await response.json();
984
+ expect(body).toBeInstanceOf(Array);
985
+ expect(body[0]).toHaveProperty('id');
986
+ });
987
+
988
+ test('Nuxt middleware redirects unauthenticated users', async ({ page }) => {
989
+ await page.goto('/admin');
990
+
991
+ // definePageMeta({ middleware: 'auth' }) in the page triggers redirect
992
+ await expect(page).toHaveURL(/\/login/);
993
+ });
994
+
995
+ test('NuxtLink enables client-side navigation', async ({ page }) => {
996
+ await page.goto('/');
997
+
998
+ await page.evaluate(() => {
999
+ (window as any).__testMarker = 'no-reload';
1000
+ });
1001
+
1002
+ await page.getByRole('link', { name: 'Blog' }).click();
1003
+ await page.waitForURL('/blog');
1004
+
1005
+ const marker = await page.evaluate(() => (window as any).__testMarker);
1006
+ expect(marker).toBe('no-reload');
1007
+ });
1008
+
1009
+ test('useHead sets meta tags for SEO', async ({ page }) => {
1010
+ await page.goto('/blog/my-post');
1011
+
1012
+ const title = await page.title();
1013
+ expect(title).toContain('My Post');
1014
+
1015
+ const description = await page.locator('meta[name="description"]').getAttribute('content');
1016
+ expect(description).toBeTruthy();
1017
+ expect(description!.length).toBeGreaterThan(50);
1018
+ });
1019
+ });
1020
+ ```
1021
+
1022
+ **JavaScript**
1023
+ ```javascript
1024
+ const { test, expect } = require('@playwright/test');
1025
+
1026
+ test.describe('Nuxt-specific patterns', () => {
1027
+ test('SSR page renders server-fetched data', async ({ page }) => {
1028
+ await page.goto('/blog');
1029
+
1030
+ await expect(page.getByRole('article')).toHaveCount(10);
1031
+ await expect(page.getByRole('article').first()).toContainText(/\w+/);
1032
+ });
1033
+
1034
+ test('Nuxt server route returns correct data', async ({ request }) => {
1035
+ const response = await request.get('/api/products');
1036
+
1037
+ expect(response.ok()).toBeTruthy();
1038
+ const body = await response.json();
1039
+ expect(body).toBeInstanceOf(Array);
1040
+ expect(body[0]).toHaveProperty('id');
1041
+ });
1042
+
1043
+ test('Nuxt middleware redirects unauthenticated users', async ({ page }) => {
1044
+ await page.goto('/admin');
1045
+
1046
+ await expect(page).toHaveURL(/\/login/);
1047
+ });
1048
+
1049
+ test('NuxtLink enables client-side navigation', async ({ page }) => {
1050
+ await page.goto('/');
1051
+
1052
+ await page.evaluate(() => {
1053
+ window.__testMarker = 'no-reload';
1054
+ });
1055
+
1056
+ await page.getByRole('link', { name: 'Blog' }).click();
1057
+ await page.waitForURL('/blog');
1058
+
1059
+ const marker = await page.evaluate(() => window.__testMarker);
1060
+ expect(marker).toBe('no-reload');
1061
+ });
1062
+ });
1063
+ ```
1064
+
1065
+ ## Framework-Specific Tips
1066
+
1067
+ ### Vue DevTools and Playwright
1068
+
1069
+ Vue DevTools is a browser extension. It does not interfere with Playwright tests since Playwright launches its own browser profile without extensions. Do not rely on Vue DevTools for debugging in CI -- use Playwright traces instead.
1070
+
1071
+ ### Testing `v-model` Two-Way Binding
1072
+
1073
+ `v-model` on form inputs works through standard HTML events. Playwright's `fill()`, `check()`, `selectOption()` methods trigger the correct events automatically. No special handling is needed.
1074
+
1075
+ ```typescript
1076
+ // This works for any v-model input -- no special Vue handling needed
1077
+ await page.getByLabel('Username').fill('janedoe');
1078
+ await page.getByRole('checkbox', { name: 'Remember me' }).check();
1079
+ await page.getByRole('combobox', { name: 'Role' }).selectOption('admin');
1080
+ ```
1081
+
1082
+ ### Vue vs Nuxt: Configuration Differences
1083
+
1084
+ | Aspect | Vue 3 (Vite) | Nuxt 3 |
1085
+ |---|---|---|
1086
+ | Default port | `5173` | `3000` |
1087
+ | Dev command | `npm run dev` (vite) | `npx nuxi dev` |
1088
+ | Build + preview | `npm run build && npx vite preview` | `npx nuxi build && npx nuxi preview` |
1089
+ | SSR | Optional (vue-server-renderer) | Built-in |
1090
+ | API routes | External backend | `/server/api/` built-in |
1091
+ | Environment variables | `VITE_*` prefix | `NUXT_PUBLIC_*` prefix for client, `NUXT_*` for server |
1092
+ | File-based routing | No (uses vue-router config) | Yes (automatic from `/pages/`) |
1093
+
1094
+ ### Component Testing: Providing Pinia and Router
1095
+
1096
+ When using `@playwright/experimental-ct-vue`, components that depend on Pinia or Vue Router need these dependencies provided. Configure the test wrapper:
1097
+
1098
+ ```typescript
1099
+ // playwright/index.ts (component testing setup)
1100
+ import { beforeMount } from '@playwright/experimental-ct-vue/hooks';
1101
+ import { createPinia } from 'pinia';
1102
+ import { createMemoryHistory, createRouter } from 'vue-router';
1103
+
1104
+ beforeMount(async ({ app, hooksConfig }) => {
1105
+ // Install Pinia for all component tests
1106
+ const pinia = createPinia();
1107
+ app.use(pinia);
1108
+
1109
+ // Install a minimal router if the component uses <RouterLink> or useRoute()
1110
+ if (hooksConfig?.routes) {
1111
+ const router = createRouter({
1112
+ history: createMemoryHistory(),
1113
+ routes: hooksConfig.routes,
1114
+ });
1115
+ app.use(router);
1116
+ }
1117
+ });
1118
+ ```
1119
+
1120
+ ### Handling Vue Warnings in Tests
1121
+
1122
+ Vue emits runtime warnings (prop validation, missing components, etc.) that can indicate real issues:
1123
+
1124
+ ```typescript
1125
+ test('no Vue warnings during render', async ({ page }) => {
1126
+ const warnings: string[] = [];
1127
+ page.on('console', (msg) => {
1128
+ if (msg.type() === 'warning' && msg.text().includes('[Vue warn]')) {
1129
+ warnings.push(msg.text());
1130
+ }
1131
+ });
1132
+
1133
+ await page.goto('/dashboard');
1134
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
1135
+
1136
+ expect(warnings).toEqual([]);
1137
+ });
1138
+ ```
1139
+
1140
+ ## Anti-Patterns
1141
+
1142
+ | Don't Do This | Problem | Do This Instead |
1143
+ |---|---|---|
1144
+ | `page.evaluate(() => app.__vue_app__.config.globalProperties.$store)` | Accesses Vue internals; breaks on upgrades; tests implementation | Assert on the UI that state produces |
1145
+ | `page.locator('[data-v-abc123]')` (scoped style hash) | Vue generates random scoped attribute hashes; changes on every build | Use `getByRole`, `getByText`, `getByTestId` |
1146
+ | Import `.vue` files in E2E tests | E2E tests run in Node.js; `.vue` files need Vite/Webpack compilation | Use `@playwright/experimental-ct-vue` for component tests |
1147
+ | `page.waitForTimeout(300)` to wait for transition to finish | Transition durations vary; arbitrary waits are fragile | `await expect(locator).toBeVisible()` auto-waits through transitions |
1148
+ | Mock Pinia stores by patching `window.__pinia` | Fragile; depends on Pinia internals; may not trigger reactivity | Control state through UI interactions or mock the API responses the store consumes |
1149
+ | Test composables by calling them via `page.evaluate` | Composables rely on Vue's setup context; calling them outside a component fails | Test composables through the components that use them, or unit test with Vitest |
1150
+ | Use `page.locator('.v-btn')` for Vuetify components | Vuetify class names are internal and change between versions | `page.getByRole('button', { name: 'Submit' })` works regardless of the component library |
1151
+ | Skip testing keyboard navigation for custom components | Vue component libraries often have incomplete keyboard support | Test `Tab`, `Enter`, `Escape`, `ArrowDown/Up` on dropdowns, modals, tabs |
1152
+ | Run Nuxt dev server in CI | Dev mode includes hot reload overhead, slower builds, development warnings | Use `npx nuxi build && npx nuxi preview` in CI |
1153
+
1154
+ ## Related
1155
+
1156
+ - [core/locators.md](locators.md) -- locator strategies for any Vue component library (Vuetify, PrimeVue, Quasar)
1157
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- auto-waiting assertions for Vue reactivity
1158
+ - [core/component-testing.md](component-testing.md) -- in-depth component testing patterns
1159
+ - [core/forms-and-validation.md](forms-and-validation.md) -- form testing patterns for VeeValidate and FormKit
1160
+ - [core/accessibility.md](accessibility.md) -- accessibility testing for Vue component libraries
1161
+ - [core/test-architecture.md](test-architecture.md) -- when to use E2E vs component vs unit tests
1162
+ - [core/nextjs.md](nextjs.md) -- comparison: Nuxt vs Next.js testing patterns