@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,146 @@
|
|
|
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.createGlobalSetup = createGlobalSetup;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const errors_js_1 = require("../internal/support/errors.js");
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const env_js_1 = require("../internal/config/env.js");
|
|
41
|
+
const http_client_js_1 = require("../internal/transport/http-client.js");
|
|
42
|
+
const logger_js_1 = require("../internal/support/logger.js");
|
|
43
|
+
const instance_id_js_1 = require("../internal/support/instance-id.js");
|
|
44
|
+
const ci_js_1 = require("../internal/support/ci.js");
|
|
45
|
+
const setup_file_js_1 = require("../internal/support/setup-file.js");
|
|
46
|
+
/**
|
|
47
|
+
* Create a Playwright `globalSetup` function that registers a test run on the
|
|
48
|
+
* Piwi Dashboard server at the start of the run. An optional `userSetup` is
|
|
49
|
+
* called afterward so existing setup logic is preserved.
|
|
50
|
+
*
|
|
51
|
+
* The server-run ID and a one-time token are written to a temp file so the
|
|
52
|
+
* reporter instance can pick them up during the streaming handshake.
|
|
53
|
+
*
|
|
54
|
+
* @param options Piwi Dashboard options (uses `serverUrl`, `projectName`, …).
|
|
55
|
+
* @param userSetup An existing global setup to chain after the Piwi registration.
|
|
56
|
+
*/
|
|
57
|
+
function createGlobalSetup(options, userSetup) {
|
|
58
|
+
return async function globalSetupFn(config) {
|
|
59
|
+
// This module compiles to `dist/public/global-setup.js`; the package entry
|
|
60
|
+
// point is `dist/index.js`, one level up.
|
|
61
|
+
const piwiReporterPath = path.resolve(__dirname, '../index.js');
|
|
62
|
+
// Extract options from the Piwi reporter entry in the Playwright config so
|
|
63
|
+
// that serverUrl / projectName etc. set inline in the reporters array are
|
|
64
|
+
// visible here without requiring PIWI_* env vars or a separate wrapConfig call.
|
|
65
|
+
let inlineReporterOptions = {};
|
|
66
|
+
if (Array.isArray(config?.reporter)) {
|
|
67
|
+
for (const r of config.reporter) {
|
|
68
|
+
if (!Array.isArray(r) || typeof r[0] !== 'string')
|
|
69
|
+
continue;
|
|
70
|
+
const isPiwi = r[0].toLowerCase().includes('piwi') ||
|
|
71
|
+
(() => {
|
|
72
|
+
try {
|
|
73
|
+
return path.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
if (isPiwi && r[1] && typeof r[1] === 'object') {
|
|
80
|
+
inlineReporterOptions = r[1];
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const opts = (0, env_js_1.resolveOptions)({ ...inlineReporterOptions, ...options });
|
|
86
|
+
const logger = new logger_js_1.Logger(opts.verbose ?? false);
|
|
87
|
+
if (opts.enabled === false || !opts.serverUrl) {
|
|
88
|
+
logger.info('Not enabled — set PIWI_DASHBOARD_URL or serverUrl to enable.');
|
|
89
|
+
if (userSetup)
|
|
90
|
+
return userSetup(config);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const hasPiwi = Object.keys(inlineReporterOptions).length > 0 ||
|
|
94
|
+
(Array.isArray(config?.reporter) &&
|
|
95
|
+
config.reporter.some((r) => {
|
|
96
|
+
if (!Array.isArray(r) || typeof r[0] !== 'string')
|
|
97
|
+
return false;
|
|
98
|
+
if (r[0].toLowerCase().includes('piwi'))
|
|
99
|
+
return true;
|
|
100
|
+
try {
|
|
101
|
+
return path.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
if (!hasPiwi) {
|
|
108
|
+
logger.debug('Not reporting — Piwi is not in the Playwright reporters list.');
|
|
109
|
+
if (userSetup)
|
|
110
|
+
return userSetup(config);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const httpClient = new http_client_js_1.HttpClient(opts.serverUrl, logger);
|
|
114
|
+
try {
|
|
115
|
+
const auth = await httpClient.resolveAuth(opts);
|
|
116
|
+
const runLabel = opts.runLabel || (0, ci_js_1.detectCiRunLabel)();
|
|
117
|
+
// Detect shard info from Playwright config (--shard=1/3)
|
|
118
|
+
const pwShard = config.shard;
|
|
119
|
+
const shardIndex = pwShard?.current;
|
|
120
|
+
const shardTotal = pwShard?.total;
|
|
121
|
+
const response = await httpClient.postJSON('/api/test-runs/setup', {
|
|
122
|
+
projectName: opts.projectName,
|
|
123
|
+
projectDescription: opts.projectDescription,
|
|
124
|
+
environment: opts.environment || null,
|
|
125
|
+
label: opts.label || null,
|
|
126
|
+
startTime: new Date().toISOString(),
|
|
127
|
+
instanceId: (0, instance_id_js_1.computeInstanceId)(opts.projectName, runLabel),
|
|
128
|
+
shardIndex,
|
|
129
|
+
shardTotal,
|
|
130
|
+
}, auth);
|
|
131
|
+
if (response?.runId && response?.setupToken) {
|
|
132
|
+
fs.writeFileSync((0, setup_file_js_1.getSetupFilePath)(opts.projectName), JSON.stringify({
|
|
133
|
+
runId: response.runId,
|
|
134
|
+
setupToken: response.setupToken,
|
|
135
|
+
projectName: opts.projectName,
|
|
136
|
+
}));
|
|
137
|
+
logger.debug(`Global setup: initialising run #${response.runId}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.warn(`Could not register global setup: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
142
|
+
}
|
|
143
|
+
if (userSetup)
|
|
144
|
+
return userSetup(config);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { PlaywrightTestConfig } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
export type { PlaywrightTestConfig } from '@playwright/test';
|
|
3
|
+
/**
|
|
4
|
+
* Playwright shard info — mirrors `config.shard` shape.
|
|
5
|
+
*
|
|
6
|
+
* Internal shape: it is NOT re-exported from the package entry point, but it
|
|
7
|
+
* lives here next to the configuration types because it describes the same
|
|
8
|
+
* `config.shard` surface the options come from. May move to `types/wire.ts`.
|
|
9
|
+
*/
|
|
3
10
|
export interface ShardInfo {
|
|
4
11
|
current: number;
|
|
5
12
|
total: number;
|
|
6
13
|
}
|
|
7
14
|
/** Options for configuring the Piwi Dashboard reporter */
|
|
8
15
|
export interface PiwiDashboardOptions extends PlaywrightTestConfig {
|
|
16
|
+
/** Explicitly enable or disable the reporter. Defaults to `true` when `serverUrl` is set. Set to `false` to disable even if `serverUrl` is provided. */
|
|
17
|
+
enabled?: boolean;
|
|
9
18
|
/** URL of the Piwi Dashboard server */
|
|
10
19
|
serverUrl?: string;
|
|
11
20
|
/** Name of the project to report results under. Defaults to `'default-project'`. */
|
|
@@ -24,6 +33,14 @@ export interface PiwiDashboardOptions extends PlaywrightTestConfig {
|
|
|
24
33
|
collectCiInfo?: boolean;
|
|
25
34
|
/** Collect step timings, network requests and web vitals. Defaults to `true`. */
|
|
26
35
|
collectPerformanceMetrics?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Capture per-action locator snapshots that power failure-time healing
|
|
38
|
+
* suggestions. Adds a small per-action cost (one DOM read, sometimes an ARIA
|
|
39
|
+
* snapshot) in the test worker. Defaults to `true`; automatically disabled
|
|
40
|
+
* when `collectPerformanceMetrics` is `false` (the reporter discards the data
|
|
41
|
+
* in that case anyway). Can also be forced off with `PIWI_CAPTURE_LOCATORS=false`.
|
|
42
|
+
*/
|
|
43
|
+
captureLocators?: boolean;
|
|
27
44
|
/** Enable live streaming of results (falls back to batch if unsupported). Defaults to `true`. */
|
|
28
45
|
streaming?: boolean;
|
|
29
46
|
/** Number of test results to batch before sending during streaming. Defaults to `5`. */
|
|
@@ -59,46 +76,3 @@ export interface PiwiDashboardOptions extends PlaywrightTestConfig {
|
|
|
59
76
|
/** Enable verbose logging for debugging. Defaults to `false`. */
|
|
60
77
|
verbose?: boolean;
|
|
61
78
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Single source of truth for the `PIWI_*` env-var → option mapping. Both
|
|
64
|
-
* `resolveOptions` (env → option) and `applyOptionsToEnv` (option → env, used
|
|
65
|
-
* by `wrapConfig` to bridge into the global-setup process) read these names so
|
|
66
|
-
* the mapping lives in exactly one place.
|
|
67
|
-
*/
|
|
68
|
-
export declare const PIWI_ENV_KEYS: {
|
|
69
|
-
readonly serverUrl: "PIWI_DASHBOARD_URL";
|
|
70
|
-
readonly projectName: "PIWI_PROJECT_NAME";
|
|
71
|
-
readonly verbose: "PIWI_VERBOSE";
|
|
72
|
-
readonly apiKey: "PIWI_API_KEY";
|
|
73
|
-
readonly username: "PIWI_USERNAME";
|
|
74
|
-
readonly password: "PIWI_PASSWORD";
|
|
75
|
-
readonly environment: "PIWI_ENVIRONMENT";
|
|
76
|
-
readonly label: "PIWI_LABEL";
|
|
77
|
-
readonly runLabel: "PIWI_RUN_LABEL";
|
|
78
|
-
readonly streaming: "PIWI_STREAMING";
|
|
79
|
-
readonly streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE";
|
|
80
|
-
readonly streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY";
|
|
81
|
-
readonly liveFileUploads: "PIWI_LIVE_FILE_UPLOADS";
|
|
82
|
-
readonly uploadTraces: "PIWI_UPLOAD_TRACES";
|
|
83
|
-
readonly uploadReport: "PIWI_UPLOAD_REPORT";
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* Merge raw user options with defaults, reading from `PIWI_*` env vars when
|
|
87
|
-
* options are not provided.
|
|
88
|
-
*
|
|
89
|
-
* Env semantics: env vars fill in values the caller didn't provide (fallback).
|
|
90
|
-
* They're applied to `raw` *before* the `DEFAULTS` merge so a built-in default
|
|
91
|
-
* never masks an env var (the pre-Phase-4 `PIWI_PROJECT_NAME` was masked by the
|
|
92
|
-
* `default-project` default — that quirk is now fixed).
|
|
93
|
-
*
|
|
94
|
-
* One preserved quirk: `PIWI_VERBOSE` wins over both the default *and* an
|
|
95
|
-
* explicit user option, matching the pre-Phase-4 behavior.
|
|
96
|
-
*/
|
|
97
|
-
export declare function resolveOptions(raw: Record<string, any>): PiwiDashboardOptions;
|
|
98
|
-
/**
|
|
99
|
-
* Write the options that the isolated `global-setup-module` process needs into
|
|
100
|
-
* `PIWI_*` env vars. `wrapConfig` calls this so the global setup (which runs
|
|
101
|
-
* `resolveOptions({})` in a separate module) picks up the same server/auth
|
|
102
|
-
* config the reporter instance uses. Only writes values that are actually set.
|
|
103
|
-
*/
|
|
104
|
-
export declare function applyOptionsToEnv(options: PiwiDashboardOptions): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
|
-
import { createGlobalSetup } from './
|
|
2
|
+
import { createGlobalSetup } from './global-setup.js';
|
|
3
|
+
import { wrapConfig } from './config-wrapper.js';
|
|
3
4
|
/**
|
|
4
5
|
* Piwi Dashboard Playwright reporter.
|
|
5
6
|
*
|
|
@@ -38,6 +39,7 @@ export declare class PiwiDashboardReporter {
|
|
|
38
39
|
private recovery;
|
|
39
40
|
private submitter;
|
|
40
41
|
private readonly logger;
|
|
42
|
+
static wrapConfig: typeof wrapConfig;
|
|
41
43
|
static createGlobalSetup: typeof createGlobalSetup;
|
|
42
44
|
constructor(rawOptions?: Record<string, any>);
|
|
43
45
|
/** Playwright reporter hook: called once at the start of the test run */
|
|
@@ -34,21 +34,38 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.PiwiDashboardReporter = void 0;
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const
|
|
39
|
-
const http_client_js_1 = require("
|
|
40
|
-
const uploader_js_1 = require("
|
|
41
|
-
const stream_buffer_js_1 = require("
|
|
42
|
-
const crash_recovery_js_1 = require("
|
|
43
|
-
const file_handler_js_1 = require("
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const env_js_1 = require("../internal/config/env.js");
|
|
39
|
+
const http_client_js_1 = require("../internal/transport/http-client.js");
|
|
40
|
+
const uploader_js_1 = require("../internal/submit/uploader.js");
|
|
41
|
+
const stream_buffer_js_1 = require("../internal/streaming/stream-buffer.js");
|
|
42
|
+
const crash_recovery_js_1 = require("../internal/streaming/crash-recovery.js");
|
|
43
|
+
const file_handler_js_1 = require("../internal/files/file-handler.js");
|
|
44
|
+
const attachments_js_1 = require("../internal/capture/attachments.js");
|
|
45
|
+
const metadata_collector_js_1 = require("../internal/collect/metadata-collector.js");
|
|
46
|
+
const stream_manager_js_1 = require("../internal/streaming/stream-manager.js");
|
|
47
|
+
const step_analyzer_js_1 = require("../internal/collect/step-analyzer.js");
|
|
48
|
+
const instance_id_js_1 = require("../internal/support/instance-id.js");
|
|
49
|
+
const source_snippet_js_1 = require("../internal/support/source-snippet.js");
|
|
50
|
+
const ci_js_1 = require("../internal/support/ci.js");
|
|
51
|
+
const worker_index_js_1 = require("../internal/support/worker-index.js");
|
|
52
|
+
const cli_filters_js_1 = require("../internal/support/cli-filters.js");
|
|
53
|
+
const global_setup_js_1 = require("./global-setup.js");
|
|
54
|
+
const config_wrapper_js_1 = require("./config-wrapper.js");
|
|
55
|
+
const serializer_js_1 = require("../internal/submit/serializer.js");
|
|
56
|
+
const skip_classify_js_1 = require("../internal/collect/skip-classify.js");
|
|
57
|
+
const error_text_js_1 = require("../internal/collect/error-text.js");
|
|
58
|
+
const run_submitter_js_1 = require("../internal/submit/run-submitter.js");
|
|
59
|
+
const logger_js_1 = require("../internal/support/logger.js");
|
|
60
|
+
/**
|
|
61
|
+
* Relative `file:line:column` location string for a test, normalized to POSIX
|
|
62
|
+
* path separators so Playwright's CLI file filter (and the dashboard's copyable
|
|
63
|
+
* retry command) match on every platform — `path.relative` yields backslashes on Windows.
|
|
64
|
+
*/
|
|
65
|
+
function testLocation(test) {
|
|
66
|
+
const relativeFilePath = path.relative(process.cwd(), test.location.file).split(path.sep).join('/');
|
|
67
|
+
return `${relativeFilePath}:${test.location.line}:${test.location.column}`;
|
|
68
|
+
}
|
|
52
69
|
/**
|
|
53
70
|
* Piwi Dashboard Playwright reporter.
|
|
54
71
|
*
|
|
@@ -80,10 +97,10 @@ class PiwiDashboardReporter {
|
|
|
80
97
|
this.streamManager = null;
|
|
81
98
|
/** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
|
|
82
99
|
this.setupSteps = [];
|
|
83
|
-
this.options = (0,
|
|
84
|
-
this.enabled = !!this.options.serverUrl;
|
|
85
|
-
this.runLabel = this.options.runLabel || (0,
|
|
86
|
-
this.instanceId = (0,
|
|
100
|
+
this.options = (0, env_js_1.resolveOptions)(rawOptions);
|
|
101
|
+
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
102
|
+
this.runLabel = this.options.runLabel || (0, ci_js_1.detectCiRunLabel)();
|
|
103
|
+
this.instanceId = (0, instance_id_js_1.computeInstanceId)(this.options.projectName, this.runLabel);
|
|
87
104
|
const logger = new logger_js_1.Logger(this.options.verbose ?? false);
|
|
88
105
|
this.logger = logger;
|
|
89
106
|
this.httpClient = new http_client_js_1.HttpClient(this.options.serverUrl ?? 'http://localhost:3000', logger);
|
|
@@ -115,7 +132,7 @@ class PiwiDashboardReporter {
|
|
|
115
132
|
const grep = grepRe && grepRe.source !== '.*' ? grepRe.source : undefined;
|
|
116
133
|
const grepInvert = grepInvertRe?.source;
|
|
117
134
|
// File/path filters come from the CLI invocation, not config.grep.
|
|
118
|
-
const fileFilters = (0,
|
|
135
|
+
const fileFilters = (0, cli_filters_js_1.detectCliFileFilters)();
|
|
119
136
|
if (grep || grepInvert || fileFilters.length > 0) {
|
|
120
137
|
this.isFullRun = false;
|
|
121
138
|
this.filterDetails = {
|
|
@@ -141,13 +158,12 @@ class PiwiDashboardReporter {
|
|
|
141
158
|
}
|
|
142
159
|
/** Playwright reporter hook: called when an individual test begins */
|
|
143
160
|
onTestBegin(test, result) {
|
|
144
|
-
const relativeFilePath = path.relative(process.cwd(), test.location.file);
|
|
145
161
|
const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
|
|
146
162
|
const beginEvent = {
|
|
147
163
|
type: 'begin',
|
|
148
164
|
title: test.title,
|
|
149
|
-
location:
|
|
150
|
-
workerIndex: (0,
|
|
165
|
+
location: testLocation(test),
|
|
166
|
+
workerIndex: (0, worker_index_js_1.workerIndexOf)(result),
|
|
151
167
|
shardIndex: this.shardInfo?.current ?? null,
|
|
152
168
|
browser: this.metadataCollector.getBrowserConfig(test) || undefined,
|
|
153
169
|
suitePath,
|
|
@@ -170,19 +186,21 @@ class PiwiDashboardReporter {
|
|
|
170
186
|
location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : 'unknown',
|
|
171
187
|
stepCategory: cat,
|
|
172
188
|
parentTitle: test?.title || null,
|
|
173
|
-
workerIndex: (0,
|
|
189
|
+
workerIndex: (0, worker_index_js_1.workerIndexOf)(_result),
|
|
174
190
|
startedAt: step.startTime instanceof Date ? step.startTime.getTime() : null,
|
|
175
191
|
};
|
|
176
192
|
this.streamManager?.queueBeginEvent(event);
|
|
177
193
|
}
|
|
178
194
|
/** Playwright reporter hook: called when a step (including hook/fixture) ends */
|
|
179
195
|
onStepEnd(test, _result, step) {
|
|
196
|
+
const cat = step.category;
|
|
197
|
+
if (cat === 'pw:api')
|
|
198
|
+
return; // not surfaced as stream events; locator locations are captured in the fixture
|
|
180
199
|
if (!this.enabled || !this.streamManager?.enabled)
|
|
181
200
|
return;
|
|
182
|
-
const cat = step.category;
|
|
183
201
|
if (cat !== 'hook' && cat !== 'fixture')
|
|
184
202
|
return;
|
|
185
|
-
const workerIndex = (0,
|
|
203
|
+
const workerIndex = (0, worker_index_js_1.workerIndexOf)(_result);
|
|
186
204
|
const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
|
|
187
205
|
const event = {
|
|
188
206
|
type: 'step-end',
|
|
@@ -212,7 +230,6 @@ class PiwiDashboardReporter {
|
|
|
212
230
|
/** Playwright reporter hook: called when an individual test finishes */
|
|
213
231
|
onTestEnd(test, result) {
|
|
214
232
|
this.totalTests++;
|
|
215
|
-
const relativeFilePath = path.relative(process.cwd(), test.location.file);
|
|
216
233
|
this.reportedTestIds.add(test.id);
|
|
217
234
|
const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
|
|
218
235
|
const annotations = (0, skip_classify_js_1.mergeAnnotations)(test, result);
|
|
@@ -220,12 +237,12 @@ class PiwiDashboardReporter {
|
|
|
220
237
|
const testCase = {
|
|
221
238
|
type: 'complete',
|
|
222
239
|
title: test.title,
|
|
223
|
-
location:
|
|
240
|
+
location: testLocation(test),
|
|
224
241
|
status,
|
|
225
242
|
duration: result.duration,
|
|
226
|
-
error:
|
|
243
|
+
error: (0, error_text_js_1.buildErrorText)(result),
|
|
227
244
|
retries: result.retry,
|
|
228
|
-
workerIndex: (0,
|
|
245
|
+
workerIndex: (0, worker_index_js_1.workerIndexOf)(result),
|
|
229
246
|
shardIndex: this.shardInfo?.current ?? null,
|
|
230
247
|
startedAt: result.startTime ? result.startTime.getTime() : null,
|
|
231
248
|
attachments: result.attachments || [],
|
|
@@ -235,7 +252,8 @@ class PiwiDashboardReporter {
|
|
|
235
252
|
testAnnotations: annotations.length ? annotations : null,
|
|
236
253
|
};
|
|
237
254
|
if (result.status === 'failed' || result.status === 'timedOut') {
|
|
238
|
-
const
|
|
255
|
+
const failingLine = (0, source_snippet_js_1.extractFailingLine)(testCase.error, test.location.file, test.location.line);
|
|
256
|
+
const snippet = (0, source_snippet_js_1.readSourceSnippet)(test.location.file, test.location.line, 30, failingLine);
|
|
239
257
|
if (snippet)
|
|
240
258
|
testCase.testSource = snippet;
|
|
241
259
|
}
|
|
@@ -249,6 +267,20 @@ class PiwiDashboardReporter {
|
|
|
249
267
|
}
|
|
250
268
|
if (this.options.collectPerformanceMetrics && result.attachments) {
|
|
251
269
|
this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
|
|
270
|
+
// Locator snapshots arrive pre-stamped with their call-site `location`
|
|
271
|
+
// (captured in the fixture at action call time). No index correlation
|
|
272
|
+
// with pw:api steps — that was unreliable across workers/concurrent calls.
|
|
273
|
+
const locatorAttachment = this.options.captureLocators !== false
|
|
274
|
+
? result.attachments.find((a) => a.name === attachments_js_1.ATTACHMENT_NAMES.locators)
|
|
275
|
+
: undefined;
|
|
276
|
+
if (locatorAttachment?.body) {
|
|
277
|
+
try {
|
|
278
|
+
testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
/* ignore parse errors */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
252
284
|
}
|
|
253
285
|
switch (status) {
|
|
254
286
|
case 'passed':
|
|
@@ -285,12 +317,11 @@ class PiwiDashboardReporter {
|
|
|
285
317
|
for (const test of this.plannedTests) {
|
|
286
318
|
if (this.reportedTestIds.has(test.id))
|
|
287
319
|
continue;
|
|
288
|
-
const relativeFilePath = path.relative(process.cwd(), test.location.file);
|
|
289
320
|
const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
|
|
290
321
|
const testCase = {
|
|
291
322
|
type: 'complete',
|
|
292
323
|
title: test.title,
|
|
293
|
-
location:
|
|
324
|
+
location: testLocation(test),
|
|
294
325
|
status: 'didnotrun',
|
|
295
326
|
duration: 0,
|
|
296
327
|
error: null,
|
|
@@ -338,4 +369,5 @@ class PiwiDashboardReporter {
|
|
|
338
369
|
}
|
|
339
370
|
}
|
|
340
371
|
exports.PiwiDashboardReporter = PiwiDashboardReporter;
|
|
341
|
-
PiwiDashboardReporter.
|
|
372
|
+
PiwiDashboardReporter.wrapConfig = config_wrapper_js_1.wrapConfig;
|
|
373
|
+
PiwiDashboardReporter.createGlobalSetup = global_setup_js_1.createGlobalSetup;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal collection model — the shapes the reporter accumulates *in process*
|
|
3
|
+
* while a run executes.
|
|
4
|
+
*
|
|
5
|
+
* These are NOT sent to the server verbatim: the wire projection happens in
|
|
6
|
+
* `serializer.ts` (`toWireTestCase` / `serializeRun`) at the JSON boundary. For
|
|
7
|
+
* the external server contract, see `./wire.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import type { BrowserConfig, SuiteConfigEntry, TestAnnotation, TestStepEvent } from './wire.js';
|
|
10
|
+
import type { LocatorSnapshot } from '../internal/capture/locator-healing.js';
|
|
11
|
+
/**
|
|
12
|
+
* A raw Playwright attachment as exposed on `TestResult.attachments`.
|
|
13
|
+
* Carried verbatim on `CollectedTestCase.attachments` so `FileHandler` can
|
|
14
|
+
* resolve trace/attachment paths; never sent to the server.
|
|
15
|
+
*/
|
|
16
|
+
export interface RawAttachment {
|
|
17
|
+
name: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
contentType?: string;
|
|
20
|
+
body?: Buffer;
|
|
21
|
+
originalName?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Performance metrics collected from `result.steps` by `step-analyzer`. */
|
|
24
|
+
export interface CollectedPerformanceMetrics {
|
|
25
|
+
steps: Array<{
|
|
26
|
+
title: string;
|
|
27
|
+
duration: number;
|
|
28
|
+
category: string;
|
|
29
|
+
error?: {
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
failed?: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
totalStepDuration: number;
|
|
35
|
+
slowestStep: {
|
|
36
|
+
title: string;
|
|
37
|
+
duration: number;
|
|
38
|
+
} | null;
|
|
39
|
+
navigationCount: number;
|
|
40
|
+
navigationTotalDuration: number;
|
|
41
|
+
waitTotalDuration: number;
|
|
42
|
+
waitCount: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What `onTestEnd` accumulates per test case. Mixes three concerns that the
|
|
46
|
+
* reporter must keep together during a run:
|
|
47
|
+
* - **wire fields** (`title`, `status`, `duration`, …) that `toWireTestCase`
|
|
48
|
+
* projects onto `WireTestCase` before sending,
|
|
49
|
+
* - **collection-only state** (`attachments`, `performanceMetrics`,
|
|
50
|
+
* `stepEvents`) consumed by `FileHandler` and the run-level summary,
|
|
51
|
+
* - the `type` discriminant so the same collected object can be queued as a
|
|
52
|
+
* stream event.
|
|
53
|
+
*
|
|
54
|
+
* Upload bookkeeping (`_filesUploaded`) is deliberately NOT on this object —
|
|
55
|
+
* `StreamManager` tracks it in a side `Set` so the data model stays clean.
|
|
56
|
+
*/
|
|
57
|
+
export interface CollectedTestCase {
|
|
58
|
+
/** Stream-event discriminant: `'begin'` or `'complete'`. Omitted for batch-only runs. */
|
|
59
|
+
type?: 'begin' | 'complete';
|
|
60
|
+
title: string;
|
|
61
|
+
location: string;
|
|
62
|
+
status?: string;
|
|
63
|
+
duration?: number;
|
|
64
|
+
error?: string | null;
|
|
65
|
+
retries?: number;
|
|
66
|
+
workerIndex?: number | null;
|
|
67
|
+
shardIndex?: number | null;
|
|
68
|
+
startedAt?: number | null;
|
|
69
|
+
/** Raw Playwright attachments — never sent on the wire. */
|
|
70
|
+
attachments?: RawAttachment[];
|
|
71
|
+
browser?: BrowserConfig | null;
|
|
72
|
+
suitePath?: string[] | null;
|
|
73
|
+
suiteConfig?: SuiteConfigEntry[] | null;
|
|
74
|
+
testAnnotations?: TestAnnotation[] | null;
|
|
75
|
+
/** Source snippet around the failing line (failed/timedOut only). */
|
|
76
|
+
testSource?: string;
|
|
77
|
+
/** Step metrics from `collectStepMetrics`. Consumed by the run summary + `toWireTestCase`. */
|
|
78
|
+
performanceMetrics?: CollectedPerformanceMetrics;
|
|
79
|
+
stepEvents?: TestStepEvent[];
|
|
80
|
+
/** Parsed from `piwi-network` attachments by `FileHandler`. */
|
|
81
|
+
networkRequests?: unknown;
|
|
82
|
+
/** Parsed from `piwi-web-vitals` attachments. */
|
|
83
|
+
webVitals?: unknown;
|
|
84
|
+
/** Parsed from `piwi-console` attachments. */
|
|
85
|
+
consoleLogs?: unknown;
|
|
86
|
+
/** Parsed from `piwi-aria-snapshot` attachment. */
|
|
87
|
+
ariaSnapshot?: string;
|
|
88
|
+
/** Parsed from `piwi-locators` attachment. */
|
|
89
|
+
locatorSnapshots?: LocatorSnapshot[];
|
|
90
|
+
}
|
|
91
|
+
/** Hash + size of a single trace file, used for dedup against the server. */
|
|
92
|
+
export interface TraceHashInfo {
|
|
93
|
+
tracePath: string;
|
|
94
|
+
hash: string;
|
|
95
|
+
size: number;
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Internal collection model — the shapes the reporter accumulates *in process*
|
|
4
|
+
* while a run executes.
|
|
5
|
+
*
|
|
6
|
+
* These are NOT sent to the server verbatim: the wire projection happens in
|
|
7
|
+
* `serializer.ts` (`toWireTestCase` / `serializeRun`) at the JSON boundary. For
|
|
8
|
+
* the external server contract, see `./wire.ts`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|