@piwitests/reporter 0.4.4 → 0.6.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 +37 -26
- package/dist/global-setup-module.js +2 -2
- package/dist/index.d.ts +15 -8
- package/dist/index.js +23 -5
- package/dist/internal/capture/attachments.d.ts +19 -0
- package/dist/internal/capture/attachments.js +22 -0
- package/dist/{fixtures.d.ts → internal/capture/capture-fixtures.d.ts} +6 -4
- package/dist/internal/capture/capture-fixtures.js +495 -0
- package/dist/internal/capture/locator-healing.d.ts +152 -0
- package/dist/internal/capture/locator-healing.js +668 -0
- package/dist/internal/collect/error-text.d.ts +18 -0
- package/dist/internal/collect/error-text.js +79 -0
- package/dist/{metadata-collector.d.ts → internal/collect/metadata-collector.d.ts} +2 -2
- package/dist/{metadata-collector.js → internal/collect/metadata-collector.js} +12 -9
- package/dist/{skip-classify.d.ts → internal/collect/skip-classify.d.ts} +1 -1
- package/dist/{step-analyzer.d.ts → internal/collect/step-analyzer.d.ts} +6 -0
- package/dist/{step-analyzer.js → internal/collect/step-analyzer.js} +7 -2
- package/dist/internal/config/env.d.ts +45 -0
- package/dist/{config.js → internal/config/env.js} +59 -33
- package/dist/internal/files/compression.js +69 -0
- package/dist/{file-handler.d.ts → internal/files/file-handler.d.ts} +4 -4
- package/dist/{file-handler.js → internal/files/file-handler.js} +61 -29
- package/dist/{crash-recovery.d.ts → internal/streaming/crash-recovery.d.ts} +5 -5
- package/dist/{crash-recovery.js → internal/streaming/crash-recovery.js} +9 -8
- package/dist/{stream-buffer.d.ts → internal/streaming/stream-buffer.d.ts} +2 -2
- package/dist/{stream-buffer.js → internal/streaming/stream-buffer.js} +5 -5
- package/dist/{stream-manager.d.ts → internal/streaming/stream-manager.d.ts} +22 -8
- package/dist/{stream-manager.js → internal/streaming/stream-manager.js} +66 -12
- package/dist/{run-submitter.d.ts → internal/submit/run-submitter.d.ts} +7 -7
- package/dist/{run-submitter.js → internal/submit/run-submitter.js} +13 -9
- package/dist/{serializer.d.ts → internal/submit/serializer.d.ts} +1 -1
- package/dist/{serializer.js → internal/submit/serializer.js} +2 -0
- package/dist/{uploader.d.ts → internal/submit/uploader.d.ts} +9 -8
- package/dist/{uploader.js → internal/submit/uploader.js} +45 -10
- package/dist/internal/support/ci.d.ts +2 -0
- package/dist/internal/support/ci.js +32 -0
- package/dist/internal/support/cli-filters.d.ts +1 -0
- package/dist/internal/support/cli-filters.js +51 -0
- package/dist/internal/support/errors.d.ts +8 -0
- package/dist/internal/support/errors.js +15 -0
- package/dist/internal/support/instance-id.d.ts +4 -0
- package/dist/internal/support/instance-id.js +48 -0
- package/dist/internal/support/limiter.d.ts +2 -0
- package/dist/internal/support/limiter.js +27 -0
- package/dist/internal/support/setup-file.d.ts +13 -0
- package/dist/internal/support/setup-file.js +61 -0
- package/dist/internal/support/source-snippet.d.ts +12 -0
- package/dist/internal/support/source-snippet.js +97 -0
- package/dist/internal/support/worker-index.d.ts +7 -0
- package/dist/internal/support/worker-index.js +14 -0
- package/dist/{http-client.d.ts → internal/transport/http-client.d.ts} +11 -1
- package/dist/{http-client.js → internal/transport/http-client.js} +57 -10
- package/dist/{config-wrapper.d.ts → public/config-wrapper.d.ts} +1 -1
- package/dist/{config-wrapper.js → public/config-wrapper.js} +4 -4
- package/dist/public/global-setup.d.ts +13 -0
- package/dist/public/global-setup.js +146 -0
- package/dist/{config.d.ts → public/options.d.ts} +18 -44
- package/dist/public/options.js +2 -0
- package/dist/{reporter.d.ts → public/reporter.d.ts} +3 -1
- package/dist/{reporter.js → public/reporter.js} +66 -34
- package/dist/types/collected.d.ts +96 -0
- package/dist/types/collected.js +10 -0
- package/dist/types/wire.d.ts +174 -0
- package/dist/types/wire.js +14 -0
- package/dist/types.d.ts +8 -248
- package/dist/types.js +23 -10
- package/package.json +1 -6
- package/dist/compression.js +0 -39
- package/dist/fixtures.js +0 -156
- package/dist/helpers.d.ts +0 -44
- package/dist/helpers.js +0 -288
- /package/dist/{skip-classify.js → internal/collect/skip-classify.js} +0 -0
- /package/dist/{compression.d.ts → internal/files/compression.d.ts} +0 -0
- /package/dist/{logger.d.ts → internal/support/logger.d.ts} +0 -0
- /package/dist/{logger.js → internal/support/logger.js} +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildErrorText = buildErrorText;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
/**
|
|
39
|
+
* Build the stored error text for a test result.
|
|
40
|
+
*
|
|
41
|
+
* Playwright's `result.error` carries the primary failure but for a **timeout**
|
|
42
|
+
* it is only the bare `Test timeout of 30000ms exceeded.` — the interrupted
|
|
43
|
+
* action's actual error (with its call log — the single most diagnostic fact)
|
|
44
|
+
* lives in `result.errors[1..]`. Concatenating all errors (deduped by message)
|
|
45
|
+
* ensures the server sees the call log so it can extract the locator, fingerprint
|
|
46
|
+
* correctly, and feed the model the right facts.
|
|
47
|
+
*
|
|
48
|
+
* A synthetic `at file:line:col` frame from `error.location` is appended when
|
|
49
|
+
* no stack frame is present, enabling locator-healing to find the pre-captured
|
|
50
|
+
* snapshot for that call site. The frame sits after the message, where
|
|
51
|
+
* `extractMessageHead` trims it before fingerprinting — so clustering is
|
|
52
|
+
* unaffected.
|
|
53
|
+
*/
|
|
54
|
+
function buildErrorText(result) {
|
|
55
|
+
if (!result.errors || result.errors.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (const err of result.errors) {
|
|
60
|
+
const msg = err.message ?? '';
|
|
61
|
+
if (!msg)
|
|
62
|
+
continue;
|
|
63
|
+
const dedupeKey = msg.trim().slice(0, 200);
|
|
64
|
+
if (seen.has(dedupeKey))
|
|
65
|
+
continue;
|
|
66
|
+
seen.add(dedupeKey);
|
|
67
|
+
parts.push(msg);
|
|
68
|
+
}
|
|
69
|
+
let text = parts.join('\n---\n');
|
|
70
|
+
if (!text)
|
|
71
|
+
return null;
|
|
72
|
+
const primary = result.error;
|
|
73
|
+
const loc = primary?.location;
|
|
74
|
+
if (loc?.file && !/\n\s+at\s/.test(text)) {
|
|
75
|
+
const rel = path.relative(process.cwd(), loc.file).split(path.sep).join('/');
|
|
76
|
+
text += `\n at ${rel}:${loc.line}:${loc.column}`;
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FullConfig, Suite, TestCase } from '@playwright/test/reporter';
|
|
2
|
-
import { Logger } from '
|
|
3
|
-
import type { SuiteConfigEntry } from '
|
|
2
|
+
import { Logger } from '../support/logger.js';
|
|
3
|
+
import type { SuiteConfigEntry } from '../../types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Collects CI/CD environment metadata, SCM (git) information, and Playwright
|
|
6
6
|
* config metadata to attach to each test run submission.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MetadataCollector = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const errors_js_1 = require("../support/errors.js");
|
|
6
|
+
const logger_js_1 = require("../support/logger.js");
|
|
6
7
|
/**
|
|
7
8
|
* Collects CI/CD environment metadata, SCM (git) information, and Playwright
|
|
8
9
|
* config metadata to attach to each test run submission.
|
|
@@ -47,7 +48,7 @@ class MetadataCollector {
|
|
|
47
48
|
const all = suite.allTests();
|
|
48
49
|
if (all.length > 0) {
|
|
49
50
|
const first = all[0];
|
|
50
|
-
if (first?.parent
|
|
51
|
+
if (first?.parent) {
|
|
51
52
|
const proj = first.parent.project();
|
|
52
53
|
if (proj?.metadata)
|
|
53
54
|
metadata.playwrightProject = proj.metadata;
|
|
@@ -146,23 +147,25 @@ class MetadataCollector {
|
|
|
146
147
|
const scm = {};
|
|
147
148
|
try {
|
|
148
149
|
const execOpts = { encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024 };
|
|
149
|
-
scm.commit = (0,
|
|
150
|
-
scm.branch = (0,
|
|
151
|
-
scm.author = (0,
|
|
152
|
-
scm.commitMessage = (0,
|
|
150
|
+
scm.commit = (0, node_child_process_1.execSync)('git rev-parse HEAD', execOpts).trim();
|
|
151
|
+
scm.branch = (0, node_child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', execOpts).trim();
|
|
152
|
+
scm.author = (0, node_child_process_1.execSync)('git log -1 --pretty=format:"%an"', execOpts).trim();
|
|
153
|
+
scm.commitMessage = (0, node_child_process_1.execSync)('git log -1 --pretty=format:"%s"', execOpts).trim();
|
|
153
154
|
try {
|
|
154
|
-
scm.remoteUrl = (0,
|
|
155
|
+
scm.remoteUrl = (0, node_child_process_1.execSync)('git config --get remote.origin.url', execOpts).trim();
|
|
155
156
|
}
|
|
156
157
|
catch {
|
|
157
158
|
/* optional */
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
catch (error) {
|
|
161
|
-
this.logger.debug(`Git info not available: ${error
|
|
162
|
+
this.logger.debug(`Git info not available: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
162
163
|
}
|
|
163
164
|
return Object.keys(scm).length > 0 ? scm : undefined;
|
|
164
165
|
}
|
|
165
166
|
collectCiInfo() {
|
|
167
|
+
// Env vars are `string | undefined`; undefined values are dropped on JSON
|
|
168
|
+
// serialization, so collecting them directly preserves the prior behavior.
|
|
166
169
|
const ci = {};
|
|
167
170
|
const env = process.env;
|
|
168
171
|
if (env.JENKINS_URL) {
|
|
@@ -14,6 +14,12 @@ export interface FlatStep {
|
|
|
14
14
|
title: string;
|
|
15
15
|
duration: number;
|
|
16
16
|
category: string;
|
|
17
|
+
/** Error message when the step failed (undefined when the step passed). */
|
|
18
|
+
error?: {
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
/** True when the step carried an error — the signal the server needs for inline failure markers. */
|
|
22
|
+
failed?: boolean;
|
|
17
23
|
}
|
|
18
24
|
/** Step-event category restricted to the values `extractTestStepEvents` emits. */
|
|
19
25
|
export type StepEventCategory = 'hook' | 'fixture' | 'test.step' | 'expect' | 'wait';
|
|
@@ -84,11 +84,16 @@ function categorizeStep(title, pwCategory) {
|
|
|
84
84
|
function flattenSteps(steps) {
|
|
85
85
|
const result = [];
|
|
86
86
|
for (const step of steps) {
|
|
87
|
-
|
|
87
|
+
const flat = {
|
|
88
88
|
title: step.title,
|
|
89
89
|
duration: step.duration,
|
|
90
90
|
category: categorizeStep(step.title, step.category),
|
|
91
|
-
}
|
|
91
|
+
};
|
|
92
|
+
if (step.error?.message) {
|
|
93
|
+
flat.error = { message: step.error.message };
|
|
94
|
+
flat.failed = true;
|
|
95
|
+
}
|
|
96
|
+
result.push(flat);
|
|
92
97
|
if (step.steps?.length > 0)
|
|
93
98
|
result.push(...flattenSteps(step.steps));
|
|
94
99
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PiwiDashboardOptions } from '../../public/options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for the `PIWI_*` env-var → option mapping. Both
|
|
4
|
+
* `resolveOptions` (env → option) and `applyOptionsToEnv` (option → env, used
|
|
5
|
+
* by `wrapConfig` to bridge into the global-setup process) read these names so
|
|
6
|
+
* the mapping lives in exactly one place.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PIWI_ENV_KEYS: {
|
|
9
|
+
readonly serverUrl: "PIWI_DASHBOARD_URL";
|
|
10
|
+
readonly projectName: "PIWI_PROJECT_NAME";
|
|
11
|
+
readonly verbose: "PIWI_VERBOSE";
|
|
12
|
+
readonly apiKey: "PIWI_API_KEY";
|
|
13
|
+
readonly username: "PIWI_USERNAME";
|
|
14
|
+
readonly password: "PIWI_PASSWORD";
|
|
15
|
+
readonly environment: "PIWI_ENVIRONMENT";
|
|
16
|
+
readonly label: "PIWI_LABEL";
|
|
17
|
+
readonly runLabel: "PIWI_RUN_LABEL";
|
|
18
|
+
readonly streaming: "PIWI_STREAMING";
|
|
19
|
+
readonly streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE";
|
|
20
|
+
readonly streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY";
|
|
21
|
+
readonly liveFileUploads: "PIWI_LIVE_FILE_UPLOADS";
|
|
22
|
+
readonly uploadTraces: "PIWI_UPLOAD_TRACES";
|
|
23
|
+
readonly uploadReport: "PIWI_UPLOAD_REPORT";
|
|
24
|
+
readonly captureLocators: "PIWI_CAPTURE_LOCATORS";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Merge raw user options with defaults, reading from `PIWI_*` env vars when
|
|
28
|
+
* options are not provided.
|
|
29
|
+
*
|
|
30
|
+
* Env semantics: env vars fill in values the caller didn't provide (fallback).
|
|
31
|
+
* They're applied to `raw` *before* the `DEFAULTS` merge so a built-in default
|
|
32
|
+
* never masks an env var (`PIWI_PROJECT_NAME` would otherwise be masked by the
|
|
33
|
+
* `default-project` default).
|
|
34
|
+
*
|
|
35
|
+
* One preserved quirk: `PIWI_VERBOSE` wins over both the default *and* an
|
|
36
|
+
* explicit user option.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveOptions(raw: Record<string, any>): PiwiDashboardOptions;
|
|
39
|
+
/**
|
|
40
|
+
* Write the options that the isolated `global-setup-module` process needs into
|
|
41
|
+
* `PIWI_*` env vars. `wrapConfig` calls this so the global setup (which runs
|
|
42
|
+
* `resolveOptions({})` in a separate module) picks up the same server/auth
|
|
43
|
+
* config the reporter instance uses. Only writes values that are actually set.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applyOptionsToEnv(options: PiwiDashboardOptions): void;
|
|
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PIWI_ENV_KEYS = void 0;
|
|
4
4
|
exports.resolveOptions = resolveOptions;
|
|
5
5
|
exports.applyOptionsToEnv = applyOptionsToEnv;
|
|
6
|
+
/**
|
|
7
|
+
* Built-in option defaults, merged *under* any user-provided or env-derived
|
|
8
|
+
* value (see `resolveOptions`).
|
|
9
|
+
*/
|
|
6
10
|
const DEFAULTS = {
|
|
7
11
|
projectName: 'default-project',
|
|
8
12
|
uploadTraces: true,
|
|
@@ -11,6 +15,7 @@ const DEFAULTS = {
|
|
|
11
15
|
collectScmInfo: true,
|
|
12
16
|
collectCiInfo: true,
|
|
13
17
|
collectPerformanceMetrics: true,
|
|
18
|
+
captureLocators: true,
|
|
14
19
|
streaming: true,
|
|
15
20
|
streamingBatchSize: 5,
|
|
16
21
|
streamingBatchDelay: 2000,
|
|
@@ -41,57 +46,71 @@ exports.PIWI_ENV_KEYS = {
|
|
|
41
46
|
liveFileUploads: 'PIWI_LIVE_FILE_UPLOADS',
|
|
42
47
|
uploadTraces: 'PIWI_UPLOAD_TRACES',
|
|
43
48
|
uploadReport: 'PIWI_UPLOAD_REPORT',
|
|
49
|
+
captureLocators: 'PIWI_CAPTURE_LOCATORS',
|
|
44
50
|
};
|
|
45
51
|
function readBool(val) {
|
|
46
52
|
if (val === undefined)
|
|
47
53
|
return undefined;
|
|
48
54
|
return val === 'true';
|
|
49
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Declarative env → option fallback table. `resolveOptions` walks this instead
|
|
58
|
+
* of repeating ~15 near-identical merge lines, so adding an env-backed option
|
|
59
|
+
* means adding one row.
|
|
60
|
+
*
|
|
61
|
+
* Guard semantics (preserved from the original hand-written merges):
|
|
62
|
+
* - `string` / `number`: a *truthy* env value fills the option, so an empty
|
|
63
|
+
* string is ignored.
|
|
64
|
+
* - `bool`: any *defined* env value fills it (via `readBool`), so
|
|
65
|
+
* `PIWI_STREAMING=false` actually disables streaming.
|
|
66
|
+
*
|
|
67
|
+
* `verbose` is intentionally absent — it *overrides* rather than falls back, and
|
|
68
|
+
* is applied after the `DEFAULTS` merge in `resolveOptions`.
|
|
69
|
+
*/
|
|
70
|
+
const ENV_FALLBACK_SPECS = [
|
|
71
|
+
{ option: 'serverUrl', env: exports.PIWI_ENV_KEYS.serverUrl, kind: 'string' },
|
|
72
|
+
{ option: 'projectName', env: exports.PIWI_ENV_KEYS.projectName, kind: 'string' },
|
|
73
|
+
{ option: 'apiKey', env: exports.PIWI_ENV_KEYS.apiKey, kind: 'string' },
|
|
74
|
+
{ option: 'username', env: exports.PIWI_ENV_KEYS.username, kind: 'string' },
|
|
75
|
+
{ option: 'password', env: exports.PIWI_ENV_KEYS.password, kind: 'string' },
|
|
76
|
+
{ option: 'environment', env: exports.PIWI_ENV_KEYS.environment, kind: 'string' },
|
|
77
|
+
{ option: 'label', env: exports.PIWI_ENV_KEYS.label, kind: 'string' },
|
|
78
|
+
{ option: 'runLabel', env: exports.PIWI_ENV_KEYS.runLabel, kind: 'string' },
|
|
79
|
+
{ option: 'streaming', env: exports.PIWI_ENV_KEYS.streaming, kind: 'bool' },
|
|
80
|
+
{ option: 'streamingBatchSize', env: exports.PIWI_ENV_KEYS.streamingBatchSize, kind: 'number' },
|
|
81
|
+
{ option: 'streamingBatchDelay', env: exports.PIWI_ENV_KEYS.streamingBatchDelay, kind: 'number' },
|
|
82
|
+
{ option: 'liveFileUploads', env: exports.PIWI_ENV_KEYS.liveFileUploads, kind: 'bool' },
|
|
83
|
+
{ option: 'uploadTraces', env: exports.PIWI_ENV_KEYS.uploadTraces, kind: 'bool' },
|
|
84
|
+
{ option: 'uploadReport', env: exports.PIWI_ENV_KEYS.uploadReport, kind: 'bool' },
|
|
85
|
+
{ option: 'captureLocators', env: exports.PIWI_ENV_KEYS.captureLocators, kind: 'bool' },
|
|
86
|
+
];
|
|
50
87
|
/**
|
|
51
88
|
* Merge raw user options with defaults, reading from `PIWI_*` env vars when
|
|
52
89
|
* options are not provided.
|
|
53
90
|
*
|
|
54
91
|
* Env semantics: env vars fill in values the caller didn't provide (fallback).
|
|
55
92
|
* They're applied to `raw` *before* the `DEFAULTS` merge so a built-in default
|
|
56
|
-
* never masks an env var (
|
|
57
|
-
* `default-project` default
|
|
93
|
+
* never masks an env var (`PIWI_PROJECT_NAME` would otherwise be masked by the
|
|
94
|
+
* `default-project` default).
|
|
58
95
|
*
|
|
59
96
|
* One preserved quirk: `PIWI_VERBOSE` wins over both the default *and* an
|
|
60
|
-
* explicit user option
|
|
97
|
+
* explicit user option.
|
|
61
98
|
*/
|
|
62
99
|
function resolveOptions(raw) {
|
|
63
100
|
const env = process.env;
|
|
64
101
|
const mergedRaw = { ...raw };
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
mergedRaw.environment = env[exports.PIWI_ENV_KEYS.environment];
|
|
78
|
-
if (mergedRaw.label === undefined && env[exports.PIWI_ENV_KEYS.label])
|
|
79
|
-
mergedRaw.label = env[exports.PIWI_ENV_KEYS.label];
|
|
80
|
-
if (mergedRaw.runLabel === undefined && env[exports.PIWI_ENV_KEYS.runLabel])
|
|
81
|
-
mergedRaw.runLabel = env[exports.PIWI_ENV_KEYS.runLabel];
|
|
82
|
-
// Boolean / numeric options: env fills in when the caller didn't provide one.
|
|
83
|
-
if (mergedRaw.streaming === undefined && env[exports.PIWI_ENV_KEYS.streaming] !== undefined)
|
|
84
|
-
mergedRaw.streaming = readBool(env[exports.PIWI_ENV_KEYS.streaming]);
|
|
85
|
-
if (mergedRaw.streamingBatchSize === undefined && env[exports.PIWI_ENV_KEYS.streamingBatchSize])
|
|
86
|
-
mergedRaw.streamingBatchSize = Number(env[exports.PIWI_ENV_KEYS.streamingBatchSize]);
|
|
87
|
-
if (mergedRaw.streamingBatchDelay === undefined && env[exports.PIWI_ENV_KEYS.streamingBatchDelay])
|
|
88
|
-
mergedRaw.streamingBatchDelay = Number(env[exports.PIWI_ENV_KEYS.streamingBatchDelay]);
|
|
89
|
-
if (mergedRaw.liveFileUploads === undefined && env[exports.PIWI_ENV_KEYS.liveFileUploads] !== undefined)
|
|
90
|
-
mergedRaw.liveFileUploads = readBool(env[exports.PIWI_ENV_KEYS.liveFileUploads]);
|
|
91
|
-
if (mergedRaw.uploadTraces === undefined && env[exports.PIWI_ENV_KEYS.uploadTraces] !== undefined)
|
|
92
|
-
mergedRaw.uploadTraces = readBool(env[exports.PIWI_ENV_KEYS.uploadTraces]);
|
|
93
|
-
if (mergedRaw.uploadReport === undefined && env[exports.PIWI_ENV_KEYS.uploadReport] !== undefined)
|
|
94
|
-
mergedRaw.uploadReport = readBool(env[exports.PIWI_ENV_KEYS.uploadReport]);
|
|
102
|
+
for (const spec of ENV_FALLBACK_SPECS) {
|
|
103
|
+
if (mergedRaw[spec.option] !== undefined)
|
|
104
|
+
continue;
|
|
105
|
+
const value = env[spec.env];
|
|
106
|
+
if (spec.kind === 'bool') {
|
|
107
|
+
if (value !== undefined)
|
|
108
|
+
mergedRaw[spec.option] = readBool(value);
|
|
109
|
+
}
|
|
110
|
+
else if (value) {
|
|
111
|
+
mergedRaw[spec.option] = spec.kind === 'number' ? Number(value) : value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
95
114
|
const opts = { ...DEFAULTS, ...mergedRaw };
|
|
96
115
|
// Preserved quirk: PIWI_VERBOSE wins over both default and user option.
|
|
97
116
|
if (env[exports.PIWI_ENV_KEYS.verbose] !== undefined)
|
|
@@ -124,4 +143,11 @@ function applyOptionsToEnv(options) {
|
|
|
124
143
|
env[exports.PIWI_ENV_KEYS.label] = options.label;
|
|
125
144
|
if (options.runLabel)
|
|
126
145
|
env[exports.PIWI_ENV_KEYS.runLabel] = options.runLabel;
|
|
146
|
+
// Locator capture is part of performance-metric collection; switch it off in
|
|
147
|
+
// the worker when either flag is disabled so the fixture skips the per-action
|
|
148
|
+
// cost. Only an explicit `true` overrides the unset (default-on) state.
|
|
149
|
+
if (options.captureLocators === false || options.collectPerformanceMetrics === false)
|
|
150
|
+
env[exports.PIWI_ENV_KEYS.captureLocators] = 'false';
|
|
151
|
+
else if (options.captureLocators === true)
|
|
152
|
+
env[exports.PIWI_ENV_KEYS.captureLocators] = 'true';
|
|
127
153
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.compressDirectory = compressDirectory;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const zlib = __importStar(require("node:zlib"));
|
|
40
|
+
const node_util_1 = require("node:util");
|
|
41
|
+
const gzipAsync = (0, node_util_1.promisify)(zlib.gzip);
|
|
42
|
+
/**
|
|
43
|
+
* Recursively read a directory and pack all files into a gzip-compressed
|
|
44
|
+
* archive (a concatenation of length-prefixed path+content pairs).
|
|
45
|
+
*/
|
|
46
|
+
async function compressDirectory(sourceDir) {
|
|
47
|
+
const files = [];
|
|
48
|
+
function collect(dir, baseDir = '') {
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const full = path.join(dir, entry.name);
|
|
51
|
+
const rel = path.join(baseDir, entry.name);
|
|
52
|
+
if (entry.isDirectory())
|
|
53
|
+
collect(full, rel);
|
|
54
|
+
else if (entry.isFile())
|
|
55
|
+
files.push({ path: rel, content: fs.readFileSync(full) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
collect(sourceDir);
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const f of files) {
|
|
61
|
+
const pb = Buffer.from(f.path, 'utf8');
|
|
62
|
+
const plb = Buffer.allocUnsafe(4);
|
|
63
|
+
plb.writeUInt32LE(pb.length, 0);
|
|
64
|
+
const clb = Buffer.allocUnsafe(4);
|
|
65
|
+
clb.writeUInt32LE(f.content.length, 0);
|
|
66
|
+
parts.push(plb, pb, clb, f.content);
|
|
67
|
+
}
|
|
68
|
+
return await gzipAsync(Buffer.concat(parts), { level: 5 });
|
|
69
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Logger } from '
|
|
2
|
-
import type { CollectedTestCase, RawAttachment, TraceHashInfo } from '
|
|
1
|
+
import { Logger } from '../support/logger.js';
|
|
2
|
+
import type { CollectedTestCase, RawAttachment, TraceHashInfo } from '../../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* File-system helpers for discovering report directories, trace files, and
|
|
5
5
|
* test attachments, as well as compressing report directories and computing
|
|
@@ -16,7 +16,7 @@ export declare class FileHandler {
|
|
|
16
16
|
compressReportDirectory(reportDir: string): Promise<Buffer | null>;
|
|
17
17
|
/** Return resolved paths for all trace attachments on a test case */
|
|
18
18
|
findTraceFiles(testCase: CollectedTestCase): string[];
|
|
19
|
-
/** Return all non-trace, non-internal attachments from a test case. Skips `trace` and `piwi
|
|
19
|
+
/** Return all non-trace, non-internal attachments from a test case. Skips `trace` and `piwi-*` attachments. */
|
|
20
20
|
findAllAttachments(testCase: CollectedTestCase): Array<{
|
|
21
21
|
name: string;
|
|
22
22
|
path: string;
|
|
@@ -25,7 +25,7 @@ export declare class FileHandler {
|
|
|
25
25
|
}>;
|
|
26
26
|
/** Mapping of well-known report type names to their default output directories */
|
|
27
27
|
getDefaultReportDirs(): Record<string, string>;
|
|
28
|
-
/** Parse Piwi-internal attachment bodies (`piwi-
|
|
28
|
+
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`) into structured fields on the test case */
|
|
29
29
|
parsePerformanceAttachments(testCase: CollectedTestCase, attachments: RawAttachment[]): void;
|
|
30
30
|
/** Compute SHA-256 hash and size for a single test case's trace file. Returns `null` when the case has no trace on disk. */
|
|
31
31
|
computeSingleTraceHash(testCase: CollectedTestCase): Promise<TraceHashInfo | null>;
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
5
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
36
|
exports.FileHandler = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const errors_js_1 = require("../support/errors.js");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
10
41
|
const compression_js_1 = require("./compression.js");
|
|
11
|
-
const logger_js_1 = require("
|
|
42
|
+
const logger_js_1 = require("../support/logger.js");
|
|
43
|
+
const attachments_js_1 = require("../capture/attachments.js");
|
|
12
44
|
/**
|
|
13
45
|
* File-system helpers for discovering report directories, trace files, and
|
|
14
46
|
* test attachments, as well as compressing report directories and computing
|
|
@@ -21,11 +53,11 @@ class FileHandler {
|
|
|
21
53
|
/** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
|
|
22
54
|
findHTMLReportDirectory(customDir) {
|
|
23
55
|
const possibleDirs = customDir
|
|
24
|
-
? [customDir,
|
|
25
|
-
: ['playwright-report', './playwright-report',
|
|
56
|
+
? [customDir, path.join(process.cwd(), customDir)]
|
|
57
|
+
: ['playwright-report', './playwright-report', path.join(process.cwd(), 'playwright-report')];
|
|
26
58
|
for (const reportDir of possibleDirs) {
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
59
|
+
if (fs.existsSync(reportDir) && fs.statSync(reportDir).isDirectory()) {
|
|
60
|
+
if (fs.existsSync(path.join(reportDir, 'index.html')))
|
|
29
61
|
return reportDir;
|
|
30
62
|
}
|
|
31
63
|
}
|
|
@@ -33,9 +65,9 @@ class FileHandler {
|
|
|
33
65
|
}
|
|
34
66
|
/** Check if `dir` exists and is a directory; also checks `path.join(process.cwd(), dir)` */
|
|
35
67
|
findReportDirectory(dir) {
|
|
36
|
-
const candidates = [dir,
|
|
68
|
+
const candidates = [dir, path.join(process.cwd(), dir)];
|
|
37
69
|
for (const candidate of candidates) {
|
|
38
|
-
if (
|
|
70
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
|
|
39
71
|
return candidate;
|
|
40
72
|
}
|
|
41
73
|
return null;
|
|
@@ -46,7 +78,7 @@ class FileHandler {
|
|
|
46
78
|
return (await (0, compression_js_1.compressDirectory)(reportDir)) || null;
|
|
47
79
|
}
|
|
48
80
|
catch (error) {
|
|
49
|
-
this.logger.warn(`Failed to compress report directory ${reportDir}: ${error
|
|
81
|
+
this.logger.warn(`Failed to compress report directory ${reportDir}: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
50
82
|
return null;
|
|
51
83
|
}
|
|
52
84
|
}
|
|
@@ -56,26 +88,26 @@ class FileHandler {
|
|
|
56
88
|
if (testCase.attachments) {
|
|
57
89
|
for (const a of testCase.attachments) {
|
|
58
90
|
if (a.name === 'trace' && a.path)
|
|
59
|
-
set.add(
|
|
91
|
+
set.add(path.resolve(a.path));
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
94
|
return Array.from(set);
|
|
63
95
|
}
|
|
64
|
-
/** Return all non-trace, non-internal attachments from a test case. Skips `trace` and `piwi
|
|
96
|
+
/** Return all non-trace, non-internal attachments from a test case. Skips `trace` and `piwi-*` attachments. */
|
|
65
97
|
findAllAttachments(testCase) {
|
|
66
98
|
const result = [];
|
|
67
99
|
if (testCase.attachments) {
|
|
68
100
|
for (const a of testCase.attachments) {
|
|
69
101
|
if (a.name === 'trace')
|
|
70
102
|
continue;
|
|
71
|
-
if (a.name
|
|
103
|
+
if (a.name && attachments_js_1.INTERNAL_ATTACHMENT_NAMES.has(a.name))
|
|
72
104
|
continue;
|
|
73
|
-
if (a.path &&
|
|
105
|
+
if (a.path && fs.existsSync(a.path)) {
|
|
74
106
|
result.push({
|
|
75
107
|
name: a.name || 'attachment',
|
|
76
|
-
path:
|
|
108
|
+
path: path.resolve(a.path),
|
|
77
109
|
contentType: a.contentType || 'application/octet-stream',
|
|
78
|
-
originalName:
|
|
110
|
+
originalName: path.basename(a.path),
|
|
79
111
|
});
|
|
80
112
|
}
|
|
81
113
|
}
|
|
@@ -91,10 +123,10 @@ class FileHandler {
|
|
|
91
123
|
blob: 'blob-report',
|
|
92
124
|
};
|
|
93
125
|
}
|
|
94
|
-
/** Parse Piwi-internal attachment bodies (`piwi-
|
|
126
|
+
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`) into structured fields on the test case */
|
|
95
127
|
parsePerformanceAttachments(testCase, attachments) {
|
|
96
128
|
const find = (name) => attachments.find((a) => a.name === name);
|
|
97
|
-
const net = find(
|
|
129
|
+
const net = find(attachments_js_1.ATTACHMENT_NAMES.network);
|
|
98
130
|
if (net?.body) {
|
|
99
131
|
try {
|
|
100
132
|
testCase.networkRequests = JSON.parse(net.body.toString());
|
|
@@ -103,7 +135,7 @@ class FileHandler {
|
|
|
103
135
|
/* ignore */
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
|
-
const vitals = find(
|
|
138
|
+
const vitals = find(attachments_js_1.ATTACHMENT_NAMES.webVitals);
|
|
107
139
|
if (vitals?.body) {
|
|
108
140
|
try {
|
|
109
141
|
testCase.webVitals = JSON.parse(vitals.body.toString());
|
|
@@ -112,7 +144,7 @@ class FileHandler {
|
|
|
112
144
|
/* ignore */
|
|
113
145
|
}
|
|
114
146
|
}
|
|
115
|
-
const consoleLog = find(
|
|
147
|
+
const consoleLog = find(attachments_js_1.ATTACHMENT_NAMES.console);
|
|
116
148
|
if (consoleLog?.body) {
|
|
117
149
|
try {
|
|
118
150
|
testCase.consoleLogs = JSON.parse(consoleLog.body.toString());
|
|
@@ -121,7 +153,7 @@ class FileHandler {
|
|
|
121
153
|
/* ignore */
|
|
122
154
|
}
|
|
123
155
|
}
|
|
124
|
-
const aria = find(
|
|
156
|
+
const aria = find(attachments_js_1.ATTACHMENT_NAMES.ariaSnapshot);
|
|
125
157
|
if (aria?.body)
|
|
126
158
|
testCase.ariaSnapshot = aria.body.toString();
|
|
127
159
|
}
|
|
@@ -130,14 +162,14 @@ class FileHandler {
|
|
|
130
162
|
const tracePaths = this.findTraceFiles(testCase);
|
|
131
163
|
let lastPath = null;
|
|
132
164
|
for (const tp of tracePaths) {
|
|
133
|
-
if (
|
|
165
|
+
if (fs.existsSync(tp))
|
|
134
166
|
lastPath = tp;
|
|
135
167
|
}
|
|
136
168
|
if (!lastPath)
|
|
137
169
|
return null;
|
|
138
|
-
const hash =
|
|
170
|
+
const hash = crypto.createHash('sha256');
|
|
139
171
|
await new Promise((resolve, reject) => {
|
|
140
|
-
|
|
172
|
+
fs.createReadStream(lastPath)
|
|
141
173
|
.on('data', (chunk) => hash.update(chunk))
|
|
142
174
|
.on('end', resolve)
|
|
143
175
|
.on('error', reject);
|
|
@@ -145,7 +177,7 @@ class FileHandler {
|
|
|
145
177
|
return {
|
|
146
178
|
tracePath: lastPath,
|
|
147
179
|
hash: hash.digest('hex'),
|
|
148
|
-
size:
|
|
180
|
+
size: fs.statSync(lastPath).size,
|
|
149
181
|
};
|
|
150
182
|
}
|
|
151
183
|
/** Ask the server which trace hashes it already has, returning the set of hashes that are missing */
|