@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,1081 @@
1
+ # Authentication Flow Recipes
2
+
3
+ > **When to use**: You need to test login, signup, logout, session management, or any authentication-related user flow.
4
+
5
+ ---
6
+
7
+ ## Recipe 1: Basic Login
8
+
9
+ ### Complete Example
10
+
11
+ **TypeScript**
12
+
13
+ ```typescript
14
+ import { test, expect } from '@playwright/test';
15
+
16
+ test('logs in with valid credentials', async ({ page }) => {
17
+ await page.goto('/login');
18
+
19
+ await page.getByLabel('Email').fill('user@example.com');
20
+ await page.getByLabel('Password').fill('SecurePass123!');
21
+ await page.getByRole('button', { name: 'Sign in' }).click();
22
+
23
+ // Wait for navigation to complete after login
24
+ await expect(page).toHaveURL('/dashboard');
25
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
26
+ await expect(page.getByText('Welcome, user@example.com')).toBeVisible();
27
+ });
28
+
29
+ test('shows error for invalid credentials', async ({ page }) => {
30
+ await page.goto('/login');
31
+
32
+ await page.getByLabel('Email').fill('user@example.com');
33
+ await page.getByLabel('Password').fill('WrongPassword');
34
+ await page.getByRole('button', { name: 'Sign in' }).click();
35
+
36
+ await expect(page.getByRole('alert')).toContainText('Invalid email or password');
37
+ // Should remain on login page
38
+ await expect(page).toHaveURL('/login');
39
+ });
40
+
41
+ test('shows validation errors for empty fields', async ({ page }) => {
42
+ await page.goto('/login');
43
+
44
+ await page.getByRole('button', { name: 'Sign in' }).click();
45
+
46
+ await expect(page.getByText('Email is required')).toBeVisible();
47
+ await expect(page.getByText('Password is required')).toBeVisible();
48
+ });
49
+ ```
50
+
51
+ **JavaScript**
52
+
53
+ ```javascript
54
+ const { test, expect } = require('@playwright/test');
55
+
56
+ test('logs in with valid credentials', async ({ page }) => {
57
+ await page.goto('/login');
58
+
59
+ await page.getByLabel('Email').fill('user@example.com');
60
+ await page.getByLabel('Password').fill('SecurePass123!');
61
+ await page.getByRole('button', { name: 'Sign in' }).click();
62
+
63
+ await expect(page).toHaveURL('/dashboard');
64
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
65
+ await expect(page.getByText('Welcome, user@example.com')).toBeVisible();
66
+ });
67
+
68
+ test('shows error for invalid credentials', async ({ page }) => {
69
+ await page.goto('/login');
70
+
71
+ await page.getByLabel('Email').fill('user@example.com');
72
+ await page.getByLabel('Password').fill('WrongPassword');
73
+ await page.getByRole('button', { name: 'Sign in' }).click();
74
+
75
+ await expect(page.getByRole('alert')).toContainText('Invalid email or password');
76
+ await expect(page).toHaveURL('/login');
77
+ });
78
+
79
+ test('shows validation errors for empty fields', async ({ page }) => {
80
+ await page.goto('/login');
81
+
82
+ await page.getByRole('button', { name: 'Sign in' }).click();
83
+
84
+ await expect(page.getByText('Email is required')).toBeVisible();
85
+ await expect(page.getByText('Password is required')).toBeVisible();
86
+ });
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Recipe 2: Login with "Remember Me"
92
+
93
+ ### Complete Example
94
+
95
+ **TypeScript**
96
+
97
+ ```typescript
98
+ import { test, expect, type BrowserContext } from '@playwright/test';
99
+
100
+ test('remember me persists session across browser restarts', async ({ browser }) => {
101
+ // First session: log in with "remember me" checked
102
+ const context1 = await browser.newContext();
103
+ const page1 = await context1.newPage();
104
+
105
+ await page1.goto('/login');
106
+ await page1.getByLabel('Email').fill('user@example.com');
107
+ await page1.getByLabel('Password').fill('SecurePass123!');
108
+ await page1.getByLabel('Remember me').check();
109
+ await page1.getByRole('button', { name: 'Sign in' }).click();
110
+
111
+ await expect(page1).toHaveURL('/dashboard');
112
+
113
+ // Save storage state (cookies + localStorage)
114
+ const storageState = await context1.storageState();
115
+ await context1.close();
116
+
117
+ // Second session: restore state to simulate browser restart
118
+ const context2 = await browser.newContext({ storageState });
119
+ const page2 = await context2.newPage();
120
+
121
+ await page2.goto('/dashboard');
122
+
123
+ // Should still be logged in without re-authenticating
124
+ await expect(page2).toHaveURL('/dashboard');
125
+ await expect(page2.getByText('Welcome, user@example.com')).toBeVisible();
126
+
127
+ await context2.close();
128
+ });
129
+
130
+ test('no remember me does not persist session', async ({ browser }) => {
131
+ const context1 = await browser.newContext();
132
+ const page1 = await context1.newPage();
133
+
134
+ await page1.goto('/login');
135
+ await page1.getByLabel('Email').fill('user@example.com');
136
+ await page1.getByLabel('Password').fill('SecurePass123!');
137
+ // Explicitly leave "Remember me" unchecked
138
+ await expect(page1.getByLabel('Remember me')).not.toBeChecked();
139
+ await page1.getByRole('button', { name: 'Sign in' }).click();
140
+
141
+ await expect(page1).toHaveURL('/dashboard');
142
+
143
+ // Capture only cookies (not sessionStorage) to simulate new tab/window
144
+ const cookies = await context1.cookies();
145
+ await context1.close();
146
+
147
+ // New context with only persistent cookies (session cookies are excluded)
148
+ const persistentCookies = cookies.filter(c => c.expires > 0);
149
+ const context2 = await browser.newContext();
150
+ await context2.addCookies(persistentCookies);
151
+ const page2 = await context2.newPage();
152
+
153
+ await page2.goto('/dashboard');
154
+
155
+ // Should redirect to login since session was not persisted
156
+ await expect(page2).toHaveURL(/\/login/);
157
+
158
+ await context2.close();
159
+ });
160
+ ```
161
+
162
+ **JavaScript**
163
+
164
+ ```javascript
165
+ const { test, expect } = require('@playwright/test');
166
+
167
+ test('remember me persists session across browser restarts', async ({ browser }) => {
168
+ const context1 = await browser.newContext();
169
+ const page1 = await context1.newPage();
170
+
171
+ await page1.goto('/login');
172
+ await page1.getByLabel('Email').fill('user@example.com');
173
+ await page1.getByLabel('Password').fill('SecurePass123!');
174
+ await page1.getByLabel('Remember me').check();
175
+ await page1.getByRole('button', { name: 'Sign in' }).click();
176
+
177
+ await expect(page1).toHaveURL('/dashboard');
178
+
179
+ const storageState = await context1.storageState();
180
+ await context1.close();
181
+
182
+ const context2 = await browser.newContext({ storageState });
183
+ const page2 = await context2.newPage();
184
+
185
+ await page2.goto('/dashboard');
186
+
187
+ await expect(page2).toHaveURL('/dashboard');
188
+ await expect(page2.getByText('Welcome, user@example.com')).toBeVisible();
189
+
190
+ await context2.close();
191
+ });
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Recipe 3: Signup with Email Verification (Mocked)
197
+
198
+ ### Complete Example
199
+
200
+ **TypeScript**
201
+
202
+ ```typescript
203
+ import { test, expect } from '@playwright/test';
204
+
205
+ test('completes signup flow with mocked email verification', async ({ page }) => {
206
+ // Intercept the verification email API so we can capture the token
207
+ let verificationToken = '';
208
+ await page.route('**/api/auth/send-verification', async (route) => {
209
+ // Let the request go through to the server
210
+ const response = await route.fetch();
211
+ const body = await response.json();
212
+
213
+ // Capture the token from the API response (test environments expose this)
214
+ verificationToken = body.verificationToken;
215
+
216
+ await route.fulfill({ response });
217
+ });
218
+
219
+ // Step 1: Fill out signup form
220
+ await page.goto('/signup');
221
+
222
+ await page.getByLabel('Full name').fill('Jane Tester');
223
+ await page.getByLabel('Email').fill('jane@example.com');
224
+ await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
225
+ await page.getByLabel('Confirm password').fill('SecurePass123!');
226
+ await page.getByLabel('I agree to the Terms of Service').check();
227
+ await page.getByRole('button', { name: 'Create account' }).click();
228
+
229
+ // Step 2: Verify "check your email" screen appears
230
+ await expect(page.getByText('Check your email')).toBeVisible();
231
+ await expect(page.getByText('jane@example.com')).toBeVisible();
232
+
233
+ // Step 3: Navigate directly to verification URL using captured token
234
+ expect(verificationToken).toBeTruthy();
235
+ await page.goto(`/verify-email?token=${verificationToken}`);
236
+
237
+ // Step 4: Verify account is now active
238
+ await expect(page.getByText('Email verified successfully')).toBeVisible();
239
+ await expect(page).toHaveURL('/login');
240
+
241
+ // Step 5: Log in with the new account
242
+ await page.getByLabel('Email').fill('jane@example.com');
243
+ await page.getByLabel('Password').fill('SecurePass123!');
244
+ await page.getByRole('button', { name: 'Sign in' }).click();
245
+
246
+ await expect(page).toHaveURL('/dashboard');
247
+ await expect(page.getByText('Welcome, Jane Tester')).toBeVisible();
248
+ });
249
+
250
+ test('signup with fully mocked email API (no server dependency)', async ({ page }) => {
251
+ const fakeToken = 'test-verification-token-12345';
252
+
253
+ // Mock the signup API endpoint
254
+ await page.route('**/api/auth/signup', async (route) => {
255
+ await route.fulfill({
256
+ status: 200,
257
+ contentType: 'application/json',
258
+ body: JSON.stringify({
259
+ message: 'Verification email sent',
260
+ verificationToken: fakeToken,
261
+ }),
262
+ });
263
+ });
264
+
265
+ // Mock the verification endpoint
266
+ await page.route(`**/api/auth/verify-email?token=${fakeToken}`, async (route) => {
267
+ await route.fulfill({
268
+ status: 200,
269
+ contentType: 'application/json',
270
+ body: JSON.stringify({ message: 'Email verified', redirectTo: '/login' }),
271
+ });
272
+ });
273
+
274
+ await page.goto('/signup');
275
+
276
+ await page.getByLabel('Full name').fill('Jane Tester');
277
+ await page.getByLabel('Email').fill('jane@example.com');
278
+ await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
279
+ await page.getByLabel('Confirm password').fill('SecurePass123!');
280
+ await page.getByLabel('I agree to the Terms of Service').check();
281
+ await page.getByRole('button', { name: 'Create account' }).click();
282
+
283
+ await expect(page.getByText('Check your email')).toBeVisible();
284
+
285
+ // Simulate clicking the verification link from the email
286
+ await page.goto(`/verify-email?token=${fakeToken}`);
287
+
288
+ await expect(page.getByText('Email verified successfully')).toBeVisible();
289
+ });
290
+ ```
291
+
292
+ **JavaScript**
293
+
294
+ ```javascript
295
+ const { test, expect } = require('@playwright/test');
296
+
297
+ test('completes signup flow with mocked email verification', async ({ page }) => {
298
+ let verificationToken = '';
299
+ await page.route('**/api/auth/send-verification', async (route) => {
300
+ const response = await route.fetch();
301
+ const body = await response.json();
302
+ verificationToken = body.verificationToken;
303
+ await route.fulfill({ response });
304
+ });
305
+
306
+ await page.goto('/signup');
307
+
308
+ await page.getByLabel('Full name').fill('Jane Tester');
309
+ await page.getByLabel('Email').fill('jane@example.com');
310
+ await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
311
+ await page.getByLabel('Confirm password').fill('SecurePass123!');
312
+ await page.getByLabel('I agree to the Terms of Service').check();
313
+ await page.getByRole('button', { name: 'Create account' }).click();
314
+
315
+ await expect(page.getByText('Check your email')).toBeVisible();
316
+
317
+ expect(verificationToken).toBeTruthy();
318
+ await page.goto(`/verify-email?token=${verificationToken}`);
319
+
320
+ await expect(page.getByText('Email verified successfully')).toBeVisible();
321
+ await expect(page).toHaveURL('/login');
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Recipe 4: Password Reset Flow
328
+
329
+ ### Complete Example
330
+
331
+ **TypeScript**
332
+
333
+ ```typescript
334
+ import { test, expect } from '@playwright/test';
335
+
336
+ test('completes password reset flow', async ({ page }) => {
337
+ let resetToken = '';
338
+
339
+ // Intercept the password reset API to capture the reset token
340
+ await page.route('**/api/auth/forgot-password', async (route) => {
341
+ const response = await route.fetch();
342
+ const body = await response.json();
343
+ resetToken = body.resetToken;
344
+ await route.fulfill({ response });
345
+ });
346
+
347
+ // Step 1: Request password reset
348
+ await page.goto('/forgot-password');
349
+
350
+ await page.getByLabel('Email').fill('user@example.com');
351
+ await page.getByRole('button', { name: 'Send reset link' }).click();
352
+
353
+ await expect(page.getByText('Reset link sent')).toBeVisible();
354
+ await expect(page.getByText('user@example.com')).toBeVisible();
355
+
356
+ // Step 2: Navigate to the reset page with token
357
+ expect(resetToken).toBeTruthy();
358
+ await page.goto(`/reset-password?token=${resetToken}`);
359
+
360
+ // Step 3: Set new password
361
+ await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
362
+ await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
363
+ await page.getByRole('button', { name: 'Reset password' }).click();
364
+
365
+ await expect(page.getByText('Password reset successfully')).toBeVisible();
366
+
367
+ // Step 4: Log in with new password
368
+ await page.goto('/login');
369
+ await page.getByLabel('Email').fill('user@example.com');
370
+ await page.getByLabel('Password').fill('NewSecurePass456!');
371
+ await page.getByRole('button', { name: 'Sign in' }).click();
372
+
373
+ await expect(page).toHaveURL('/dashboard');
374
+ });
375
+
376
+ test('password reset with expired token shows error', async ({ page }) => {
377
+ await page.goto('/reset-password?token=expired-token-123');
378
+
379
+ await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
380
+ await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
381
+ await page.getByRole('button', { name: 'Reset password' }).click();
382
+
383
+ await expect(page.getByRole('alert')).toContainText(
384
+ /token has expired|link is no longer valid/i
385
+ );
386
+ await expect(page.getByRole('link', { name: 'Request a new reset link' })).toBeVisible();
387
+ });
388
+
389
+ test('password reset enforces password strength requirements', async ({ page }) => {
390
+ await page.goto('/reset-password?token=valid-token');
391
+
392
+ // Try a weak password
393
+ await page.getByLabel('New password', { exact: true }).fill('123');
394
+ await page.getByLabel('Confirm new password').fill('123');
395
+ await page.getByRole('button', { name: 'Reset password' }).click();
396
+
397
+ await expect(page.getByText(/at least 8 characters/i)).toBeVisible();
398
+ });
399
+ ```
400
+
401
+ **JavaScript**
402
+
403
+ ```javascript
404
+ const { test, expect } = require('@playwright/test');
405
+
406
+ test('completes password reset flow', async ({ page }) => {
407
+ let resetToken = '';
408
+
409
+ await page.route('**/api/auth/forgot-password', async (route) => {
410
+ const response = await route.fetch();
411
+ const body = await response.json();
412
+ resetToken = body.resetToken;
413
+ await route.fulfill({ response });
414
+ });
415
+
416
+ await page.goto('/forgot-password');
417
+
418
+ await page.getByLabel('Email').fill('user@example.com');
419
+ await page.getByRole('button', { name: 'Send reset link' }).click();
420
+
421
+ await expect(page.getByText('Reset link sent')).toBeVisible();
422
+
423
+ expect(resetToken).toBeTruthy();
424
+ await page.goto(`/reset-password?token=${resetToken}`);
425
+
426
+ await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
427
+ await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
428
+ await page.getByRole('button', { name: 'Reset password' }).click();
429
+
430
+ await expect(page.getByText('Password reset successfully')).toBeVisible();
431
+
432
+ await page.goto('/login');
433
+ await page.getByLabel('Email').fill('user@example.com');
434
+ await page.getByLabel('Password').fill('NewSecurePass456!');
435
+ await page.getByRole('button', { name: 'Sign in' }).click();
436
+
437
+ await expect(page).toHaveURL('/dashboard');
438
+ });
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Recipe 5: OAuth Login (Mocked Callback)
444
+
445
+ ### Complete Example
446
+
447
+ **TypeScript**
448
+
449
+ ```typescript
450
+ import { test, expect } from '@playwright/test';
451
+
452
+ test('logs in via mocked Google OAuth callback', async ({ page }) => {
453
+ // Intercept the OAuth redirect to Google and simulate the callback
454
+ await page.route('**/accounts.google.com/**', async (route) => {
455
+ // Instead of actually going to Google, redirect back to our callback URL
456
+ // with a mock authorization code
457
+ const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
458
+ callbackUrl.searchParams.set('code', 'mock-auth-code-12345');
459
+ callbackUrl.searchParams.set('state', route.request().url().match(/state=([^&]+)/)?.[1] || '');
460
+
461
+ await route.fulfill({
462
+ status: 302,
463
+ headers: { location: callbackUrl.toString() },
464
+ });
465
+ });
466
+
467
+ // Mock the token exchange on our backend
468
+ await page.route('**/api/auth/callback/google**', async (route) => {
469
+ await route.fulfill({
470
+ status: 200,
471
+ contentType: 'application/json',
472
+ body: JSON.stringify({
473
+ user: {
474
+ id: '1',
475
+ email: 'googleuser@gmail.com',
476
+ name: 'Google User',
477
+ avatar: 'https://example.com/avatar.jpg',
478
+ },
479
+ token: 'mock-jwt-token',
480
+ }),
481
+ });
482
+ });
483
+
484
+ await page.goto('/login');
485
+
486
+ // Click the OAuth button
487
+ await page.getByRole('button', { name: /Sign in with Google/i }).click();
488
+
489
+ // After the mocked OAuth flow, we should land on the dashboard
490
+ await expect(page).toHaveURL('/dashboard');
491
+ await expect(page.getByText('Google User')).toBeVisible();
492
+ });
493
+
494
+ test('handles OAuth login failure gracefully', async ({ page }) => {
495
+ // Simulate OAuth provider returning an error
496
+ await page.route('**/accounts.google.com/**', async (route) => {
497
+ const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
498
+ callbackUrl.searchParams.set('error', 'access_denied');
499
+ callbackUrl.searchParams.set('error_description', 'User denied access');
500
+
501
+ await route.fulfill({
502
+ status: 302,
503
+ headers: { location: callbackUrl.toString() },
504
+ });
505
+ });
506
+
507
+ await page.goto('/login');
508
+ await page.getByRole('button', { name: /Sign in with Google/i }).click();
509
+
510
+ await expect(page.getByRole('alert')).toContainText(/authentication failed|access denied/i);
511
+ await expect(page).toHaveURL(/\/login/);
512
+ });
513
+ ```
514
+
515
+ **JavaScript**
516
+
517
+ ```javascript
518
+ const { test, expect } = require('@playwright/test');
519
+
520
+ test('logs in via mocked Google OAuth callback', async ({ page }) => {
521
+ await page.route('**/accounts.google.com/**', async (route) => {
522
+ const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
523
+ callbackUrl.searchParams.set('code', 'mock-auth-code-12345');
524
+ callbackUrl.searchParams.set('state', route.request().url().match(/state=([^&]+)/)?.[1] || '');
525
+
526
+ await route.fulfill({
527
+ status: 302,
528
+ headers: { location: callbackUrl.toString() },
529
+ });
530
+ });
531
+
532
+ await page.route('**/api/auth/callback/google**', async (route) => {
533
+ await route.fulfill({
534
+ status: 200,
535
+ contentType: 'application/json',
536
+ body: JSON.stringify({
537
+ user: { id: '1', email: 'googleuser@gmail.com', name: 'Google User' },
538
+ token: 'mock-jwt-token',
539
+ }),
540
+ });
541
+ });
542
+
543
+ await page.goto('/login');
544
+ await page.getByRole('button', { name: /Sign in with Google/i }).click();
545
+
546
+ await expect(page).toHaveURL('/dashboard');
547
+ await expect(page.getByText('Google User')).toBeVisible();
548
+ });
549
+ ```
550
+
551
+ ---
552
+
553
+ ## Recipe 6: Role-Based Access Testing
554
+
555
+ ### Complete Example
556
+
557
+ **TypeScript**
558
+
559
+ ```typescript
560
+ import { test, expect, type Page } from '@playwright/test';
561
+ import path from 'path';
562
+
563
+ // Store auth state for each role in separate files
564
+ const authDir = path.resolve(__dirname, '../.auth');
565
+
566
+ // Setup: create auth state files for each role (run in globalSetup or auth.setup)
567
+ // This pattern uses Playwright's storage state for efficient role switching
568
+ const roles = ['admin', 'editor', 'viewer'] as const;
569
+ type Role = typeof roles[number];
570
+
571
+ function authFile(role: Role): string {
572
+ return path.join(authDir, `${role}.json`);
573
+ }
574
+
575
+ // Auth setup project — runs before all tests
576
+ test.describe('auth setup', () => {
577
+ const credentials: Record<Role, { email: string; password: string }> = {
578
+ admin: { email: 'admin@example.com', password: 'AdminPass123!' },
579
+ editor: { email: 'editor@example.com', password: 'EditorPass123!' },
580
+ viewer: { email: 'viewer@example.com', password: 'ViewerPass123!' },
581
+ };
582
+
583
+ for (const role of roles) {
584
+ test(`authenticate as ${role}`, async ({ page }) => {
585
+ await page.goto('/login');
586
+ await page.getByLabel('Email').fill(credentials[role].email);
587
+ await page.getByLabel('Password').fill(credentials[role].password);
588
+ await page.getByRole('button', { name: 'Sign in' }).click();
589
+ await expect(page).toHaveURL('/dashboard');
590
+
591
+ await page.context().storageState({ path: authFile(role) });
592
+ });
593
+ }
594
+ });
595
+
596
+ // Admin tests
597
+ test.describe('admin access', () => {
598
+ test.use({ storageState: authFile('admin') });
599
+
600
+ test('admin can access user management', async ({ page }) => {
601
+ await page.goto('/admin/users');
602
+ await expect(page).toHaveURL('/admin/users');
603
+ await expect(page.getByRole('heading', { name: 'User Management' })).toBeVisible();
604
+ await expect(page.getByRole('button', { name: 'Add user' })).toBeVisible();
605
+ await expect(page.getByRole('button', { name: 'Delete' }).first()).toBeVisible();
606
+ });
607
+
608
+ test('admin can access system settings', async ({ page }) => {
609
+ await page.goto('/admin/settings');
610
+ await expect(page).toHaveURL('/admin/settings');
611
+ await expect(page.getByRole('heading', { name: 'System Settings' })).toBeVisible();
612
+ });
613
+ });
614
+
615
+ // Editor tests
616
+ test.describe('editor access', () => {
617
+ test.use({ storageState: authFile('editor') });
618
+
619
+ test('editor can create and edit content', async ({ page }) => {
620
+ await page.goto('/content');
621
+ await expect(page.getByRole('button', { name: 'New post' })).toBeVisible();
622
+ await expect(page.getByRole('button', { name: 'Edit' }).first()).toBeVisible();
623
+ });
624
+
625
+ test('editor cannot access admin area', async ({ page }) => {
626
+ await page.goto('/admin/users');
627
+ // Should be redirected or shown forbidden
628
+ await expect(page).toHaveURL(/\/(403|dashboard|login)/);
629
+ });
630
+ });
631
+
632
+ // Viewer tests
633
+ test.describe('viewer access', () => {
634
+ test.use({ storageState: authFile('viewer') });
635
+
636
+ test('viewer can read content', async ({ page }) => {
637
+ await page.goto('/content');
638
+ await expect(page.getByRole('article').first()).toBeVisible();
639
+ });
640
+
641
+ test('viewer cannot create content', async ({ page }) => {
642
+ await page.goto('/content');
643
+ await expect(page.getByRole('button', { name: 'New post' })).not.toBeVisible();
644
+ });
645
+
646
+ test('viewer cannot access admin area', async ({ page }) => {
647
+ await page.goto('/admin/users');
648
+ await expect(page).toHaveURL(/\/(403|dashboard|login)/);
649
+ });
650
+ });
651
+ ```
652
+
653
+ **JavaScript**
654
+
655
+ ```javascript
656
+ const { test, expect } = require('@playwright/test');
657
+ const path = require('path');
658
+
659
+ const authDir = path.resolve(__dirname, '../.auth');
660
+
661
+ function authFile(role) {
662
+ return path.join(authDir, `${role}.json`);
663
+ }
664
+
665
+ // Admin tests
666
+ test.describe('admin access', () => {
667
+ test.use({ storageState: authFile('admin') });
668
+
669
+ test('admin can access user management', async ({ page }) => {
670
+ await page.goto('/admin/users');
671
+ await expect(page).toHaveURL('/admin/users');
672
+ await expect(page.getByRole('heading', { name: 'User Management' })).toBeVisible();
673
+ await expect(page.getByRole('button', { name: 'Add user' })).toBeVisible();
674
+ });
675
+
676
+ test('admin can access system settings', async ({ page }) => {
677
+ await page.goto('/admin/settings');
678
+ await expect(page).toHaveURL('/admin/settings');
679
+ });
680
+ });
681
+
682
+ // Viewer tests
683
+ test.describe('viewer access', () => {
684
+ test.use({ storageState: authFile('viewer') });
685
+
686
+ test('viewer cannot create content', async ({ page }) => {
687
+ await page.goto('/content');
688
+ await expect(page.getByRole('button', { name: 'New post' })).not.toBeVisible();
689
+ });
690
+
691
+ test('viewer cannot access admin area', async ({ page }) => {
692
+ await page.goto('/admin/users');
693
+ await expect(page).toHaveURL(/\/(403|dashboard|login)/);
694
+ });
695
+ });
696
+ ```
697
+
698
+ ---
699
+
700
+ ## Recipe 7: Session Timeout Handling
701
+
702
+ ### Complete Example
703
+
704
+ **TypeScript**
705
+
706
+ ```typescript
707
+ import { test, expect } from '@playwright/test';
708
+
709
+ test('redirects to login after session timeout', async ({ page, context }) => {
710
+ // Log in first
711
+ await page.goto('/login');
712
+ await page.getByLabel('Email').fill('user@example.com');
713
+ await page.getByLabel('Password').fill('SecurePass123!');
714
+ await page.getByRole('button', { name: 'Sign in' }).click();
715
+ await expect(page).toHaveURL('/dashboard');
716
+
717
+ // Simulate session expiration by clearing the session cookie
718
+ const cookies = await context.cookies();
719
+ const sessionCookie = cookies.find(
720
+ (c) => c.name === 'session' || c.name === 'sid' || c.name === 'connect.sid'
721
+ );
722
+
723
+ if (sessionCookie) {
724
+ await context.clearCookies({ name: sessionCookie.name });
725
+ }
726
+
727
+ // Try to navigate to a protected page
728
+ await page.goto('/settings');
729
+
730
+ // Should redirect to login with a return URL
731
+ await expect(page).toHaveURL(/\/login/);
732
+ await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible();
733
+ });
734
+
735
+ test('shows session expiry warning before timeout', async ({ page }) => {
736
+ // Mock the session check endpoint to return "expiring soon"
737
+ await page.route('**/api/auth/session', async (route) => {
738
+ await route.fulfill({
739
+ status: 200,
740
+ contentType: 'application/json',
741
+ body: JSON.stringify({
742
+ valid: true,
743
+ expiresIn: 120, // 2 minutes remaining
744
+ }),
745
+ });
746
+ });
747
+
748
+ await page.goto('/dashboard');
749
+
750
+ // The app should show a warning when session is about to expire
751
+ await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 });
752
+ await expect(page.getByRole('button', { name: /extend|stay logged in/i })).toBeVisible();
753
+ });
754
+
755
+ test('extends session when user clicks extend', async ({ page }) => {
756
+ let sessionExtended = false;
757
+
758
+ await page.route('**/api/auth/session', async (route) => {
759
+ await route.fulfill({
760
+ status: 200,
761
+ contentType: 'application/json',
762
+ body: JSON.stringify({ valid: true, expiresIn: 120 }),
763
+ });
764
+ });
765
+
766
+ await page.route('**/api/auth/refresh', async (route) => {
767
+ sessionExtended = true;
768
+ await route.fulfill({
769
+ status: 200,
770
+ contentType: 'application/json',
771
+ body: JSON.stringify({ valid: true, expiresIn: 3600 }),
772
+ });
773
+ });
774
+
775
+ await page.goto('/dashboard');
776
+
777
+ await expect(page.getByRole('button', { name: /extend|stay logged in/i })).toBeVisible({
778
+ timeout: 10000,
779
+ });
780
+ await page.getByRole('button', { name: /extend|stay logged in/i }).click();
781
+
782
+ expect(sessionExtended).toBe(true);
783
+ await expect(page.getByText(/session.*expir/i)).not.toBeVisible();
784
+ });
785
+ ```
786
+
787
+ **JavaScript**
788
+
789
+ ```javascript
790
+ const { test, expect } = require('@playwright/test');
791
+
792
+ test('redirects to login after session timeout', async ({ page, context }) => {
793
+ await page.goto('/login');
794
+ await page.getByLabel('Email').fill('user@example.com');
795
+ await page.getByLabel('Password').fill('SecurePass123!');
796
+ await page.getByRole('button', { name: 'Sign in' }).click();
797
+ await expect(page).toHaveURL('/dashboard');
798
+
799
+ const cookies = await context.cookies();
800
+ const sessionCookie = cookies.find(
801
+ (c) => c.name === 'session' || c.name === 'sid' || c.name === 'connect.sid'
802
+ );
803
+
804
+ if (sessionCookie) {
805
+ await context.clearCookies({ name: sessionCookie.name });
806
+ }
807
+
808
+ await page.goto('/settings');
809
+
810
+ await expect(page).toHaveURL(/\/login/);
811
+ await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible();
812
+ });
813
+ ```
814
+
815
+ ---
816
+
817
+ ## Recipe 8: Logout
818
+
819
+ ### Complete Example
820
+
821
+ **TypeScript**
822
+
823
+ ```typescript
824
+ import { test, expect } from '@playwright/test';
825
+
826
+ // Use a logged-in state
827
+ test.use({ storageState: '.auth/user.json' });
828
+
829
+ test('logs out and redirects to login page', async ({ page }) => {
830
+ await page.goto('/dashboard');
831
+ await expect(page.getByText('Welcome')).toBeVisible();
832
+
833
+ // Open user menu and click logout
834
+ await page.getByRole('button', { name: /user menu|account|profile/i }).click();
835
+ await page.getByRole('menuitem', { name: 'Log out' }).click();
836
+
837
+ // Should redirect to login
838
+ await expect(page).toHaveURL('/login');
839
+
840
+ // Verify we can no longer access protected pages
841
+ await page.goto('/dashboard');
842
+ await expect(page).toHaveURL(/\/login/);
843
+ });
844
+
845
+ test('logout clears all session data', async ({ page, context }) => {
846
+ await page.goto('/dashboard');
847
+
848
+ await page.getByRole('button', { name: /user menu|account|profile/i }).click();
849
+ await page.getByRole('menuitem', { name: 'Log out' }).click();
850
+
851
+ await expect(page).toHaveURL('/login');
852
+
853
+ // Verify cookies are cleared
854
+ const cookies = await context.cookies();
855
+ const sessionCookies = cookies.filter(
856
+ (c) => c.name === 'session' || c.name === 'sid' || c.name === 'token'
857
+ );
858
+ expect(sessionCookies).toHaveLength(0);
859
+
860
+ // Verify localStorage is cleared
861
+ const token = await page.evaluate(() => localStorage.getItem('authToken'));
862
+ expect(token).toBeNull();
863
+ });
864
+
865
+ test('logout from all devices', async ({ page }) => {
866
+ let logoutAllCalled = false;
867
+
868
+ await page.route('**/api/auth/logout-all', async (route) => {
869
+ logoutAllCalled = true;
870
+ await route.fulfill({
871
+ status: 200,
872
+ contentType: 'application/json',
873
+ body: JSON.stringify({ message: 'Logged out from all devices' }),
874
+ });
875
+ });
876
+
877
+ await page.goto('/settings/security');
878
+
879
+ await page.getByRole('button', { name: 'Log out of all devices' }).click();
880
+
881
+ // Confirm the action in the dialog
882
+ await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();
883
+
884
+ expect(logoutAllCalled).toBe(true);
885
+ await expect(page).toHaveURL(/\/login/);
886
+ });
887
+ ```
888
+
889
+ **JavaScript**
890
+
891
+ ```javascript
892
+ const { test, expect } = require('@playwright/test');
893
+
894
+ test.use({ storageState: '.auth/user.json' });
895
+
896
+ test('logs out and redirects to login page', async ({ page }) => {
897
+ await page.goto('/dashboard');
898
+
899
+ await page.getByRole('button', { name: /user menu|account|profile/i }).click();
900
+ await page.getByRole('menuitem', { name: 'Log out' }).click();
901
+
902
+ await expect(page).toHaveURL('/login');
903
+
904
+ await page.goto('/dashboard');
905
+ await expect(page).toHaveURL(/\/login/);
906
+ });
907
+
908
+ test('logout clears all session data', async ({ page, context }) => {
909
+ await page.goto('/dashboard');
910
+
911
+ await page.getByRole('button', { name: /user menu|account|profile/i }).click();
912
+ await page.getByRole('menuitem', { name: 'Log out' }).click();
913
+
914
+ await expect(page).toHaveURL('/login');
915
+
916
+ const cookies = await context.cookies();
917
+ const sessionCookies = cookies.filter(
918
+ (c) => c.name === 'session' || c.name === 'sid' || c.name === 'token'
919
+ );
920
+ expect(sessionCookies).toHaveLength(0);
921
+ });
922
+ ```
923
+
924
+ ---
925
+
926
+ ## Variations
927
+
928
+ ### Login via API for Speed
929
+
930
+ Skip the UI for login when testing non-auth features. Use `request` context to get tokens faster.
931
+
932
+ **TypeScript**
933
+
934
+ ```typescript
935
+ import { test as setup } from '@playwright/test';
936
+
937
+ setup('authenticate via API', async ({ request }) => {
938
+ const response = await request.post('/api/auth/login', {
939
+ data: {
940
+ email: 'user@example.com',
941
+ password: 'SecurePass123!',
942
+ },
943
+ });
944
+
945
+ expect(response.ok()).toBeTruthy();
946
+
947
+ // Save the auth state
948
+ await request.storageState({ path: '.auth/user.json' });
949
+ });
950
+ ```
951
+
952
+ ### Multi-Factor Authentication
953
+
954
+ ```typescript
955
+ test('logs in with MFA (TOTP)', async ({ page }) => {
956
+ await page.goto('/login');
957
+ await page.getByLabel('Email').fill('mfa-user@example.com');
958
+ await page.getByLabel('Password').fill('SecurePass123!');
959
+ await page.getByRole('button', { name: 'Sign in' }).click();
960
+
961
+ // MFA challenge screen appears
962
+ await expect(page.getByText('Enter verification code')).toBeVisible();
963
+
964
+ // In test environments, use a predictable TOTP code or mock the verification
965
+ await page.route('**/api/auth/verify-mfa', async (route) => {
966
+ await route.fulfill({
967
+ status: 200,
968
+ contentType: 'application/json',
969
+ body: JSON.stringify({ success: true, token: 'jwt-token' }),
970
+ });
971
+ });
972
+
973
+ await page.getByLabel('Verification code').fill('123456');
974
+ await page.getByRole('button', { name: 'Verify' }).click();
975
+
976
+ await expect(page).toHaveURL('/dashboard');
977
+ });
978
+ ```
979
+
980
+ ### Passkeys / WebAuthn (Playwright 1.61+)
981
+
982
+ Playwright 1.61 ships a virtual WebAuthn authenticator scoped to the browser context: `context.credentials`. Tests can register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies without a hardware security key. Call `credentials.install()` before any page in the context starts a WebAuthn ceremony.
983
+
984
+ **Pattern A: let the app register a passkey, then sign in with it**
985
+
986
+ ```typescript
987
+ test('registers a passkey and signs in with it', async ({ browser }) => {
988
+ const context = await browser.newContext();
989
+ await context.credentials.install(); // override navigator.credentials in this context
990
+
991
+ const page = await context.newPage();
992
+
993
+ // Register: the app's "Create a passkey" flow now talks to the virtual authenticator
994
+ await page.goto('/settings/security');
995
+ await page.getByRole('button', { name: 'Create a passkey' }).click();
996
+ await expect(page.getByText('Passkey added')).toBeVisible();
997
+
998
+ // Sign out, then sign back in with the passkey
999
+ await page.goto('/logout');
1000
+ await page.goto('/login');
1001
+ await page.getByRole('button', { name: 'Sign in with a passkey' }).click();
1002
+ // navigator.credentials.get() is answered with the registered passkey
1003
+ await expect(page).toHaveURL('/dashboard');
1004
+
1005
+ await context.close();
1006
+ });
1007
+ ```
1008
+
1009
+ **Pattern B: capture once, seed everywhere (the storageState pattern for passkeys)**
1010
+
1011
+ ```typescript
1012
+ // setup project: register a passkey once and save it (it includes the private key)
1013
+ const [credential] = await context.credentials.get({ rpId: 'example.com' });
1014
+ fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));
1015
+
1016
+ // later tests: seed the captured passkey so the user is already enrolled
1017
+ const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
1018
+ const context = await browser.newContext();
1019
+ await context.credentials.create(credential.rpId, credential);
1020
+ await context.credentials.install();
1021
+ // navigator.credentials.get() now resolves the seeded passkey — no registration UI needed
1022
+ ```
1023
+
1024
+ You can also seed a credential your backend already provisioned for a test user by passing `id`, `userHandle`, `privateKey` (base64url PKCS#8), and `publicKey` (base64url SPKI) to `credentials.create(rpId, …)`, and remove one with `credentials.delete(id)`.
1025
+
1026
+ **Tips:**
1027
+ - Treat saved passkey JSON like a saved `storageState` — it contains a private key, so keep it out of version control (`.gitignore` the `.auth/` directory).
1028
+ - Combine with a setup project (see Recipe 1) so passkey enrollment happens once per run, not per test.
1029
+ - Test the fallback path too: what happens when the user cancels the ceremony or has no passkey.
1030
+
1031
+ ### Testing Auth with Different Browser Contexts
1032
+
1033
+ ```typescript
1034
+ test('two users interact simultaneously', async ({ browser }) => {
1035
+ const adminContext = await browser.newContext({
1036
+ storageState: '.auth/admin.json',
1037
+ });
1038
+ const userContext = await browser.newContext({
1039
+ storageState: '.auth/viewer.json',
1040
+ });
1041
+
1042
+ const adminPage = await adminContext.newPage();
1043
+ const userPage = await userContext.newPage();
1044
+
1045
+ // Admin bans a user
1046
+ await adminPage.goto('/admin/users');
1047
+ await adminPage.getByRole('row', { name: 'viewer@example.com' })
1048
+ .getByRole('button', { name: 'Ban' }).click();
1049
+ await adminPage.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();
1050
+
1051
+ // User tries to navigate and should be blocked
1052
+ await userPage.goto('/dashboard');
1053
+ await expect(userPage.getByText(/account.*banned|access.*revoked/i)).toBeVisible();
1054
+
1055
+ await adminContext.close();
1056
+ await userContext.close();
1057
+ });
1058
+ ```
1059
+
1060
+ ---
1061
+
1062
+ ## Tips
1063
+
1064
+ 1. **Use `storageState` for reusable auth**. Run login once in a setup project, save the state to a JSON file, and reuse it across all tests. This dramatically speeds up your test suite since you only log in once per role.
1065
+
1066
+ 2. **Never hard-code credentials in test files**. Use environment variables or a `.env` file loaded via `dotenv`. In `playwright.config.ts`, set `process.env` values or use the `env` option in `use`.
1067
+
1068
+ 3. **Prefer API-based login for non-auth tests**. Only test the login UI in your auth-specific tests. For everything else, authenticate via the API or by restoring `storageState`. This makes tests faster and less brittle.
1069
+
1070
+ 4. **Test the negative paths too**. Invalid credentials, expired tokens, locked accounts, rate limiting. These are the scenarios users actually encounter in production and are frequently undertested.
1071
+
1072
+ 5. **Set a shorter session timeout in your test environment**. If your app has a 30-minute session timeout, configure it to 30 seconds in the test environment so you can test timeout behavior without slow tests.
1073
+
1074
+ ---
1075
+
1076
+ ## Related
1077
+
1078
+ - [Playwright Authentication Docs](https://playwright.dev/docs/auth)
1079
+ - `patterns/page-objects.md` -- Encapsulate login flows in page objects
1080
+ - `foundations/assertions.md` -- Assertion patterns for auth states
1081
+ - `recipes/crud-testing.md` -- Test CRUD operations once authenticated