@test-station/adapter-playwright 0.1.7 → 0.2.10
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 +24 -0
- package/package.json +4 -1
- package/src/index.js +237 -17
package/README.md
CHANGED
|
@@ -15,9 +15,33 @@ npm install --save-dev @test-station/adapter-playwright
|
|
|
15
15
|
- runs Playwright with JSON reporter output
|
|
16
16
|
- normalizes browser suite and test results into the shared report model
|
|
17
17
|
- writes raw Playwright JSON artifacts under `raw/`
|
|
18
|
+
- optionally collects browser Istanbul coverage when `suite.coverage.strategy` is `browser-istanbul`
|
|
19
|
+
- writes suite-scoped browser coverage artifacts under `raw/`
|
|
18
20
|
|
|
19
21
|
Playwright browsers must already be installed in CI or on the machine executing the suite.
|
|
20
22
|
|
|
23
|
+
## Browser Coverage
|
|
24
|
+
|
|
25
|
+
When coverage is enabled for the run and the suite declares:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
coverage: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
strategy: 'browser-istanbul',
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
the adapter sets:
|
|
35
|
+
|
|
36
|
+
- `PLAYWRIGHT_BROWSER_COVERAGE=1`
|
|
37
|
+
- `PLAYWRIGHT_BROWSER_COVERAGE_DIR=<temp dir>`
|
|
38
|
+
|
|
39
|
+
The Playwright suite can use those environment variables to persist `window.__coverage__` payloads. Test Station merges those payloads into the normalized suite coverage summary and retains the raw files under a stable suite-scoped artifact directory such as:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
raw/<package>-<suite>-playwright-coverage/
|
|
43
|
+
```
|
|
44
|
+
|
|
21
45
|
## Direct Use
|
|
22
46
|
|
|
23
47
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@test-station/adapter-playwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.js"
|
|
10
10
|
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"istanbul-lib-coverage": "^3.2.2"
|
|
13
|
+
},
|
|
11
14
|
"scripts": {
|
|
12
15
|
"build": "node ../../scripts/check-package.mjs ./src/index.js",
|
|
13
16
|
"lint": "node ../../scripts/lint-syntax.mjs ./src"
|
package/src/index.js
CHANGED
|
@@ -1,41 +1,102 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
2
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
import istanbulCoverage from 'istanbul-lib-coverage';
|
|
3
6
|
|
|
4
7
|
export const id = 'playwright';
|
|
5
8
|
export const description = 'Playwright adapter';
|
|
6
9
|
|
|
10
|
+
const { createCoverageMap } = istanbulCoverage;
|
|
11
|
+
|
|
7
12
|
export function createPlaywrightAdapter() {
|
|
8
13
|
return {
|
|
9
14
|
id,
|
|
10
15
|
description,
|
|
11
|
-
phase:
|
|
12
|
-
async run({ project, suite }) {
|
|
16
|
+
phase: 8,
|
|
17
|
+
async run({ project, suite, execution: executionOptions }) {
|
|
13
18
|
const commandSpec = parseCommandSpec(suite.command);
|
|
14
|
-
const
|
|
19
|
+
const browserCoverageEnabled = shouldCollectBrowserCoverage({
|
|
20
|
+
execution: executionOptions,
|
|
21
|
+
suite,
|
|
22
|
+
});
|
|
23
|
+
const coverageDir = browserCoverageEnabled
|
|
24
|
+
? fs.mkdtempSync(
|
|
25
|
+
path.join(
|
|
26
|
+
os.tmpdir(),
|
|
27
|
+
`test-station-playwright-coverage-${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-`
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
: null;
|
|
31
|
+
const commandExecution = await spawnCommand(commandSpec.command, appendPlaywrightJsonArgs(commandSpec.args), {
|
|
15
32
|
cwd: suite.cwd || project.rootDir,
|
|
16
|
-
env: resolveSuiteEnv(
|
|
33
|
+
env: resolveSuiteEnv({
|
|
34
|
+
...(suite.env || {}),
|
|
35
|
+
...(browserCoverageEnabled
|
|
36
|
+
? {
|
|
37
|
+
PLAYWRIGHT_BROWSER_COVERAGE: '1',
|
|
38
|
+
PLAYWRIGHT_BROWSER_COVERAGE_DIR: coverageDir,
|
|
39
|
+
}
|
|
40
|
+
: {}),
|
|
41
|
+
}),
|
|
17
42
|
});
|
|
18
43
|
|
|
19
|
-
const payload = extractJsonPayload(
|
|
44
|
+
const payload = extractJsonPayload(commandExecution.stdout || commandExecution.stderr);
|
|
20
45
|
const parsed = parsePlaywrightReport(payload, suite.cwd || project.rootDir);
|
|
46
|
+
const rawArtifacts = [
|
|
47
|
+
{
|
|
48
|
+
relativePath: `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-playwright.json`,
|
|
49
|
+
content: JSON.stringify(payload, null, 2),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const warnings = [];
|
|
53
|
+
let coverage = null;
|
|
54
|
+
|
|
55
|
+
if (browserCoverageEnabled && coverageDir) {
|
|
56
|
+
const coverageArtifactBase = `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-playwright-coverage`;
|
|
57
|
+
coverage = mergeBrowserCoverage({
|
|
58
|
+
coverageDir,
|
|
59
|
+
packageName: suite.packageName || null,
|
|
60
|
+
coverageRootDir: suite.cwd || project.rootDir,
|
|
61
|
+
projectRootDir: project.rootDir,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
rawArtifacts.push({
|
|
65
|
+
relativePath: coverageArtifactBase,
|
|
66
|
+
sourcePath: coverageDir,
|
|
67
|
+
kind: 'directory',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (coverage) {
|
|
71
|
+
const coverageSummaryPath = path.join(coverageDir, 'coverage-summary.json');
|
|
72
|
+
const mergedCoveragePath = path.join(coverageDir, 'merged-coverage.json');
|
|
73
|
+
fs.writeFileSync(coverageSummaryPath, `${JSON.stringify(coverage, null, 2)}\n`);
|
|
74
|
+
fs.writeFileSync(mergedCoveragePath, `${JSON.stringify(buildMergedCoveragePayload(coverageDir), null, 2)}\n`);
|
|
75
|
+
rawArtifacts.push({
|
|
76
|
+
relativePath: `${coverageArtifactBase}/coverage-summary.json`,
|
|
77
|
+
sourcePath: coverageSummaryPath,
|
|
78
|
+
});
|
|
79
|
+
rawArtifacts.push({
|
|
80
|
+
relativePath: `${coverageArtifactBase}/merged-coverage.json`,
|
|
81
|
+
sourcePath: mergedCoveragePath,
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
warnings.push('Browser coverage was requested, but no window.__coverage__ payloads were collected.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
21
87
|
|
|
22
88
|
return {
|
|
23
|
-
status: deriveSuiteStatus(parsed.summary,
|
|
24
|
-
durationMs:
|
|
89
|
+
status: deriveSuiteStatus(parsed.summary, commandExecution.exitCode),
|
|
90
|
+
durationMs: commandExecution.durationMs,
|
|
25
91
|
summary: parsed.summary,
|
|
26
|
-
coverage
|
|
92
|
+
coverage,
|
|
27
93
|
tests: parsed.tests,
|
|
28
|
-
warnings
|
|
94
|
+
warnings,
|
|
29
95
|
output: {
|
|
30
|
-
stdout:
|
|
31
|
-
stderr:
|
|
96
|
+
stdout: commandExecution.stdout,
|
|
97
|
+
stderr: commandExecution.stderr,
|
|
32
98
|
},
|
|
33
|
-
rawArtifacts
|
|
34
|
-
{
|
|
35
|
-
relativePath: `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-playwright.json`,
|
|
36
|
-
content: JSON.stringify(payload, null, 2),
|
|
37
|
-
},
|
|
38
|
-
],
|
|
99
|
+
rawArtifacts,
|
|
39
100
|
};
|
|
40
101
|
},
|
|
41
102
|
};
|
|
@@ -68,6 +129,14 @@ function appendPlaywrightJsonArgs(args) {
|
|
|
68
129
|
return [...filtered, '--reporter=json'];
|
|
69
130
|
}
|
|
70
131
|
|
|
132
|
+
function shouldCollectBrowserCoverage(context) {
|
|
133
|
+
return Boolean(
|
|
134
|
+
context?.execution?.coverage
|
|
135
|
+
&& context?.suite?.coverage?.enabled !== false
|
|
136
|
+
&& context?.suite?.coverage?.strategy === 'browser-istanbul'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
71
140
|
function tokenizeCommand(command) {
|
|
72
141
|
const tokens = [];
|
|
73
142
|
let current = '';
|
|
@@ -170,6 +239,157 @@ function parsePlaywrightReport(report, workspaceDir) {
|
|
|
170
239
|
};
|
|
171
240
|
}
|
|
172
241
|
|
|
242
|
+
function mergeBrowserCoverage(options) {
|
|
243
|
+
const payloads = readCoveragePayloads(options.coverageDir);
|
|
244
|
+
if (payloads.length === 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const coverageMap = createCoverageMap({});
|
|
249
|
+
for (const payload of payloads) {
|
|
250
|
+
const pageEntries = Array.isArray(payload.pages) ? payload.pages : [];
|
|
251
|
+
for (const pageEntry of pageEntries) {
|
|
252
|
+
if (pageEntry?.coverage && typeof pageEntry.coverage === 'object') {
|
|
253
|
+
coverageMap.merge(pageEntry.coverage);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const files = coverageMap.files()
|
|
259
|
+
.map((filePath) => normalizeCoverageFileEntry(coverageMap, filePath, options))
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
262
|
+
|
|
263
|
+
if (files.length === 0) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return normalizeCoverageSummary({ files }, options.packageName || null);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildMergedCoveragePayload(coverageDir) {
|
|
271
|
+
const payloads = readCoveragePayloads(coverageDir);
|
|
272
|
+
const coverageMap = createCoverageMap({});
|
|
273
|
+
for (const payload of payloads) {
|
|
274
|
+
for (const pageEntry of payload.pages || []) {
|
|
275
|
+
if (pageEntry?.coverage && typeof pageEntry.coverage === 'object') {
|
|
276
|
+
coverageMap.merge(pageEntry.coverage);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return coverageMap.toJSON();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readCoveragePayloads(coverageDir) {
|
|
284
|
+
if (!coverageDir || !fs.existsSync(coverageDir)) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return fs.readdirSync(coverageDir)
|
|
289
|
+
.filter((fileName) => fileName.endsWith('.json'))
|
|
290
|
+
.filter((fileName) => !['coverage-summary.json', 'merged-coverage.json'].includes(fileName))
|
|
291
|
+
.map((fileName) => path.join(coverageDir, fileName))
|
|
292
|
+
.map((filePath) => {
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
.filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeCoverageFileEntry(coverageMap, filePath, options = {}) {
|
|
303
|
+
const normalizedPath = normalizeSourcePath(filePath, options.projectRootDir);
|
|
304
|
+
if (options.coverageRootDir && !isWithinDirectory(normalizedPath, options.coverageRootDir)) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const summary = coverageMap.fileCoverageFor(filePath).toSummary();
|
|
309
|
+
return {
|
|
310
|
+
path: normalizedPath,
|
|
311
|
+
lines: createCoverageMetric(summary.lines?.covered, summary.lines?.total),
|
|
312
|
+
statements: createCoverageMetric(summary.statements?.covered, summary.statements?.total),
|
|
313
|
+
functions: createCoverageMetric(summary.functions?.covered, summary.functions?.total),
|
|
314
|
+
branches: createCoverageMetric(summary.branches?.covered, summary.branches?.total),
|
|
315
|
+
packageName: options.packageName || null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizeCoverageSummary(coverage, packageName = null) {
|
|
320
|
+
if (!coverage) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const files = Array.isArray(coverage.files)
|
|
325
|
+
? coverage.files.map((file) => ({
|
|
326
|
+
path: file.path,
|
|
327
|
+
lines: file.lines || null,
|
|
328
|
+
statements: file.statements || null,
|
|
329
|
+
functions: file.functions || null,
|
|
330
|
+
branches: file.branches || null,
|
|
331
|
+
packageName: file.packageName || packageName || null,
|
|
332
|
+
}))
|
|
333
|
+
: [];
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
lines: coverage.lines || aggregateCoverageMetric(files, 'lines'),
|
|
337
|
+
statements: coverage.statements || aggregateCoverageMetric(files, 'statements'),
|
|
338
|
+
functions: coverage.functions || aggregateCoverageMetric(files, 'functions'),
|
|
339
|
+
branches: coverage.branches || aggregateCoverageMetric(files, 'branches'),
|
|
340
|
+
files,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function aggregateCoverageMetric(files, metricKey) {
|
|
345
|
+
const valid = files
|
|
346
|
+
.map((file) => file?.[metricKey])
|
|
347
|
+
.filter((metric) => metric && Number.isFinite(metric.total));
|
|
348
|
+
|
|
349
|
+
if (valid.length === 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const total = valid.reduce((sum, metric) => sum + metric.total, 0);
|
|
354
|
+
const covered = valid.reduce((sum, metric) => sum + metric.covered, 0);
|
|
355
|
+
return createCoverageMetric(covered, total);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createCoverageMetric(covered, total) {
|
|
359
|
+
if (!Number.isFinite(total)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const safeTotal = Math.max(0, total);
|
|
363
|
+
const safeCovered = Number.isFinite(covered) ? Math.max(0, Math.min(safeTotal, covered)) : 0;
|
|
364
|
+
const pct = safeTotal === 0 ? 100 : Number(((safeCovered / safeTotal) * 100).toFixed(2));
|
|
365
|
+
return {
|
|
366
|
+
covered: safeCovered,
|
|
367
|
+
total: safeTotal,
|
|
368
|
+
pct,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeSourcePath(filePath, projectRootDir) {
|
|
373
|
+
let nextPath = String(filePath || '');
|
|
374
|
+
nextPath = nextPath.replace(/^file:\/\//, '');
|
|
375
|
+
nextPath = nextPath.replace(/^webpack:\/\/_N_E\//, '');
|
|
376
|
+
nextPath = nextPath.replace(/^webpack:\/\//, '');
|
|
377
|
+
nextPath = nextPath.replace(/^\.\//, '');
|
|
378
|
+
nextPath = nextPath.split('?')[0];
|
|
379
|
+
nextPath = nextPath.split('#')[0];
|
|
380
|
+
|
|
381
|
+
if (path.isAbsolute(nextPath)) {
|
|
382
|
+
return path.normalize(nextPath);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return path.resolve(projectRootDir || process.cwd(), nextPath);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isWithinDirectory(targetPath, rootDir) {
|
|
389
|
+
const relative = path.relative(rootDir, targetPath);
|
|
390
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
391
|
+
}
|
|
392
|
+
|
|
173
393
|
function collectPlaywrightTests(suite, titleTrail, bucket, rootDir) {
|
|
174
394
|
const nextTrail = suite.title && !suite.title.endsWith('.spec.js') ? [...titleTrail, suite.title] : titleTrail;
|
|
175
395
|
for (const spec of suite.specs || []) {
|