@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,608 @@
1
+ # Performance Testing
2
+
3
+ > **When to use**: Measuring and enforcing Web Vitals, resource loading timing, bundle sizes, and runtime performance. Use Playwright to catch performance regressions in CI before users notice them.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Measure Largest Contentful Paint (LCP)
10
+ const lcp = await page.evaluate(() => {
11
+ return new Promise<number>((resolve) => {
12
+ new PerformanceObserver((list) => {
13
+ const entries = list.getEntries();
14
+ resolve(entries[entries.length - 1].startTime);
15
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
16
+ });
17
+ });
18
+ expect(lcp).toBeLessThan(2500); // Good LCP threshold
19
+
20
+ // Throttle network to 3G
21
+ const client = await page.context().newCDPSession(page);
22
+ await client.send('Network.emulateNetworkConditions', {
23
+ offline: false, downloadThroughput: 1.6 * 1024 * 1024 / 8,
24
+ uploadThroughput: 750 * 1024 / 8, latency: 150,
25
+ });
26
+ ```
27
+
28
+ ## Patterns
29
+
30
+ ### Web Vitals Measurement (LCP, CLS, FID/INP)
31
+
32
+ **Use when**: Enforcing Core Web Vitals thresholds as part of your test suite.
33
+ **Avoid when**: You only need aggregate field data -- use Chrome UX Report or RUM tools instead.
34
+
35
+ **TypeScript**
36
+ ```typescript
37
+ import { test, expect } from '@playwright/test';
38
+
39
+ test('Core Web Vitals meet thresholds on homepage', async ({ page }) => {
40
+ // Inject Web Vitals observer before navigation
41
+ await page.addInitScript(() => {
42
+ (window as any).__webVitals = { lcp: 0, cls: 0, fid: 0 };
43
+
44
+ new PerformanceObserver((list) => {
45
+ const entries = list.getEntries();
46
+ (window as any).__webVitals.lcp = entries[entries.length - 1].startTime;
47
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
48
+
49
+ new PerformanceObserver((list) => {
50
+ let clsValue = 0;
51
+ for (const entry of list.getEntries()) {
52
+ if (!(entry as any).hadRecentInput) {
53
+ clsValue += (entry as any).value;
54
+ }
55
+ }
56
+ (window as any).__webVitals.cls = clsValue;
57
+ }).observe({ type: 'layout-shift', buffered: true });
58
+
59
+ new PerformanceObserver((list) => {
60
+ const entries = list.getEntries();
61
+ (window as any).__webVitals.fid = entries[0]?.processingStart - entries[0]?.startTime;
62
+ }).observe({ type: 'first-input', buffered: true });
63
+ });
64
+
65
+ await page.goto('/');
66
+
67
+ // Trigger a user interaction to measure FID
68
+ await page.getByRole('button', { name: 'Get started' }).click();
69
+
70
+ // Wait for LCP to settle
71
+ await page.waitForTimeout(1000); // Acceptable here: waiting for metric to finalize
72
+
73
+ const vitals = await page.evaluate(() => (window as any).__webVitals);
74
+
75
+ expect(vitals.lcp).toBeLessThan(2500); // Good: <2.5s
76
+ expect(vitals.cls).toBeLessThan(0.1); // Good: <0.1
77
+ // FID may be 0 in automated tests due to no real user delay
78
+ if (vitals.fid > 0) {
79
+ expect(vitals.fid).toBeLessThan(100); // Good: <100ms
80
+ }
81
+ });
82
+ ```
83
+
84
+ **JavaScript**
85
+ ```javascript
86
+ const { test, expect } = require('@playwright/test');
87
+
88
+ test('LCP meets threshold on homepage', async ({ page }) => {
89
+ await page.addInitScript(() => {
90
+ window.__lcp = 0;
91
+ new PerformanceObserver((list) => {
92
+ const entries = list.getEntries();
93
+ window.__lcp = entries[entries.length - 1].startTime;
94
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
95
+ });
96
+
97
+ await page.goto('/');
98
+ await page.waitForTimeout(1000);
99
+
100
+ const lcp = await page.evaluate(() => window.__lcp);
101
+ expect(lcp).toBeLessThan(2500);
102
+ });
103
+ ```
104
+
105
+ ### Performance API Access
106
+
107
+ **Use when**: Measuring navigation timing, resource loading, or custom performance marks.
108
+ **Avoid when**: Web Vitals alone cover your needs.
109
+
110
+ **TypeScript**
111
+ ```typescript
112
+ import { test, expect } from '@playwright/test';
113
+
114
+ test('page load timing is within budget', async ({ page }) => {
115
+ await page.goto('/dashboard');
116
+
117
+ const timing = await page.evaluate(() => {
118
+ const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
119
+ return {
120
+ dns: nav.domainLookupEnd - nav.domainLookupStart,
121
+ tcp: nav.connectEnd - nav.connectStart,
122
+ ttfb: nav.responseStart - nav.requestStart,
123
+ domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
124
+ loadComplete: nav.loadEventEnd - nav.startTime,
125
+ domInteractive: nav.domInteractive - nav.startTime,
126
+ };
127
+ });
128
+
129
+ expect(timing.ttfb).toBeLessThan(600); // TTFB under 600ms
130
+ expect(timing.domContentLoaded).toBeLessThan(2000); // DOM ready under 2s
131
+ expect(timing.loadComplete).toBeLessThan(5000); // Full load under 5s
132
+ });
133
+
134
+ test('critical API calls complete within budget', async ({ page }) => {
135
+ await page.goto('/dashboard');
136
+
137
+ const apiTimings = await page.evaluate(() => {
138
+ return performance
139
+ .getEntriesByType('resource')
140
+ .filter((r) => r.name.includes('/api/'))
141
+ .map((r) => ({
142
+ name: r.name.split('/api/')[1],
143
+ duration: r.duration,
144
+ size: (r as PerformanceResourceTiming).transferSize,
145
+ }));
146
+ });
147
+
148
+ for (const api of apiTimings) {
149
+ expect(api.duration, `API ${api.name} too slow`).toBeLessThan(1000);
150
+ }
151
+ });
152
+ ```
153
+
154
+ **JavaScript**
155
+ ```javascript
156
+ const { test, expect } = require('@playwright/test');
157
+
158
+ test('page load timing is within budget', async ({ page }) => {
159
+ await page.goto('/dashboard');
160
+
161
+ const timing = await page.evaluate(() => {
162
+ const nav = performance.getEntriesByType('navigation')[0];
163
+ return {
164
+ ttfb: nav.responseStart - nav.requestStart,
165
+ domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
166
+ loadComplete: nav.loadEventEnd - nav.startTime,
167
+ };
168
+ });
169
+
170
+ expect(timing.ttfb).toBeLessThan(600);
171
+ expect(timing.domContentLoaded).toBeLessThan(2000);
172
+ expect(timing.loadComplete).toBeLessThan(5000);
173
+ });
174
+ ```
175
+
176
+ ### Resource Loading and Bundle Size Monitoring
177
+
178
+ **Use when**: Enforcing bundle size budgets and catching unexpected large resources.
179
+ **Avoid when**: Bundle analysis is handled by webpack-bundle-analyzer or similar build tools.
180
+
181
+ **TypeScript**
182
+ ```typescript
183
+ import { test, expect } from '@playwright/test';
184
+
185
+ test('JavaScript bundle sizes are within budget', async ({ page }) => {
186
+ const resourceSizes: { name: string; size: number }[] = [];
187
+
188
+ page.on('response', async (response) => {
189
+ const url = response.url();
190
+ if (url.endsWith('.js') || url.includes('.js?')) {
191
+ const headers = response.headers();
192
+ const size = parseInt(headers['content-length'] || '0');
193
+ resourceSizes.push({
194
+ name: url.split('/').pop()!.split('?')[0],
195
+ size,
196
+ });
197
+ }
198
+ });
199
+
200
+ await page.goto('/');
201
+ await page.waitForLoadState('networkidle');
202
+
203
+ // No single JS bundle should exceed 250KB compressed
204
+ for (const resource of resourceSizes) {
205
+ expect(
206
+ resource.size,
207
+ `Bundle ${resource.name} is ${(resource.size / 1024).toFixed(1)}KB`
208
+ ).toBeLessThan(250 * 1024);
209
+ }
210
+
211
+ // Total JS payload should not exceed 500KB
212
+ const totalSize = resourceSizes.reduce((sum, r) => sum + r.size, 0);
213
+ expect(totalSize, `Total JS: ${(totalSize / 1024).toFixed(1)}KB`).toBeLessThan(500 * 1024);
214
+ });
215
+
216
+ test('no unexpected large images', async ({ page }) => {
217
+ const largeImages: { url: string; size: number }[] = [];
218
+
219
+ page.on('response', async (response) => {
220
+ const contentType = response.headers()['content-type'] || '';
221
+ if (contentType.startsWith('image/')) {
222
+ const size = parseInt(response.headers()['content-length'] || '0');
223
+ if (size > 200 * 1024) {
224
+ largeImages.push({ url: response.url(), size });
225
+ }
226
+ }
227
+ });
228
+
229
+ await page.goto('/');
230
+ await page.waitForLoadState('networkidle');
231
+
232
+ expect(
233
+ largeImages,
234
+ `Found ${largeImages.length} images over 200KB: ${largeImages.map(i => i.url).join(', ')}`
235
+ ).toHaveLength(0);
236
+ });
237
+ ```
238
+
239
+ **JavaScript**
240
+ ```javascript
241
+ const { test, expect } = require('@playwright/test');
242
+
243
+ test('JavaScript bundle sizes are within budget', async ({ page }) => {
244
+ const resourceSizes = [];
245
+
246
+ page.on('response', async (response) => {
247
+ const url = response.url();
248
+ if (url.endsWith('.js') || url.includes('.js?')) {
249
+ const size = parseInt(response.headers()['content-length'] || '0');
250
+ resourceSizes.push({ name: url.split('/').pop().split('?')[0], size });
251
+ }
252
+ });
253
+
254
+ await page.goto('/');
255
+ await page.waitForLoadState('networkidle');
256
+
257
+ const totalSize = resourceSizes.reduce((sum, r) => sum + r.size, 0);
258
+ expect(totalSize).toBeLessThan(500 * 1024);
259
+ });
260
+ ```
261
+
262
+ ### Slow Network Simulation via CDP
263
+
264
+ **Use when**: Testing your app's behavior and performance under constrained network conditions.
265
+ **Avoid when**: Playwright's built-in `offline` option is sufficient for your test.
266
+
267
+ **TypeScript**
268
+ ```typescript
269
+ import { test, expect, type Page } from '@playwright/test';
270
+
271
+ // Network presets
272
+ const NETWORK_PRESETS = {
273
+ slow3G: {
274
+ offline: false,
275
+ downloadThroughput: (500 * 1024) / 8, // 500 Kbps
276
+ uploadThroughput: (500 * 1024) / 8,
277
+ latency: 400,
278
+ },
279
+ fast3G: {
280
+ offline: false,
281
+ downloadThroughput: (1.6 * 1024 * 1024) / 8, // 1.6 Mbps
282
+ uploadThroughput: (750 * 1024) / 8,
283
+ latency: 150,
284
+ },
285
+ regularLTE: {
286
+ offline: false,
287
+ downloadThroughput: (4 * 1024 * 1024) / 8, // 4 Mbps
288
+ uploadThroughput: (3 * 1024 * 1024) / 8,
289
+ latency: 20,
290
+ },
291
+ } as const;
292
+
293
+ async function throttleNetwork(page: Page, preset: keyof typeof NETWORK_PRESETS) {
294
+ const client = await page.context().newCDPSession(page);
295
+ await client.send('Network.enable');
296
+ await client.send('Network.emulateNetworkConditions', NETWORK_PRESETS[preset]);
297
+ return client;
298
+ }
299
+
300
+ test('app shows loading states on slow network', async ({ page }) => {
301
+ await throttleNetwork(page, 'slow3G');
302
+
303
+ await page.goto('/dashboard');
304
+
305
+ // Loading skeleton should appear while content loads slowly
306
+ await expect(page.getByTestId('loading-skeleton')).toBeVisible();
307
+
308
+ // Content should eventually load
309
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({ timeout: 30000 });
310
+ });
311
+
312
+ test('images lazy-load on slow connection', async ({ page }) => {
313
+ await throttleNetwork(page, 'fast3G');
314
+ await page.goto('/gallery');
315
+
316
+ // Only above-the-fold images should be loaded initially
317
+ const loadedImages = await page.evaluate(() =>
318
+ Array.from(document.querySelectorAll('img'))
319
+ .filter((img) => img.complete && img.naturalWidth > 0).length
320
+ );
321
+
322
+ // Scroll to trigger lazy loading
323
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
324
+ await page.waitForTimeout(2000);
325
+
326
+ const allLoadedImages = await page.evaluate(() =>
327
+ Array.from(document.querySelectorAll('img'))
328
+ .filter((img) => img.complete && img.naturalWidth > 0).length
329
+ );
330
+
331
+ expect(allLoadedImages).toBeGreaterThan(loadedImages);
332
+ });
333
+ ```
334
+
335
+ **JavaScript**
336
+ ```javascript
337
+ const { test, expect } = require('@playwright/test');
338
+
339
+ async function throttleNetwork(page, preset) {
340
+ const presets = {
341
+ slow3G: { offline: false, downloadThroughput: (500 * 1024) / 8, uploadThroughput: (500 * 1024) / 8, latency: 400 },
342
+ fast3G: { offline: false, downloadThroughput: (1.6 * 1024 * 1024) / 8, uploadThroughput: (750 * 1024) / 8, latency: 150 },
343
+ };
344
+ const client = await page.context().newCDPSession(page);
345
+ await client.send('Network.enable');
346
+ await client.send('Network.emulateNetworkConditions', presets[preset]);
347
+ return client;
348
+ }
349
+
350
+ test('app shows loading states on slow network', async ({ page }) => {
351
+ await throttleNetwork(page, 'slow3G');
352
+ await page.goto('/dashboard');
353
+
354
+ await expect(page.getByTestId('loading-skeleton')).toBeVisible();
355
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({ timeout: 30000 });
356
+ });
357
+ ```
358
+
359
+ ### CPU Throttling via CDP
360
+
361
+ **Use when**: Simulating low-powered devices to test animation smoothness, interaction responsiveness, or heavy computation.
362
+ **Avoid when**: Network performance is the bottleneck, not CPU.
363
+
364
+ **TypeScript**
365
+ ```typescript
366
+ import { test, expect } from '@playwright/test';
367
+
368
+ test('animations remain smooth under CPU throttling', async ({ page }) => {
369
+ const client = await page.context().newCDPSession(page);
370
+
371
+ // 4x slowdown simulates a mid-tier mobile device
372
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
373
+
374
+ await page.goto('/animations-demo');
375
+ await page.getByRole('button', { name: 'Start animation' }).click();
376
+
377
+ // Measure frame rate during animation
378
+ const fps = await page.evaluate(() => {
379
+ return new Promise<number>((resolve) => {
380
+ let frames = 0;
381
+ const start = performance.now();
382
+ function count() {
383
+ frames++;
384
+ if (performance.now() - start < 1000) {
385
+ requestAnimationFrame(count);
386
+ } else {
387
+ resolve(frames);
388
+ }
389
+ }
390
+ requestAnimationFrame(count);
391
+ });
392
+ });
393
+
394
+ // Should maintain at least 30fps even on throttled CPU
395
+ expect(fps).toBeGreaterThan(30);
396
+
397
+ // Reset throttling
398
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 1 });
399
+ });
400
+
401
+ test('search input responds quickly under CPU constraint', async ({ page }) => {
402
+ const client = await page.context().newCDPSession(page);
403
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
404
+
405
+ await page.goto('/search');
406
+
407
+ const start = Date.now();
408
+ await page.getByRole('textbox', { name: 'Search' }).fill('test query');
409
+ await expect(page.getByRole('listbox')).toBeVisible();
410
+ const elapsed = Date.now() - start;
411
+
412
+ // Autocomplete should appear within 500ms even under 4x CPU throttle
413
+ expect(elapsed).toBeLessThan(500);
414
+
415
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 1 });
416
+ });
417
+ ```
418
+
419
+ **JavaScript**
420
+ ```javascript
421
+ const { test, expect } = require('@playwright/test');
422
+
423
+ test('animations remain smooth under CPU throttling', async ({ page }) => {
424
+ const client = await page.context().newCDPSession(page);
425
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
426
+
427
+ await page.goto('/animations-demo');
428
+ await page.getByRole('button', { name: 'Start animation' }).click();
429
+
430
+ const fps = await page.evaluate(() => {
431
+ return new Promise((resolve) => {
432
+ let frames = 0;
433
+ const start = performance.now();
434
+ function count() {
435
+ frames++;
436
+ if (performance.now() - start < 1000) {
437
+ requestAnimationFrame(count);
438
+ } else {
439
+ resolve(frames);
440
+ }
441
+ }
442
+ requestAnimationFrame(count);
443
+ });
444
+ });
445
+
446
+ expect(fps).toBeGreaterThan(30);
447
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 1 });
448
+ });
449
+ ```
450
+
451
+ ### Performance Budgets in CI
452
+
453
+ **Use when**: Enforcing hard performance limits that block merges when thresholds are exceeded.
454
+ **Avoid when**: Performance varies too much in CI environment -- use trend-based monitoring instead.
455
+
456
+ **TypeScript**
457
+ ```typescript
458
+ import { test, expect } from '@playwright/test';
459
+
460
+ // Define budgets in a shared config
461
+ const PERFORMANCE_BUDGETS = {
462
+ homepage: {
463
+ lcp: 2500,
464
+ cls: 0.1,
465
+ ttfb: 600,
466
+ totalJsSize: 500 * 1024,
467
+ totalImageSize: 1000 * 1024,
468
+ domContentLoaded: 2000,
469
+ },
470
+ dashboard: {
471
+ lcp: 3000,
472
+ cls: 0.1,
473
+ ttfb: 800,
474
+ totalJsSize: 750 * 1024,
475
+ totalImageSize: 500 * 1024,
476
+ domContentLoaded: 3000,
477
+ },
478
+ } as const;
479
+
480
+ test.describe('performance budgets', () => {
481
+ test('homepage meets performance budget', async ({ page }) => {
482
+ const budget = PERFORMANCE_BUDGETS.homepage;
483
+ let totalJsSize = 0;
484
+
485
+ page.on('response', (response) => {
486
+ if (response.url().endsWith('.js') || response.url().includes('.js?')) {
487
+ totalJsSize += parseInt(response.headers()['content-length'] || '0');
488
+ }
489
+ });
490
+
491
+ // Inject LCP observer
492
+ await page.addInitScript(() => {
493
+ (window as any).__lcp = 0;
494
+ new PerformanceObserver((list) => {
495
+ const entries = list.getEntries();
496
+ (window as any).__lcp = entries[entries.length - 1].startTime;
497
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
498
+ });
499
+
500
+ await page.goto('/');
501
+ await page.waitForLoadState('networkidle');
502
+
503
+ const metrics = await page.evaluate(() => {
504
+ const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
505
+ return {
506
+ lcp: (window as any).__lcp,
507
+ ttfb: nav.responseStart - nav.requestStart,
508
+ domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
509
+ };
510
+ });
511
+
512
+ expect(metrics.lcp, 'LCP budget exceeded').toBeLessThan(budget.lcp);
513
+ expect(metrics.ttfb, 'TTFB budget exceeded').toBeLessThan(budget.ttfb);
514
+ expect(metrics.domContentLoaded, 'DOMContentLoaded budget exceeded').toBeLessThan(budget.domContentLoaded);
515
+ expect(totalJsSize, 'JS bundle budget exceeded').toBeLessThan(budget.totalJsSize);
516
+ });
517
+ });
518
+ ```
519
+
520
+ **JavaScript**
521
+ ```javascript
522
+ const { test, expect } = require('@playwright/test');
523
+
524
+ const PERFORMANCE_BUDGETS = {
525
+ homepage: { lcp: 2500, ttfb: 600, totalJsSize: 500 * 1024, domContentLoaded: 2000 },
526
+ };
527
+
528
+ test('homepage meets performance budget', async ({ page }) => {
529
+ const budget = PERFORMANCE_BUDGETS.homepage;
530
+ let totalJsSize = 0;
531
+
532
+ page.on('response', (response) => {
533
+ if (response.url().endsWith('.js')) {
534
+ totalJsSize += parseInt(response.headers()['content-length'] || '0');
535
+ }
536
+ });
537
+
538
+ await page.addInitScript(() => {
539
+ window.__lcp = 0;
540
+ new PerformanceObserver((list) => {
541
+ const entries = list.getEntries();
542
+ window.__lcp = entries[entries.length - 1].startTime;
543
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
544
+ });
545
+
546
+ await page.goto('/');
547
+ await page.waitForLoadState('networkidle');
548
+
549
+ const metrics = await page.evaluate(() => {
550
+ const nav = performance.getEntriesByType('navigation')[0];
551
+ return {
552
+ lcp: window.__lcp,
553
+ ttfb: nav.responseStart - nav.requestStart,
554
+ domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
555
+ };
556
+ });
557
+
558
+ expect(metrics.lcp).toBeLessThan(budget.lcp);
559
+ expect(metrics.ttfb).toBeLessThan(budget.ttfb);
560
+ expect(totalJsSize).toBeLessThan(budget.totalJsSize);
561
+ });
562
+ ```
563
+
564
+ ## Decision Guide
565
+
566
+ | What to Measure | Technique | When to Use |
567
+ |---|---|---|
568
+ | LCP, CLS, FID/INP | `PerformanceObserver` via `addInitScript` | Core Web Vitals regression testing |
569
+ | TTFB, DOM load times | `performance.getEntriesByType('navigation')` | Server response and page load budgets |
570
+ | API call durations | `performance.getEntriesByType('resource')` | Backend performance regression |
571
+ | JS/CSS bundle sizes | `page.on('response')` + `content-length` header | Bundle size budgets in CI |
572
+ | Slow network behavior | CDP `Network.emulateNetworkConditions` | Testing loading states, lazy loading, offline |
573
+ | Low-end device behavior | CDP `Emulation.setCPUThrottlingRate` | Animation smoothness, interaction latency |
574
+ | Full Lighthouse audit | `@playwright/test` + Lighthouse CLI via CDP port | Comprehensive performance scoring |
575
+ | Runtime performance | `page.evaluate` + `requestAnimationFrame` FPS count | Animation and rendering performance |
576
+
577
+ ## Anti-Patterns
578
+
579
+ | Don't Do This | Problem | Do This Instead |
580
+ |---|---|---|
581
+ | Setting absolute thresholds based on local dev machine | CI machines are slower; thresholds flap | Calibrate budgets on CI hardware or use relative comparisons |
582
+ | Using `networkidle` as a performance measurement point | `networkidle` includes analytics, ads, non-critical resources | Measure specific metrics (LCP, TTFB) directly via Performance API |
583
+ | Running performance tests with `--headed` in CI | Headed mode adds GPU overhead and inconsistency | Use headless mode for consistent measurement |
584
+ | Measuring FID in automated tests | No real user input delay exists in automation | Measure INP or use Lighthouse for FID estimates |
585
+ | Running perf tests in parallel with other CI jobs | CPU contention skews results | Run performance tests in isolation or on dedicated CI runners |
586
+ | Ignoring `content-length` being `0` | Compressed responses may not report size | Use `response.body().length` for actual transfer size |
587
+ | Only testing happy-path performance | Slow error paths degrade user experience | Test performance of error states, empty states, and large datasets |
588
+ | Hard-failing CI on minor regressions | Causes merge friction for non-performance changes | Use warning thresholds with mandatory review, fail only on large regressions |
589
+
590
+ ## Troubleshooting
591
+
592
+ | Symptom | Likely Cause | Fix |
593
+ |---|---|---|
594
+ | LCP is 0 or unrealistically low | Observer did not fire; page has no qualifying LCP element | Verify the page has images or large text blocks; add `buffered: true` to observer |
595
+ | CLS is always 0 | Layout shifts occur before observer is registered | Use `addInitScript` to inject observer before page load |
596
+ | CDP session errors with Firefox/WebKit | CDP is Chromium-only | Guard CDP code: `test.skip(browserName !== 'chromium')` |
597
+ | Performance numbers vary wildly between runs | CI machine load fluctuates | Run performance tests multiple times and take the median; use dedicated runners |
598
+ | `content-length` header is missing | Server uses chunked transfer encoding | Use `response.body()` then check `Buffer.byteLength()` |
599
+ | Network throttling has no effect | CDP session created on wrong page | Create the CDP session from the page's context, not a separate browser |
600
+ | Bundle size test passes but app feels slow | Measuring compressed size, not parsed size | Also check `performance.getEntriesByType('resource')` for `decodedBodySize` |
601
+
602
+ ## Related
603
+
604
+ - [core/configuration.md](configuration.md) -- timeout and retry settings for performance-sensitive tests
605
+ - [core/network-mocking.md](network-mocking.md) -- mocking slow APIs for performance boundary testing
606
+ - [core/browser-apis.md](browser-apis.md) -- using browser APIs for measurement
607
+ - [ci/ci-github-actions.md](../ci/ci-github-actions.md) -- CI configuration for performance budgets
608
+ - [core/clock-and-time-mocking.md](clock-and-time-mocking.md) -- time-related performance testing