@arcadialdev/arcality 2.2.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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Performance Testing & Web Vitals
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Core Web Vitals](#core-web-vitals)
|
|
6
|
+
2. [Performance Metrics](#performance-metrics)
|
|
7
|
+
3. [Performance Budgets](#performance-budgets)
|
|
8
|
+
4. [Lighthouse Integration](#lighthouse-integration)
|
|
9
|
+
5. [Performance Fixtures](#performance-fixtures)
|
|
10
|
+
6. [CI Performance Monitoring](#ci-performance-monitoring)
|
|
11
|
+
|
|
12
|
+
## Core Web Vitals
|
|
13
|
+
|
|
14
|
+
### Measure LCP, FID, CLS
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
test("core web vitals within thresholds", async ({ page }) => {
|
|
18
|
+
// Inject web-vitals library
|
|
19
|
+
await page.addInitScript(() => {
|
|
20
|
+
(window as any).__webVitals = {};
|
|
21
|
+
|
|
22
|
+
// Simplified web vitals collection
|
|
23
|
+
new PerformanceObserver((list) => {
|
|
24
|
+
for (const entry of list.getEntries()) {
|
|
25
|
+
if (entry.entryType === "largest-contentful-paint") {
|
|
26
|
+
(window as any).__webVitals.lcp = entry.startTime;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}).observe({ type: "largest-contentful-paint", buffered: true });
|
|
30
|
+
|
|
31
|
+
new PerformanceObserver((list) => {
|
|
32
|
+
let cls = 0;
|
|
33
|
+
for (const entry of list.getEntries() as any[]) {
|
|
34
|
+
if (!entry.hadRecentInput) {
|
|
35
|
+
cls += entry.value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
(window as any).__webVitals.cls = cls;
|
|
39
|
+
}).observe({ type: "layout-shift", buffered: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await page.goto("/");
|
|
43
|
+
|
|
44
|
+
// Wait for page to stabilize
|
|
45
|
+
await page.waitForLoadState("networkidle");
|
|
46
|
+
|
|
47
|
+
// Get metrics
|
|
48
|
+
const vitals = await page.evaluate(() => (window as any).__webVitals);
|
|
49
|
+
|
|
50
|
+
// Assert thresholds (Google's "good" thresholds)
|
|
51
|
+
expect(vitals.lcp).toBeLessThan(2500); // LCP < 2.5s
|
|
52
|
+
expect(vitals.cls).toBeLessThan(0.1); // CLS < 0.1
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Using web-vitals Library
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
test("web vitals with library", async ({ page }) => {
|
|
60
|
+
await page.addInitScript(() => {
|
|
61
|
+
(window as any).__vitals = {};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Inject web-vitals after navigation
|
|
65
|
+
await page.goto("/");
|
|
66
|
+
|
|
67
|
+
await page.addScriptTag({
|
|
68
|
+
url: "https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await page.evaluate(() => {
|
|
72
|
+
const { onLCP, onFID, onCLS, onFCP, onTTFB } = (window as any).webVitals;
|
|
73
|
+
|
|
74
|
+
onLCP((metric: any) => ((window as any).__vitals.lcp = metric.value));
|
|
75
|
+
onFID((metric: any) => ((window as any).__vitals.fid = metric.value));
|
|
76
|
+
onCLS((metric: any) => ((window as any).__vitals.cls = metric.value));
|
|
77
|
+
onFCP((metric: any) => ((window as any).__vitals.fcp = metric.value));
|
|
78
|
+
onTTFB((metric: any) => ((window as any).__vitals.ttfb = metric.value));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Trigger FID by clicking
|
|
82
|
+
await page.getByRole("button").first().click();
|
|
83
|
+
|
|
84
|
+
// Wait and collect
|
|
85
|
+
await page.waitForTimeout(1000);
|
|
86
|
+
|
|
87
|
+
const vitals = await page.evaluate(() => (window as any).__vitals);
|
|
88
|
+
|
|
89
|
+
console.log("Web Vitals:", vitals);
|
|
90
|
+
|
|
91
|
+
// Assertions
|
|
92
|
+
if (vitals.lcp) expect(vitals.lcp).toBeLessThan(2500);
|
|
93
|
+
if (vitals.fid) expect(vitals.fid).toBeLessThan(100);
|
|
94
|
+
if (vitals.cls) expect(vitals.cls).toBeLessThan(0.1);
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Performance Metrics
|
|
99
|
+
|
|
100
|
+
### Navigation Timing
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
test("page load performance", async ({ page }) => {
|
|
104
|
+
await page.goto("/");
|
|
105
|
+
|
|
106
|
+
const timing = await page.evaluate(() => {
|
|
107
|
+
const nav = performance.getEntriesByType(
|
|
108
|
+
"navigation",
|
|
109
|
+
)[0] as PerformanceNavigationTiming;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
// Time to First Byte
|
|
113
|
+
ttfb: nav.responseStart - nav.requestStart,
|
|
114
|
+
// DOM Content Loaded
|
|
115
|
+
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
|
|
116
|
+
// Full page load
|
|
117
|
+
loadComplete: nav.loadEventEnd - nav.startTime,
|
|
118
|
+
// DNS lookup
|
|
119
|
+
dns: nav.domainLookupEnd - nav.domainLookupStart,
|
|
120
|
+
// Connection time
|
|
121
|
+
connection: nav.connectEnd - nav.connectStart,
|
|
122
|
+
// Download time
|
|
123
|
+
download: nav.responseEnd - nav.responseStart,
|
|
124
|
+
// DOM processing
|
|
125
|
+
domProcessing: nav.domComplete - nav.domInteractive,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
console.log("Performance timing:", timing);
|
|
130
|
+
|
|
131
|
+
// Assertions
|
|
132
|
+
expect(timing.ttfb).toBeLessThan(600); // TTFB < 600ms
|
|
133
|
+
expect(timing.domContentLoaded).toBeLessThan(2000); // DCL < 2s
|
|
134
|
+
expect(timing.loadComplete).toBeLessThan(4000); // Load < 4s
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Resource Timing
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
test("resource loading performance", async ({ page }) => {
|
|
142
|
+
await page.goto("/");
|
|
143
|
+
|
|
144
|
+
const resources = await page.evaluate(() => {
|
|
145
|
+
return performance.getEntriesByType("resource").map((entry) => ({
|
|
146
|
+
name: entry.name.split("/").pop(),
|
|
147
|
+
type: (entry as PerformanceResourceTiming).initiatorType,
|
|
148
|
+
duration: entry.duration,
|
|
149
|
+
size: (entry as PerformanceResourceTiming).transferSize,
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Find slow resources
|
|
154
|
+
const slowResources = resources.filter((r) => r.duration > 1000);
|
|
155
|
+
|
|
156
|
+
if (slowResources.length > 0) {
|
|
157
|
+
console.warn("Slow resources:", slowResources);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find large resources
|
|
161
|
+
const largeResources = resources.filter((r) => r.size > 500000); // > 500KB
|
|
162
|
+
|
|
163
|
+
expect(largeResources.length).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Memory Usage
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
test("memory usage is reasonable", async ({ page }) => {
|
|
171
|
+
await page.goto("/dashboard");
|
|
172
|
+
|
|
173
|
+
// Check memory (Chrome only)
|
|
174
|
+
const memory = await page.evaluate(() => {
|
|
175
|
+
if ((performance as any).memory) {
|
|
176
|
+
return {
|
|
177
|
+
usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
|
|
178
|
+
totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (memory) {
|
|
185
|
+
const usedMB = memory.usedJSHeapSize / 1024 / 1024;
|
|
186
|
+
console.log(`Memory usage: ${usedMB.toFixed(2)} MB`);
|
|
187
|
+
|
|
188
|
+
// Assert reasonable memory usage
|
|
189
|
+
expect(usedMB).toBeLessThan(100); // < 100MB
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Performance Budgets
|
|
195
|
+
|
|
196
|
+
### Define Budgets
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// performance-budgets.ts
|
|
200
|
+
export const budgets = {
|
|
201
|
+
homepage: {
|
|
202
|
+
lcp: 2500,
|
|
203
|
+
cls: 0.1,
|
|
204
|
+
fcp: 1800,
|
|
205
|
+
ttfb: 600,
|
|
206
|
+
totalSize: 1500000, // 1.5MB
|
|
207
|
+
jsSize: 500000, // 500KB
|
|
208
|
+
imageCount: 20,
|
|
209
|
+
},
|
|
210
|
+
dashboard: {
|
|
211
|
+
lcp: 3000,
|
|
212
|
+
cls: 0.1,
|
|
213
|
+
fcp: 2000,
|
|
214
|
+
ttfb: 800,
|
|
215
|
+
totalSize: 2000000,
|
|
216
|
+
jsSize: 800000,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Test Against Budgets
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { budgets } from "./performance-budgets";
|
|
225
|
+
|
|
226
|
+
test("homepage meets performance budget", async ({ page }) => {
|
|
227
|
+
const budget = budgets.homepage;
|
|
228
|
+
|
|
229
|
+
await page.goto("/");
|
|
230
|
+
await page.waitForLoadState("networkidle");
|
|
231
|
+
|
|
232
|
+
// Measure LCP
|
|
233
|
+
const lcp = await page.evaluate(() => {
|
|
234
|
+
return new Promise<number>((resolve) => {
|
|
235
|
+
new PerformanceObserver((list) => {
|
|
236
|
+
const entries = list.getEntries();
|
|
237
|
+
resolve(entries[entries.length - 1].startTime);
|
|
238
|
+
}).observe({ type: "largest-contentful-paint", buffered: true });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Measure resources
|
|
243
|
+
const resources = await page.evaluate(() => {
|
|
244
|
+
const entries = performance.getEntriesByType(
|
|
245
|
+
"resource",
|
|
246
|
+
) as PerformanceResourceTiming[];
|
|
247
|
+
return {
|
|
248
|
+
totalSize: entries.reduce((sum, e) => sum + (e.transferSize || 0), 0),
|
|
249
|
+
jsSize: entries
|
|
250
|
+
.filter((e) => e.initiatorType === "script")
|
|
251
|
+
.reduce((sum, e) => sum + (e.transferSize || 0), 0),
|
|
252
|
+
imageCount: entries.filter((e) => e.initiatorType === "img").length,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Assert budgets
|
|
257
|
+
expect(lcp, "LCP exceeds budget").toBeLessThan(budget.lcp);
|
|
258
|
+
expect(resources.totalSize, "Total size exceeds budget").toBeLessThan(
|
|
259
|
+
budget.totalSize,
|
|
260
|
+
);
|
|
261
|
+
expect(resources.jsSize, "JS size exceeds budget").toBeLessThan(
|
|
262
|
+
budget.jsSize,
|
|
263
|
+
);
|
|
264
|
+
expect(resources.imageCount, "Too many images").toBeLessThanOrEqual(
|
|
265
|
+
budget.imageCount,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Budget Fixture
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// fixtures/performance.fixture.ts
|
|
274
|
+
type PerformanceBudget = {
|
|
275
|
+
lcp?: number;
|
|
276
|
+
cls?: number;
|
|
277
|
+
ttfb?: number;
|
|
278
|
+
totalSize?: number;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
type PerformanceFixtures = {
|
|
282
|
+
assertBudget: (budget: PerformanceBudget) => Promise<void>;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const test = base.extend<PerformanceFixtures>({
|
|
286
|
+
assertBudget: async ({ page }, use) => {
|
|
287
|
+
await use(async (budget) => {
|
|
288
|
+
const metrics = await page.evaluate(() => {
|
|
289
|
+
const nav = performance.getEntriesByType(
|
|
290
|
+
"navigation",
|
|
291
|
+
)[0] as PerformanceNavigationTiming;
|
|
292
|
+
const resources = performance.getEntriesByType(
|
|
293
|
+
"resource",
|
|
294
|
+
) as PerformanceResourceTiming[];
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
ttfb: nav.responseStart - nav.requestStart,
|
|
298
|
+
totalSize: resources.reduce(
|
|
299
|
+
(sum, r) => sum + (r.transferSize || 0),
|
|
300
|
+
0,
|
|
301
|
+
),
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (budget.ttfb) {
|
|
306
|
+
expect(
|
|
307
|
+
metrics.ttfb,
|
|
308
|
+
`TTFB ${metrics.ttfb}ms exceeds budget ${budget.ttfb}ms`,
|
|
309
|
+
).toBeLessThan(budget.ttfb);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (budget.totalSize) {
|
|
313
|
+
expect(metrics.totalSize, `Total size exceeds budget`).toBeLessThan(
|
|
314
|
+
budget.totalSize,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Lighthouse Integration
|
|
323
|
+
|
|
324
|
+
### Using playwright-lighthouse
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
npm install -D playwright-lighthouse lighthouse
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { playAudit } from "playwright-lighthouse";
|
|
332
|
+
|
|
333
|
+
test("lighthouse audit", async ({ page }) => {
|
|
334
|
+
await page.goto("/");
|
|
335
|
+
|
|
336
|
+
// Run Lighthouse
|
|
337
|
+
const audit = await playAudit({
|
|
338
|
+
page,
|
|
339
|
+
port: 9222, // Chrome debugging port
|
|
340
|
+
thresholds: {
|
|
341
|
+
performance: 80,
|
|
342
|
+
accessibility: 90,
|
|
343
|
+
"best-practices": 80,
|
|
344
|
+
seo: 80,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Assertions
|
|
349
|
+
expect(audit.lhr.categories.performance.score * 100).toBeGreaterThanOrEqual(
|
|
350
|
+
80,
|
|
351
|
+
);
|
|
352
|
+
expect(audit.lhr.categories.accessibility.score * 100).toBeGreaterThanOrEqual(
|
|
353
|
+
90,
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Lighthouse with Config
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
test("lighthouse with custom config", async ({ page }, testInfo) => {
|
|
362
|
+
await page.goto("/");
|
|
363
|
+
|
|
364
|
+
const audit = await playAudit({
|
|
365
|
+
page,
|
|
366
|
+
port: 9222,
|
|
367
|
+
thresholds: {
|
|
368
|
+
performance: 70,
|
|
369
|
+
},
|
|
370
|
+
config: {
|
|
371
|
+
extends: "lighthouse:default",
|
|
372
|
+
settings: {
|
|
373
|
+
onlyCategories: ["performance"],
|
|
374
|
+
throttling: {
|
|
375
|
+
rttMs: 40,
|
|
376
|
+
throughputKbps: 10240,
|
|
377
|
+
cpuSlowdownMultiplier: 1,
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Save report
|
|
384
|
+
const reportPath = testInfo.outputPath("lighthouse-report.html");
|
|
385
|
+
// Save audit.report to file
|
|
386
|
+
|
|
387
|
+
// Attach to test report
|
|
388
|
+
await testInfo.attach("lighthouse", {
|
|
389
|
+
body: JSON.stringify(audit.lhr),
|
|
390
|
+
contentType: "application/json",
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## CI Performance Monitoring
|
|
396
|
+
|
|
397
|
+
### Track Performance Over Time
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// reporters/perf-reporter.ts
|
|
401
|
+
import { Reporter, TestResult } from "@playwright/test/reporter";
|
|
402
|
+
|
|
403
|
+
class PerfReporter implements Reporter {
|
|
404
|
+
private metrics: any[] = [];
|
|
405
|
+
|
|
406
|
+
onTestEnd(test: any, result: TestResult) {
|
|
407
|
+
const perfAnnotation = test.annotations.find(
|
|
408
|
+
(a: any) => a.type === "performance",
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (perfAnnotation) {
|
|
412
|
+
this.metrics.push({
|
|
413
|
+
test: test.title,
|
|
414
|
+
...JSON.parse(perfAnnotation.description),
|
|
415
|
+
timestamp: new Date().toISOString(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async onEnd() {
|
|
421
|
+
// Send to metrics service
|
|
422
|
+
if (process.env.METRICS_ENDPOINT) {
|
|
423
|
+
await fetch(process.env.METRICS_ENDPOINT, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
body: JSON.stringify({
|
|
426
|
+
commit: process.env.GITHUB_SHA,
|
|
427
|
+
branch: process.env.GITHUB_REF,
|
|
428
|
+
metrics: this.metrics,
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export default PerfReporter;
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Performance Regression Detection
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
test("no performance regression", async ({ page }) => {
|
|
442
|
+
await page.goto("/");
|
|
443
|
+
|
|
444
|
+
const metrics = await page.evaluate(() => {
|
|
445
|
+
const nav = performance.getEntriesByType(
|
|
446
|
+
"navigation",
|
|
447
|
+
)[0] as PerformanceNavigationTiming;
|
|
448
|
+
return {
|
|
449
|
+
loadTime: nav.loadEventEnd - nav.startTime,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Compare against baseline (could be from file or API)
|
|
454
|
+
const baseline = 2000; // ms
|
|
455
|
+
const threshold = 1.1; // 10% regression allowed
|
|
456
|
+
|
|
457
|
+
expect(
|
|
458
|
+
metrics.loadTime,
|
|
459
|
+
`Load time ${metrics.loadTime}ms is ${((metrics.loadTime / baseline - 1) * 100).toFixed(1)}% slower than baseline`,
|
|
460
|
+
).toBeLessThan(baseline * threshold);
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Anti-Patterns to Avoid
|
|
465
|
+
|
|
466
|
+
| Anti-Pattern | Problem | Solution |
|
|
467
|
+
| --------------------------- | ------------------------- | -------------------------------- |
|
|
468
|
+
| Testing only once | Results vary | Run multiple times, use averages |
|
|
469
|
+
| Ignoring network conditions | Unrealistic results | Test with throttling |
|
|
470
|
+
| No baseline comparison | Can't detect regressions | Track metrics over time |
|
|
471
|
+
| Testing in dev mode | Slow, not production-like | Test production builds |
|
|
472
|
+
|
|
473
|
+
## Related References
|
|
474
|
+
|
|
475
|
+
- **Performance Optimization**: See [performance.md](../infrastructure-ci-cd/performance.md) for test execution performance
|
|
476
|
+
- **CI/CD**: See [ci-cd.md](../infrastructure-ci-cd/ci-cd.md) for CI integration
|