@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.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- 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
|