@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,569 @@
1
+ # Test Architecture: E2E vs Component vs API
2
+
3
+ > **When to use**: When deciding what kind of test to write for a feature. Before you write any test, ask: "What is the cheapest test that gives me confidence this works?"
4
+
5
+ ## Quick Answer
6
+
7
+ Most teams write too many E2E tests. Default to the **testing trophy** approach:
8
+
9
+ 1. **API tests** for business logic, data validation, and permissions (fastest, most stable)
10
+ 2. **Component tests** for isolated UI behavior, form validation, and interactive widgets (fast, focused)
11
+ 3. **E2E tests only for critical user paths** -- login, checkout, onboarding (slow, highest confidence)
12
+
13
+ If you only have time for one layer, start with API tests. They cover the most ground with the least maintenance cost.
14
+
15
+ ## The Testing Trophy
16
+
17
+ The testing trophy (coined by Kent C. Dodds) replaces the traditional testing pyramid for modern web apps:
18
+
19
+ ```
20
+ ╱ ╲
21
+ ╱ E2E ╲ Thin layer -- critical paths only
22
+ ╱─────────╲
23
+ ╱ ╲
24
+ ╱ Integration ╲ Thickest layer -- component + API tests
25
+ ╱ ╲
26
+ ╱───────────────────╲
27
+ ╱ Unit ╲ Utility functions, pure logic
28
+ ╱───────────────────────╲
29
+ Static Analysis TypeScript, ESLint, Prettier
30
+ ```
31
+
32
+ **Where Playwright fits**:
33
+ - **E2E layer**: `@playwright/test` with a browser -- full user flow testing
34
+ - **Integration / Component layer**: `@playwright/experimental-ct-*` -- render components in a real browser without a full app
35
+ - **Integration / API layer**: `@playwright/test` with `request` context -- HTTP testing without a browser
36
+ - **Static layer**: Not Playwright's job. Use TypeScript and ESLint.
37
+
38
+ The trophy shape means integration tests (component + API) should be your **largest** investment. They give the best ratio of confidence to cost.
39
+
40
+ ## Decision Matrix
41
+
42
+ | What You're Testing | Test Type | Why | Playwright Example |
43
+ |---|---|---|---|
44
+ | Login / auth flow | E2E | Cross-page, cookies, redirects, session state | Full browser flow with `storageState` |
45
+ | Form submission | Component | Isolated validation logic, error states, UX | Mount form component, test states |
46
+ | CRUD operations | API | Data integrity matters more than UI for create/update/delete | `request.post()`, `request.put()`, `request.delete()` |
47
+ | Search with results UI | Component + API | API test for query logic; component test for rendering results | Split: API for data, component for display |
48
+ | Cross-page navigation | E2E | Routing, history, deep linking are browser concerns | `page.goto()`, `page.waitForURL()` |
49
+ | Error handling (API errors) | API | Validate status codes, error shapes, edge cases without UI | `expect(response.status()).toBe(422)` |
50
+ | Error handling (UI feedback) | Component | Toast, banner, inline error rendering | Mount component, mock error response |
51
+ | Accessibility | Component | Test ARIA roles, keyboard nav per-component; faster than full E2E | `expect(locator).toHaveAttribute('aria-expanded')` |
52
+ | Responsive layout | Component | Viewport-specific rendering without full app overhead | `mount()` with viewport config |
53
+ | API integration (contract) | API | Validate response shapes, headers, auth independently | `request.get()` with schema validation |
54
+ | Real-time features (WebSocket) | E2E | Requires full browser environment for WebSocket connections | `page.evaluate()` with WebSocket listeners |
55
+ | Payment / checkout flow | E2E | Multi-step, third-party iframes, real-world reliability | Full browser flow, `frameLocator()` |
56
+ | Onboarding / wizard | E2E | Multi-step, state persists across pages | `test.step()` for each wizard stage |
57
+ | Individual widget behavior | Component | Toggle, accordion, date picker, modal -- isolated interactions | Mount component, test open/close/select |
58
+ | Permissions / authorization | API | Role-based access is backend logic; test without UI overhead | Request with different auth tokens |
59
+
60
+ ## When to Use E2E Tests
61
+
62
+ **Best for**:
63
+ - Critical user flows that generate revenue (checkout, signup, subscription)
64
+ - Authentication and authorization flows (login, SSO, MFA, password reset)
65
+ - Multi-page workflows where state carries across navigation (wizards, onboarding)
66
+ - Flows involving third-party iframes (payment widgets, embedded forms)
67
+ - Smoke tests validating the entire stack is wired together
68
+ - Real-time collaboration features requiring multiple browser contexts
69
+
70
+ **Avoid for**:
71
+ - Testing every permutation of form validation (use component tests)
72
+ - CRUD operations where the UI is a thin wrapper (use API tests)
73
+ - Verifying individual component states (use component tests)
74
+ - Testing API response shapes or error codes (use API tests)
75
+ - Responsive layout at every breakpoint (use component tests)
76
+ - Edge cases that only affect the backend (use API tests)
77
+
78
+ **TypeScript**
79
+ ```typescript
80
+ import { test, expect } from '@playwright/test';
81
+
82
+ // E2E: critical checkout flow -- this justifies the cost of a full browser
83
+ test.describe('checkout flow', () => {
84
+ test.beforeEach(async ({ page }) => {
85
+ // Seed data via API to keep E2E tests focused on the flow, not setup
86
+ await page.request.post('/api/test/seed-cart', {
87
+ data: { items: [{ sku: 'SHOE-001', qty: 1 }] },
88
+ });
89
+ await page.goto('/cart');
90
+ });
91
+
92
+ test('completes purchase with valid payment', async ({ page }) => {
93
+ await test.step('review cart', async () => {
94
+ await expect(page.getByRole('heading', { name: 'Your Cart' })).toBeVisible();
95
+ await expect(page.getByText('Running Shoes')).toBeVisible();
96
+ await page.getByRole('button', { name: 'Proceed to checkout' }).click();
97
+ });
98
+
99
+ await test.step('fill shipping details', async () => {
100
+ await page.getByLabel('Full name').fill('Jane Doe');
101
+ await page.getByLabel('Address').fill('123 Main St');
102
+ await page.getByLabel('City').fill('Portland');
103
+ await page.getByRole('combobox', { name: 'State' }).selectOption('OR');
104
+ await page.getByLabel('ZIP code').fill('97201');
105
+ await page.getByRole('button', { name: 'Continue to payment' }).click();
106
+ });
107
+
108
+ await test.step('enter payment', async () => {
109
+ const paymentFrame = page.frameLocator('iframe[title="Payment"]');
110
+ await paymentFrame.getByLabel('Card number').fill('4242424242424242');
111
+ await paymentFrame.getByLabel('Expiration').fill('12/28');
112
+ await paymentFrame.getByLabel('CVC').fill('123');
113
+ await page.getByRole('button', { name: 'Place order' }).click();
114
+ });
115
+
116
+ await test.step('verify confirmation', async () => {
117
+ await page.waitForURL('**/order/confirmation/**');
118
+ await expect(page.getByRole('heading', { name: 'Order Confirmed' })).toBeVisible();
119
+ await expect(page.getByText(/Order #\d+/)).toBeVisible();
120
+ });
121
+ });
122
+ });
123
+ ```
124
+
125
+ **JavaScript**
126
+ ```javascript
127
+ const { test, expect } = require('@playwright/test');
128
+
129
+ test.describe('checkout flow', () => {
130
+ test.beforeEach(async ({ page }) => {
131
+ await page.request.post('/api/test/seed-cart', {
132
+ data: { items: [{ sku: 'SHOE-001', qty: 1 }] },
133
+ });
134
+ await page.goto('/cart');
135
+ });
136
+
137
+ test('completes purchase with valid payment', async ({ page }) => {
138
+ await test.step('review cart', async () => {
139
+ await expect(page.getByRole('heading', { name: 'Your Cart' })).toBeVisible();
140
+ await expect(page.getByText('Running Shoes')).toBeVisible();
141
+ await page.getByRole('button', { name: 'Proceed to checkout' }).click();
142
+ });
143
+
144
+ await test.step('fill shipping details', async () => {
145
+ await page.getByLabel('Full name').fill('Jane Doe');
146
+ await page.getByLabel('Address').fill('123 Main St');
147
+ await page.getByLabel('City').fill('Portland');
148
+ await page.getByRole('combobox', { name: 'State' }).selectOption('OR');
149
+ await page.getByLabel('ZIP code').fill('97201');
150
+ await page.getByRole('button', { name: 'Continue to payment' }).click();
151
+ });
152
+
153
+ await test.step('enter payment', async () => {
154
+ const paymentFrame = page.frameLocator('iframe[title="Payment"]');
155
+ await paymentFrame.getByLabel('Card number').fill('4242424242424242');
156
+ await paymentFrame.getByLabel('Expiration').fill('12/28');
157
+ await paymentFrame.getByLabel('CVC').fill('123');
158
+ await page.getByRole('button', { name: 'Place order' }).click();
159
+ });
160
+
161
+ await test.step('verify confirmation', async () => {
162
+ await page.waitForURL('**/order/confirmation/**');
163
+ await expect(page.getByRole('heading', { name: 'Order Confirmed' })).toBeVisible();
164
+ await expect(page.getByText(/Order #\d+/)).toBeVisible();
165
+ });
166
+ });
167
+ });
168
+ ```
169
+
170
+ ## When to Use Component Tests
171
+
172
+ **Best for**:
173
+ - Form validation (required fields, format rules, error messages, field interactions)
174
+ - Interactive widgets (modals, dropdowns, accordions, date pickers, tabs)
175
+ - Conditional rendering (show/hide logic, loading states, empty states, error states)
176
+ - Accessibility per-component (ARIA attributes, keyboard navigation, focus management)
177
+ - Responsive layout at different viewports without full app overhead
178
+ - Visual states (hover, focus, disabled, selected) for design system components
179
+
180
+ **Avoid for**:
181
+ - Testing routing or navigation between pages (use E2E)
182
+ - Flows requiring real cookies, sessions, or server-side state (use E2E)
183
+ - Data persistence or API contract validation (use API tests)
184
+ - Third-party iframe interactions (use E2E)
185
+ - Anything requiring multiple pages or browser contexts (use E2E)
186
+
187
+ **TypeScript** (React example using `@playwright/experimental-ct-react`)
188
+ ```typescript
189
+ import { test, expect } from '@playwright/experimental-ct-react';
190
+ import { LoginForm } from '../src/components/LoginForm';
191
+
192
+ test.describe('LoginForm component', () => {
193
+ test('shows validation errors for empty submission', async ({ mount }) => {
194
+ const component = await mount(<LoginForm onSubmit={() => {}} />);
195
+
196
+ await component.getByRole('button', { name: 'Sign in' }).click();
197
+
198
+ await expect(component.getByText('Email is required')).toBeVisible();
199
+ await expect(component.getByText('Password is required')).toBeVisible();
200
+ });
201
+
202
+ test('shows error for invalid email format', async ({ mount }) => {
203
+ const component = await mount(<LoginForm onSubmit={() => {}} />);
204
+
205
+ await component.getByLabel('Email').fill('not-an-email');
206
+ await component.getByLabel('Password').fill('password123');
207
+ await component.getByRole('button', { name: 'Sign in' }).click();
208
+
209
+ await expect(component.getByText('Enter a valid email address')).toBeVisible();
210
+ });
211
+
212
+ test('calls onSubmit with credentials for valid input', async ({ mount }) => {
213
+ const submitted: Array<{ email: string; password: string }> = [];
214
+ const component = await mount(
215
+ <LoginForm onSubmit={(data) => submitted.push(data)} />
216
+ );
217
+
218
+ await component.getByLabel('Email').fill('jane@example.com');
219
+ await component.getByLabel('Password').fill('s3cure!Pass');
220
+ await component.getByRole('button', { name: 'Sign in' }).click();
221
+
222
+ expect(submitted).toHaveLength(1);
223
+ expect(submitted[0]).toEqual({
224
+ email: 'jane@example.com',
225
+ password: 's3cure!Pass',
226
+ });
227
+ });
228
+
229
+ test('disables submit button while loading', async ({ mount }) => {
230
+ const component = await mount(<LoginForm onSubmit={() => {}} loading={true} />);
231
+
232
+ await expect(component.getByRole('button', { name: 'Signing in...' })).toBeDisabled();
233
+ });
234
+
235
+ test('meets accessibility requirements', async ({ mount }) => {
236
+ const component = await mount(<LoginForm onSubmit={() => {}} />);
237
+
238
+ // Labels are associated with inputs
239
+ await expect(component.getByRole('textbox', { name: 'Email' })).toBeVisible();
240
+
241
+ // Error messages are announced via aria-live
242
+ await component.getByRole('button', { name: 'Sign in' }).click();
243
+ await expect(component.getByRole('alert')).toContainText('Email is required');
244
+ });
245
+ });
246
+ ```
247
+
248
+ **JavaScript** (React example using `@playwright/experimental-ct-react`)
249
+ ```javascript
250
+ const { test, expect } = require('@playwright/experimental-ct-react');
251
+ const { LoginForm } = require('../src/components/LoginForm');
252
+
253
+ test.describe('LoginForm component', () => {
254
+ test('shows validation errors for empty submission', async ({ mount }) => {
255
+ const component = await mount(<LoginForm onSubmit={() => {}} />);
256
+
257
+ await component.getByRole('button', { name: 'Sign in' }).click();
258
+
259
+ await expect(component.getByText('Email is required')).toBeVisible();
260
+ await expect(component.getByText('Password is required')).toBeVisible();
261
+ });
262
+
263
+ test('calls onSubmit with credentials for valid input', async ({ mount }) => {
264
+ const submitted = [];
265
+ const component = await mount(
266
+ <LoginForm onSubmit={(data) => submitted.push(data)} />
267
+ );
268
+
269
+ await component.getByLabel('Email').fill('jane@example.com');
270
+ await component.getByLabel('Password').fill('s3cure!Pass');
271
+ await component.getByRole('button', { name: 'Sign in' }).click();
272
+
273
+ expect(submitted).toHaveLength(1);
274
+ expect(submitted[0]).toEqual({
275
+ email: 'jane@example.com',
276
+ password: 's3cure!Pass',
277
+ });
278
+ });
279
+
280
+ test('disables submit button while loading', async ({ mount }) => {
281
+ const component = await mount(<LoginForm onSubmit={() => {}} loading={true} />);
282
+
283
+ await expect(component.getByRole('button', { name: 'Signing in...' })).toBeDisabled();
284
+ });
285
+ });
286
+ ```
287
+
288
+ ## When to Use API Tests
289
+
290
+ **Best for**:
291
+ - CRUD operations (create, read, update, delete resources)
292
+ - Input validation and error responses (400, 422 with structured error bodies)
293
+ - Permission and authorization checks (role-based access, token scoping)
294
+ - Data integrity and business rules (uniqueness, referential integrity, calculations)
295
+ - API contract verification (response shapes, headers, pagination)
296
+ - Edge cases that are expensive to reproduce through the UI (rate limiting, concurrent updates)
297
+ - Test data setup and teardown for E2E tests (seed via API, verify via API)
298
+
299
+ **Avoid for**:
300
+ - Testing how errors are displayed to the user (use component tests)
301
+ - Testing browser-specific behavior (cookies, redirects, navigation)
302
+ - Verifying visual layout or responsive design (use component tests)
303
+ - Flows requiring JavaScript execution or DOM interaction (use E2E or component)
304
+ - Third-party iframe interactions (use E2E)
305
+
306
+ **TypeScript**
307
+ ```typescript
308
+ import { test, expect } from '@playwright/test';
309
+
310
+ test.describe('Users API', () => {
311
+ let authToken: string;
312
+
313
+ test.beforeAll(async ({ request }) => {
314
+ const response = await request.post('/api/auth/login', {
315
+ data: { email: 'admin@example.com', password: 'admin-pass' },
316
+ });
317
+ const body = await response.json();
318
+ authToken = body.token;
319
+ });
320
+
321
+ test('creates a user with valid data', async ({ request }) => {
322
+ const response = await request.post('/api/users', {
323
+ headers: { Authorization: `Bearer ${authToken}` },
324
+ data: {
325
+ email: 'newuser@example.com',
326
+ name: 'Jane Doe',
327
+ role: 'editor',
328
+ },
329
+ });
330
+
331
+ expect(response.status()).toBe(201);
332
+
333
+ const user = await response.json();
334
+ expect(user).toMatchObject({
335
+ email: 'newuser@example.com',
336
+ name: 'Jane Doe',
337
+ role: 'editor',
338
+ });
339
+ expect(user).toHaveProperty('id');
340
+ expect(user).toHaveProperty('createdAt');
341
+ });
342
+
343
+ test('rejects duplicate email with 409', async ({ request }) => {
344
+ const response = await request.post('/api/users', {
345
+ headers: { Authorization: `Bearer ${authToken}` },
346
+ data: {
347
+ email: 'admin@example.com', // already exists
348
+ name: 'Duplicate',
349
+ role: 'viewer',
350
+ },
351
+ });
352
+
353
+ expect(response.status()).toBe(409);
354
+
355
+ const error = await response.json();
356
+ expect(error.message).toContain('already exists');
357
+ });
358
+
359
+ test('returns 422 for invalid email format', async ({ request }) => {
360
+ const response = await request.post('/api/users', {
361
+ headers: { Authorization: `Bearer ${authToken}` },
362
+ data: {
363
+ email: 'not-valid',
364
+ name: 'Bad Email',
365
+ role: 'viewer',
366
+ },
367
+ });
368
+
369
+ expect(response.status()).toBe(422);
370
+
371
+ const error = await response.json();
372
+ expect(error.errors).toContainEqual(
373
+ expect.objectContaining({ field: 'email' })
374
+ );
375
+ });
376
+
377
+ test('non-admin cannot create users', async ({ request }) => {
378
+ // Login as a non-admin user
379
+ const loginResponse = await request.post('/api/auth/login', {
380
+ data: { email: 'viewer@example.com', password: 'viewer-pass' },
381
+ });
382
+ const { token: viewerToken } = await loginResponse.json();
383
+
384
+ const response = await request.post('/api/users', {
385
+ headers: { Authorization: `Bearer ${viewerToken}` },
386
+ data: {
387
+ email: 'another@example.com',
388
+ name: 'Unauthorized',
389
+ role: 'editor',
390
+ },
391
+ });
392
+
393
+ expect(response.status()).toBe(403);
394
+ });
395
+
396
+ test('lists users with pagination', async ({ request }) => {
397
+ const response = await request.get('/api/users', {
398
+ headers: { Authorization: `Bearer ${authToken}` },
399
+ params: { page: '1', limit: '10' },
400
+ });
401
+
402
+ expect(response.status()).toBe(200);
403
+
404
+ const body = await response.json();
405
+ expect(body.data).toBeInstanceOf(Array);
406
+ expect(body.data.length).toBeLessThanOrEqual(10);
407
+ expect(body).toHaveProperty('total');
408
+ expect(body).toHaveProperty('page', 1);
409
+ });
410
+ });
411
+ ```
412
+
413
+ **JavaScript**
414
+ ```javascript
415
+ const { test, expect } = require('@playwright/test');
416
+
417
+ test.describe('Users API', () => {
418
+ let authToken;
419
+
420
+ test.beforeAll(async ({ request }) => {
421
+ const response = await request.post('/api/auth/login', {
422
+ data: { email: 'admin@example.com', password: 'admin-pass' },
423
+ });
424
+ const body = await response.json();
425
+ authToken = body.token;
426
+ });
427
+
428
+ test('creates a user with valid data', async ({ request }) => {
429
+ const response = await request.post('/api/users', {
430
+ headers: { Authorization: `Bearer ${authToken}` },
431
+ data: {
432
+ email: 'newuser@example.com',
433
+ name: 'Jane Doe',
434
+ role: 'editor',
435
+ },
436
+ });
437
+
438
+ expect(response.status()).toBe(201);
439
+
440
+ const user = await response.json();
441
+ expect(user).toMatchObject({
442
+ email: 'newuser@example.com',
443
+ name: 'Jane Doe',
444
+ role: 'editor',
445
+ });
446
+ expect(user).toHaveProperty('id');
447
+ expect(user).toHaveProperty('createdAt');
448
+ });
449
+
450
+ test('rejects duplicate email with 409', async ({ request }) => {
451
+ const response = await request.post('/api/users', {
452
+ headers: { Authorization: `Bearer ${authToken}` },
453
+ data: {
454
+ email: 'admin@example.com',
455
+ name: 'Duplicate',
456
+ role: 'viewer',
457
+ },
458
+ });
459
+
460
+ expect(response.status()).toBe(409);
461
+
462
+ const error = await response.json();
463
+ expect(error.message).toContain('already exists');
464
+ });
465
+
466
+ test('non-admin cannot create users', async ({ request }) => {
467
+ const loginResponse = await request.post('/api/auth/login', {
468
+ data: { email: 'viewer@example.com', password: 'viewer-pass' },
469
+ });
470
+ const { token: viewerToken } = await loginResponse.json();
471
+
472
+ const response = await request.post('/api/users', {
473
+ headers: { Authorization: `Bearer ${viewerToken}` },
474
+ data: {
475
+ email: 'another@example.com',
476
+ name: 'Unauthorized',
477
+ role: 'editor',
478
+ },
479
+ });
480
+
481
+ expect(response.status()).toBe(403);
482
+ });
483
+ });
484
+ ```
485
+
486
+ ## Combining Test Types
487
+
488
+ The most effective test suites layer all three types. Here is how they work together for a "user management" feature:
489
+
490
+ ### Layer 1: API Tests (60% of test count)
491
+
492
+ Cover every permutation of the backend logic. These are cheap to run and maintain.
493
+
494
+ ```
495
+ tests/api/users.spec.ts
496
+ - creates user with valid data (201)
497
+ - rejects duplicate email (409)
498
+ - rejects invalid email format (422)
499
+ - rejects missing required fields (422)
500
+ - non-admin cannot create users (403)
501
+ - unauthenticated request returns 401
502
+ - lists users with pagination
503
+ - filters users by role
504
+ - updates user profile
505
+ - soft-deletes a user
506
+ - prevents deleting last admin
507
+ ```
508
+
509
+ ### Layer 2: Component Tests (30% of test count)
510
+
511
+ Cover every visual state and interaction of the UI components.
512
+
513
+ ```
514
+ tests/components/UserForm.spec.tsx
515
+ - shows validation errors on empty submit
516
+ - shows inline error for invalid email
517
+ - disables submit while loading
518
+ - calls onSubmit with form data
519
+ - resets form after successful submit
520
+
521
+ tests/components/UserTable.spec.tsx
522
+ - renders user rows from props
523
+ - shows empty state when no users
524
+ - handles delete confirmation modal
525
+ - sorts by column header click
526
+ - shows role badges with correct colors
527
+ ```
528
+
529
+ ### Layer 3: E2E Tests (10% of test count)
530
+
531
+ Cover only the critical path that proves the full stack works together.
532
+
533
+ ```
534
+ tests/e2e/user-management.spec.ts
535
+ - admin creates a user and sees them in the list
536
+ - admin edits a user's role
537
+ - viewer cannot access user management page
538
+ ```
539
+
540
+ ### The math
541
+
542
+ For this single feature, you might have:
543
+ - **11 API tests** -- run in ~2 seconds total, no browser needed
544
+ - **10 component tests** -- run in ~5 seconds total, real browser but no server
545
+ - **3 E2E tests** -- run in ~15 seconds total, full stack
546
+
547
+ Total: 24 tests, ~22 seconds. The 11 API tests catch most regressions. The 10 component tests catch UI bugs. The 3 E2E tests prove the wiring works. If the E2E tests fail but API and component tests pass, you know the problem is in the integration layer (routing, state management, API client) -- not in the business logic or UI components.
548
+
549
+ ## Anti-Patterns
550
+
551
+ | Anti-Pattern | Problem | Better Approach |
552
+ |---|---|---|
553
+ | E2E test for every form validation rule | 30-second browser test for something an API test covers in 200ms | API test for validation logic, one component test for error display |
554
+ | No API tests -- all E2E | Slow suite, flaky from UI timing, hard to diagnose failures | API tests for data/logic, E2E for critical paths only |
555
+ | Component tests that mock everything | Tests pass but app is broken because mocks drift from reality | Mock only external boundaries; use API tests to verify real contracts |
556
+ | Duplicating assertions across layers | Same check in API, component, AND E2E test -- triple maintenance cost | Each layer tests what it is uniquely positioned to verify |
557
+ | E2E test that creates its own test data via the UI | 2-minute test where 90 seconds is setup; breaks when unrelated UI changes | Seed data via API calls in `beforeEach`, then test the actual flow |
558
+ | Testing third-party behavior | Testing that Stripe validates card numbers (that is Stripe's job) | Mock Stripe in component/E2E tests; trust their API contract |
559
+ | Skipping the API layer entirely | UI tests catch the bug but you can't tell if it is frontend or backend | API tests isolate backend bugs; component tests isolate frontend bugs |
560
+ | One giant E2E test for the whole feature | 5-minute test that fails somewhere in the middle with no clear cause | Break into focused E2E tests per critical path; use `test.step()` for clarity |
561
+
562
+ ## Related
563
+
564
+ - [core/test-organization.md](test-organization.md) -- file structure and naming conventions for each test type
565
+ - [core/api-testing.md](api-testing.md) -- deep-dive on Playwright's `request` API for HTTP testing
566
+ - [core/component-testing.md](component-testing.md) -- setting up and writing component tests with Playwright CT
567
+ - [core/authentication.md](authentication.md) -- auth flow testing patterns (E2E + `storageState` reuse)
568
+ - [core/when-to-mock.md](when-to-mock.md) -- when to mock network requests vs hit real services
569
+ - [pom/pom-vs-fixtures-vs-helpers.md](../pom/pom-vs-fixtures-vs-helpers.md) -- organizing shared test logic across layers