@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,497 @@
|
|
|
1
|
+
# Test Coverage
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Coverage Setup](#coverage-setup)
|
|
6
|
+
2. [Collecting Coverage](#collecting-coverage)
|
|
7
|
+
3. [Coverage Reports](#coverage-reports)
|
|
8
|
+
4. [Coverage Thresholds](#coverage-thresholds)
|
|
9
|
+
5. [Advanced Patterns](#advanced-patterns)
|
|
10
|
+
6. [CI Integration](#ci-integration)
|
|
11
|
+
|
|
12
|
+
## Coverage Setup
|
|
13
|
+
|
|
14
|
+
### Install Dependencies
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# For V8 coverage (built into Playwright)
|
|
18
|
+
# No additional dependencies needed
|
|
19
|
+
|
|
20
|
+
# For Istanbul-based coverage (more features)
|
|
21
|
+
npm install -D nyc @istanbuljs/nyc-config-typescript
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Basic Configuration
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// playwright.config.ts
|
|
28
|
+
import { defineConfig } from "@playwright/test";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
use: {
|
|
32
|
+
// Enable coverage collection
|
|
33
|
+
contextOptions: {
|
|
34
|
+
// V8 coverage is automatic with the API below
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### V8 Coverage Fixture
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// fixtures/coverage.ts
|
|
44
|
+
import { test as base, expect } from "@playwright/test";
|
|
45
|
+
import fs from "fs";
|
|
46
|
+
import path from "path";
|
|
47
|
+
import { randomUUID } from "crypto";
|
|
48
|
+
|
|
49
|
+
export const test = base.extend<{}, { collectCoverage: void }>({
|
|
50
|
+
collectCoverage: [
|
|
51
|
+
async ({ browser }, use) => {
|
|
52
|
+
// Start coverage for all pages
|
|
53
|
+
const context = await browser.newContext();
|
|
54
|
+
const page = await context.newPage();
|
|
55
|
+
|
|
56
|
+
await page.coverage.startJSCoverage();
|
|
57
|
+
await page.coverage.startCSSCoverage();
|
|
58
|
+
|
|
59
|
+
await use();
|
|
60
|
+
|
|
61
|
+
// Collect coverage
|
|
62
|
+
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
63
|
+
page.coverage.stopJSCoverage(),
|
|
64
|
+
page.coverage.stopCSSCoverage(),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// Save coverage data
|
|
68
|
+
const coverageDir = "./coverage";
|
|
69
|
+
if (!fs.existsSync(coverageDir)) {
|
|
70
|
+
fs.mkdirSync(coverageDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(coverageDir, `coverage-${randomUUID()}.json`),
|
|
75
|
+
JSON.stringify([...jsCoverage, ...cssCoverage])
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await context.close();
|
|
79
|
+
},
|
|
80
|
+
{ scope: "worker", auto: true },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Collecting Coverage
|
|
86
|
+
|
|
87
|
+
### Per-Test Coverage
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
test("collect coverage for single test", async ({ page }) => {
|
|
91
|
+
// Start coverage collection
|
|
92
|
+
await page.coverage.startJSCoverage({
|
|
93
|
+
resetOnNavigation: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Run test
|
|
97
|
+
await page.goto("/app");
|
|
98
|
+
await page.getByRole("button", { name: "Submit" }).click();
|
|
99
|
+
await expect(page.getByText("Success")).toBeVisible();
|
|
100
|
+
|
|
101
|
+
// Stop and get coverage
|
|
102
|
+
const coverage = await page.coverage.stopJSCoverage();
|
|
103
|
+
|
|
104
|
+
// Filter to only your source files
|
|
105
|
+
const appCoverage = coverage.filter((entry) => entry.url.includes("/src/"));
|
|
106
|
+
|
|
107
|
+
console.log(`Covered ${appCoverage.length} source files`);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Coverage for Specific Files
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
test("track specific module coverage", async ({ page }) => {
|
|
115
|
+
await page.coverage.startJSCoverage();
|
|
116
|
+
|
|
117
|
+
await page.goto("/checkout");
|
|
118
|
+
await page.getByRole("button", { name: "Pay" }).click();
|
|
119
|
+
|
|
120
|
+
const coverage = await page.coverage.stopJSCoverage();
|
|
121
|
+
|
|
122
|
+
// Find coverage for checkout module
|
|
123
|
+
const checkoutCoverage = coverage.find((c) => c.url.includes("checkout.js"));
|
|
124
|
+
|
|
125
|
+
if (checkoutCoverage) {
|
|
126
|
+
const totalBytes = checkoutCoverage.text?.length || 0;
|
|
127
|
+
const coveredBytes = checkoutCoverage.ranges.reduce(
|
|
128
|
+
(sum, range) => sum + (range.end - range.start),
|
|
129
|
+
0
|
|
130
|
+
);
|
|
131
|
+
const percentage = (coveredBytes / totalBytes) * 100;
|
|
132
|
+
|
|
133
|
+
console.log(`Checkout module: ${percentage.toFixed(1)}% covered`);
|
|
134
|
+
expect(percentage).toBeGreaterThan(80);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### CSS Coverage
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
test("collect CSS coverage", async ({ page }) => {
|
|
143
|
+
await page.coverage.startCSSCoverage();
|
|
144
|
+
|
|
145
|
+
await page.goto("/app");
|
|
146
|
+
|
|
147
|
+
// Interact to trigger different CSS states
|
|
148
|
+
await page.getByRole("button").hover();
|
|
149
|
+
await page.getByRole("dialog").waitFor();
|
|
150
|
+
|
|
151
|
+
const cssCoverage = await page.coverage.stopCSSCoverage();
|
|
152
|
+
|
|
153
|
+
// Find unused CSS
|
|
154
|
+
for (const entry of cssCoverage) {
|
|
155
|
+
const totalBytes = entry.text?.length || 0;
|
|
156
|
+
const usedBytes = entry.ranges.reduce(
|
|
157
|
+
(sum, range) => sum + (range.end - range.start),
|
|
158
|
+
0
|
|
159
|
+
);
|
|
160
|
+
const unusedPercentage = ((totalBytes - usedBytes) / totalBytes) * 100;
|
|
161
|
+
|
|
162
|
+
if (unusedPercentage > 50) {
|
|
163
|
+
console.warn(`${entry.url}: ${unusedPercentage.toFixed(1)}% unused CSS`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Coverage Reports
|
|
170
|
+
|
|
171
|
+
### Converting to Istanbul Format
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// scripts/convert-coverage.ts
|
|
175
|
+
import { execSync } from "child_process";
|
|
176
|
+
import fs from "fs";
|
|
177
|
+
import path from "path";
|
|
178
|
+
import v8ToIstanbul from "v8-to-istanbul";
|
|
179
|
+
|
|
180
|
+
async function convertCoverage() {
|
|
181
|
+
const coverageDir = "./coverage";
|
|
182
|
+
const files = fs.readdirSync(coverageDir).filter((f) => f.endsWith(".json"));
|
|
183
|
+
|
|
184
|
+
const istanbulCoverage: any = {};
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const coverageData = JSON.parse(
|
|
188
|
+
fs.readFileSync(path.join(coverageDir, file), "utf-8")
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for (const entry of coverageData) {
|
|
192
|
+
if (!entry.url.startsWith("file://")) continue;
|
|
193
|
+
|
|
194
|
+
const filePath = entry.url.replace("file://", "");
|
|
195
|
+
const converter = v8ToIstanbul(filePath);
|
|
196
|
+
|
|
197
|
+
await converter.load();
|
|
198
|
+
converter.applyCoverage(entry.functions || []);
|
|
199
|
+
|
|
200
|
+
const istanbul = converter.toIstanbul();
|
|
201
|
+
Object.assign(istanbulCoverage, istanbul);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync(
|
|
206
|
+
path.join(coverageDir, "coverage-final.json"),
|
|
207
|
+
JSON.stringify(istanbulCoverage)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
convertCoverage();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Generating HTML Report
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Using nyc to generate report
|
|
218
|
+
npx nyc report --reporter=html --reporter=text --temp-dir=./coverage
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// package.json scripts
|
|
223
|
+
{
|
|
224
|
+
"scripts": {
|
|
225
|
+
"test": "playwright test",
|
|
226
|
+
"test:coverage": "playwright test && npm run coverage:report",
|
|
227
|
+
"coverage:report": "npx nyc report --reporter=html --reporter=lcov --temp-dir=./coverage"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Custom Coverage Reporter
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// reporters/coverage-reporter.ts
|
|
236
|
+
import type { Reporter, FullResult } from "@playwright/test/reporter";
|
|
237
|
+
import fs from "fs";
|
|
238
|
+
import path from "path";
|
|
239
|
+
|
|
240
|
+
class CoverageReporter implements Reporter {
|
|
241
|
+
private coverageData: any[] = [];
|
|
242
|
+
|
|
243
|
+
onEnd(result: FullResult) {
|
|
244
|
+
// Aggregate all coverage files
|
|
245
|
+
const coverageDir = "./coverage";
|
|
246
|
+
const files = fs
|
|
247
|
+
.readdirSync(coverageDir)
|
|
248
|
+
.filter((f) => f.endsWith(".json"));
|
|
249
|
+
|
|
250
|
+
for (const file of files) {
|
|
251
|
+
const data = JSON.parse(
|
|
252
|
+
fs.readFileSync(path.join(coverageDir, file), "utf-8")
|
|
253
|
+
);
|
|
254
|
+
this.coverageData.push(...data);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Generate summary
|
|
258
|
+
const summary = this.generateSummary();
|
|
259
|
+
console.log("\n📊 Coverage Summary:");
|
|
260
|
+
console.log(` Files: ${summary.totalFiles}`);
|
|
261
|
+
console.log(` Lines: ${summary.lineCoverage.toFixed(1)}%`);
|
|
262
|
+
console.log(` Bytes: ${summary.byteCoverage.toFixed(1)}%`);
|
|
263
|
+
|
|
264
|
+
if (summary.lineCoverage < 80) {
|
|
265
|
+
console.warn("⚠️ Coverage below 80% threshold!");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private generateSummary() {
|
|
270
|
+
let totalBytes = 0;
|
|
271
|
+
let coveredBytes = 0;
|
|
272
|
+
const files = new Set<string>();
|
|
273
|
+
|
|
274
|
+
for (const entry of this.coverageData) {
|
|
275
|
+
if (entry.url.includes("/src/")) {
|
|
276
|
+
files.add(entry.url);
|
|
277
|
+
totalBytes += entry.text?.length || 0;
|
|
278
|
+
coveredBytes += entry.ranges.reduce(
|
|
279
|
+
(sum: number, r: any) => sum + (r.end - r.start),
|
|
280
|
+
0
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
totalFiles: files.size,
|
|
287
|
+
byteCoverage: (coveredBytes / totalBytes) * 100,
|
|
288
|
+
lineCoverage: (coveredBytes / totalBytes) * 100, // Simplified
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default CoverageReporter;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Coverage Thresholds
|
|
297
|
+
|
|
298
|
+
### Enforcing Minimum Coverage
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// tests/coverage.spec.ts
|
|
302
|
+
import { test, expect } from "@playwright/test";
|
|
303
|
+
import fs from "fs";
|
|
304
|
+
import path from "path";
|
|
305
|
+
|
|
306
|
+
test.afterAll(async () => {
|
|
307
|
+
const coverageDir = "./coverage";
|
|
308
|
+
const files = fs.readdirSync(coverageDir).filter((f) => f.endsWith(".json"));
|
|
309
|
+
|
|
310
|
+
let totalBytes = 0;
|
|
311
|
+
let coveredBytes = 0;
|
|
312
|
+
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
const coverage = JSON.parse(
|
|
315
|
+
fs.readFileSync(path.join(coverageDir, file), "utf-8")
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
for (const entry of coverage) {
|
|
319
|
+
if (!entry.url.includes("/src/")) continue;
|
|
320
|
+
totalBytes += entry.text?.length || 0;
|
|
321
|
+
coveredBytes += entry.ranges.reduce(
|
|
322
|
+
(sum: number, r: any) => sum + (r.end - r.start),
|
|
323
|
+
0
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const coveragePercent = (coveredBytes / totalBytes) * 100;
|
|
329
|
+
|
|
330
|
+
// Enforce threshold
|
|
331
|
+
expect(coveragePercent).toBeGreaterThan(80);
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Per-Directory Thresholds
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// coverage-check.ts
|
|
339
|
+
interface CoverageThreshold {
|
|
340
|
+
pattern: RegExp;
|
|
341
|
+
minCoverage: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const thresholds: CoverageThreshold[] = [
|
|
345
|
+
{ pattern: /\/src\/core\//, minCoverage: 90 },
|
|
346
|
+
{ pattern: /\/src\/utils\//, minCoverage: 85 },
|
|
347
|
+
{ pattern: /\/src\/components\//, minCoverage: 70 },
|
|
348
|
+
{ pattern: /\/src\/pages\//, minCoverage: 60 },
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
function checkThresholds(coverage: any[]): string[] {
|
|
352
|
+
const violations: string[] = [];
|
|
353
|
+
|
|
354
|
+
for (const threshold of thresholds) {
|
|
355
|
+
const matchingFiles = coverage.filter((c) => threshold.pattern.test(c.url));
|
|
356
|
+
|
|
357
|
+
let total = 0;
|
|
358
|
+
let covered = 0;
|
|
359
|
+
|
|
360
|
+
for (const file of matchingFiles) {
|
|
361
|
+
total += file.text?.length || 0;
|
|
362
|
+
covered += file.ranges.reduce(
|
|
363
|
+
(sum: number, r: any) => sum + (r.end - r.start),
|
|
364
|
+
0
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const percent = total > 0 ? (covered / total) * 100 : 0;
|
|
369
|
+
|
|
370
|
+
if (percent < threshold.minCoverage) {
|
|
371
|
+
violations.push(
|
|
372
|
+
`${threshold.pattern}: ${percent.toFixed(1)}% < ${
|
|
373
|
+
threshold.minCoverage
|
|
374
|
+
}%`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return violations;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Advanced Patterns
|
|
384
|
+
|
|
385
|
+
### Merging Coverage Across Shards
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// scripts/merge-coverage.ts
|
|
389
|
+
import fs from "fs";
|
|
390
|
+
import { glob } from "glob";
|
|
391
|
+
|
|
392
|
+
async function mergeCoverage() {
|
|
393
|
+
const files = await glob("shard-*/coverage/*.json");
|
|
394
|
+
const merged = new Map<string, any>();
|
|
395
|
+
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
398
|
+
for (const entry of data) {
|
|
399
|
+
if (merged.has(entry.url)) {
|
|
400
|
+
const existing = merged.get(entry.url);
|
|
401
|
+
existing.ranges.push(...entry.ranges);
|
|
402
|
+
} else {
|
|
403
|
+
merged.set(entry.url, { ...entry });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fs.writeFileSync(
|
|
409
|
+
"./coverage/merged.json",
|
|
410
|
+
JSON.stringify([...merged.values()])
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
mergeCoverage();
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Incremental Coverage
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// Check coverage only for changed files in CI
|
|
421
|
+
import { execSync } from "child_process";
|
|
422
|
+
import fs from "fs";
|
|
423
|
+
|
|
424
|
+
const changedFiles = execSync("git diff --name-only HEAD~1")
|
|
425
|
+
.toString()
|
|
426
|
+
.split("\n")
|
|
427
|
+
.filter((f) => f.endsWith(".ts"));
|
|
428
|
+
|
|
429
|
+
const coverage = JSON.parse(fs.readFileSync("./coverage/merged.json", "utf-8"));
|
|
430
|
+
|
|
431
|
+
for (const file of changedFiles) {
|
|
432
|
+
const entry = coverage.find((c: any) => c.url.includes(file));
|
|
433
|
+
if (entry) {
|
|
434
|
+
const percent =
|
|
435
|
+
(entry.ranges.reduce((s: number, r: any) => s + r.end - r.start, 0) /
|
|
436
|
+
(entry.text?.length || 1)) *
|
|
437
|
+
100;
|
|
438
|
+
console.log(`${file}: ${percent.toFixed(1)}%`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## CI Integration
|
|
444
|
+
|
|
445
|
+
### GitHub Actions
|
|
446
|
+
|
|
447
|
+
```yaml
|
|
448
|
+
# .github/workflows/test.yml
|
|
449
|
+
name: Tests with Coverage
|
|
450
|
+
|
|
451
|
+
on: [push, pull_request]
|
|
452
|
+
|
|
453
|
+
jobs:
|
|
454
|
+
test:
|
|
455
|
+
runs-on: ubuntu-latest
|
|
456
|
+
steps:
|
|
457
|
+
- uses: actions/checkout@v4
|
|
458
|
+
|
|
459
|
+
- uses: actions/setup-node@v4
|
|
460
|
+
with:
|
|
461
|
+
node-version: 22
|
|
462
|
+
|
|
463
|
+
- run: npm ci
|
|
464
|
+
- run: npx playwright install --with-deps
|
|
465
|
+
|
|
466
|
+
- name: Run tests with coverage
|
|
467
|
+
run: npm run test:coverage
|
|
468
|
+
|
|
469
|
+
- name: Upload coverage to Codecov
|
|
470
|
+
uses: codecov/codecov-action@v3
|
|
471
|
+
with:
|
|
472
|
+
files: ./coverage/lcov.info
|
|
473
|
+
fail_ci_if_error: true
|
|
474
|
+
|
|
475
|
+
- name: Check coverage threshold
|
|
476
|
+
run: |
|
|
477
|
+
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
|
|
478
|
+
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
|
479
|
+
echo "Coverage $COVERAGE% is below 80% threshold"
|
|
480
|
+
exit 1
|
|
481
|
+
fi
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Anti-Patterns to Avoid
|
|
485
|
+
|
|
486
|
+
| Anti-Pattern | Problem | Solution |
|
|
487
|
+
| ---------------------------- | -------------------------------------- | --------------------------- |
|
|
488
|
+
| Coverage for coverage's sake | Gaming metrics | Focus on critical paths |
|
|
489
|
+
| 100% coverage target | Diminishing returns, tests for getters | Set realistic thresholds |
|
|
490
|
+
| Ignoring coverage drops | Technical debt | Enforce thresholds in CI |
|
|
491
|
+
| No source map support | Wrong line numbers | Enable source maps in build |
|
|
492
|
+
| Coverage only in CI | Late feedback | Run locally too |
|
|
493
|
+
|
|
494
|
+
## Related References
|
|
495
|
+
|
|
496
|
+
- **CI/CD**: See [ci-cd.md](ci-cd.md) for pipeline configuration
|
|
497
|
+
- **Performance**: See [performance.md](performance.md) for optimizing coverage collection
|