@piwitests/reporter 0.4.2

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/dist/compression.d.ts +5 -0
  4. package/dist/compression.js +39 -0
  5. package/dist/config-wrapper.d.ts +21 -0
  6. package/dist/config-wrapper.js +64 -0
  7. package/dist/config.d.ts +104 -0
  8. package/dist/config.js +127 -0
  9. package/dist/crash-recovery.d.ts +23 -0
  10. package/dist/crash-recovery.js +105 -0
  11. package/dist/file-handler.d.ts +38 -0
  12. package/dist/file-handler.js +166 -0
  13. package/dist/fixtures.d.ts +25 -0
  14. package/dist/fixtures.js +156 -0
  15. package/dist/global-setup-module.d.ts +2 -0
  16. package/dist/global-setup-module.js +4 -0
  17. package/dist/helpers.d.ts +44 -0
  18. package/dist/helpers.js +288 -0
  19. package/dist/http-client.d.ts +42 -0
  20. package/dist/http-client.js +154 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +6 -0
  23. package/dist/logger.d.ts +26 -0
  24. package/dist/logger.js +43 -0
  25. package/dist/metadata-collector.d.ts +32 -0
  26. package/dist/metadata-collector.js +243 -0
  27. package/dist/reporter.d.ts +65 -0
  28. package/dist/reporter.js +341 -0
  29. package/dist/run-submitter.d.ts +66 -0
  30. package/dist/run-submitter.js +184 -0
  31. package/dist/serializer.d.ts +45 -0
  32. package/dist/serializer.js +104 -0
  33. package/dist/skip-classify.d.ts +27 -0
  34. package/dist/skip-classify.js +40 -0
  35. package/dist/step-analyzer.d.ts +97 -0
  36. package/dist/step-analyzer.js +216 -0
  37. package/dist/stream-buffer.d.ts +17 -0
  38. package/dist/stream-buffer.js +102 -0
  39. package/dist/stream-manager.d.ts +74 -0
  40. package/dist/stream-manager.js +338 -0
  41. package/dist/types.d.ts +251 -0
  42. package/dist/types.js +14 -0
  43. package/dist/uploader.d.ts +86 -0
  44. package/dist/uploader.js +191 -0
  45. package/package.json +62 -0
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetadataCollector = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const logger_js_1 = require("./logger.js");
6
+ /**
7
+ * Collects CI/CD environment metadata, SCM (git) information, and Playwright
8
+ * config metadata to attach to each test run submission.
9
+ *
10
+ * Also owns all reach-through access to Playwright-internal suite fields
11
+ * (`_parallelMode`, `_annotations`, `project()`) so the surface area that
12
+ * breaks when Playwright renames those internals is guarded behind one class.
13
+ */
14
+ class MetadataCollector {
15
+ constructor(logger = new logger_js_1.Logger()) {
16
+ this.logger = logger;
17
+ }
18
+ /** Collect all available metadata from the environment, config, and suite */
19
+ collect(config, suite, options) {
20
+ const metadata = {};
21
+ if (options.projectDescription)
22
+ metadata.projectDescription = options.projectDescription;
23
+ if (options.relatedIssue)
24
+ metadata.relatedIssue = options.relatedIssue;
25
+ if (options.ciInfo)
26
+ metadata.ciInfo = options.ciInfo;
27
+ if (options.tags && Array.isArray(options.tags))
28
+ metadata.tags = options.tags;
29
+ if (options.customData)
30
+ metadata.customData = options.customData;
31
+ if (options.collectScmInfo) {
32
+ const scm = this.collectScmInfo(options);
33
+ if (scm)
34
+ metadata.scm = scm;
35
+ }
36
+ if (options.collectCiInfo) {
37
+ const ci = this.collectCiInfo();
38
+ if (ci)
39
+ metadata.ci = ci;
40
+ }
41
+ const htmlMeta = this.extractPlaywrightConfigMetadata(config);
42
+ if (htmlMeta && Object.keys(htmlMeta).length > 0)
43
+ metadata.htmlReport = htmlMeta;
44
+ if (config.metadata)
45
+ metadata.playwrightConfig = config.metadata;
46
+ if (suite?.allTests) {
47
+ const all = suite.allTests();
48
+ if (all.length > 0) {
49
+ const first = all[0];
50
+ if (first?.parent?.project) {
51
+ const proj = first.parent.project();
52
+ if (proj?.metadata)
53
+ metadata.playwrightProject = proj.metadata;
54
+ }
55
+ }
56
+ }
57
+ return metadata;
58
+ }
59
+ /** Walk the test's parent hierarchy to extract the resolved browser/project configuration */
60
+ getBrowserConfig(test) {
61
+ try {
62
+ let suite = test.parent;
63
+ let depth = 0;
64
+ while (suite && depth < 20) {
65
+ depth++;
66
+ const project = suite.project?.();
67
+ if (project) {
68
+ const use = project.use ?? {};
69
+ const config = { projectName: project.name };
70
+ if (use.browserName)
71
+ config.browserName = use.browserName;
72
+ if (use.channel)
73
+ config.channel = use.channel;
74
+ if (use.viewport)
75
+ config.viewport = { width: use.viewport.width, height: use.viewport.height };
76
+ if (use.deviceScaleFactor != null)
77
+ config.deviceScaleFactor = use.deviceScaleFactor;
78
+ if (use.isMobile != null)
79
+ config.isMobile = use.isMobile;
80
+ if (use.hasTouch != null)
81
+ config.hasTouch = use.hasTouch;
82
+ if (use.locale)
83
+ config.locale = use.locale;
84
+ if (use.timezoneId)
85
+ config.timezoneId = use.timezoneId;
86
+ if (use.geolocation) {
87
+ config.geolocation = {
88
+ longitude: use.geolocation.longitude,
89
+ latitude: use.geolocation.latitude,
90
+ ...(use.geolocation.accuracy != null && { accuracy: use.geolocation.accuracy }),
91
+ };
92
+ }
93
+ if (use.colorScheme)
94
+ config.colorScheme = use.colorScheme;
95
+ if (use.reducedMotion)
96
+ config.reducedMotion = use.reducedMotion;
97
+ if (use.forcedColors)
98
+ config.forcedColors = use.forcedColors;
99
+ if (use.offline)
100
+ config.offline = use.offline;
101
+ if (use.bypassCSP)
102
+ config.bypassCSP = use.bypassCSP;
103
+ if (use.javaScriptEnabled === false)
104
+ config.javaScriptEnabled = false;
105
+ if (use.serviceWorkers)
106
+ config.serviceWorkers = use.serviceWorkers;
107
+ if (use.userAgent)
108
+ config.userAgent = use.userAgent;
109
+ return config;
110
+ }
111
+ suite = suite.parent;
112
+ }
113
+ return null;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Walk the test's parent `describe` suites to extract the suite path (titles)
121
+ * and per-level config (parallel mode + annotations). Reaches into Playwright
122
+ * suite internals (`_parallelMode`, `_annotations`) — kept here next to
123
+ * `getBrowserConfig` so all such access is guarded behind one class.
124
+ */
125
+ getSuiteInfo(test) {
126
+ const suitePath = [];
127
+ const suiteConfig = [];
128
+ const suites = [];
129
+ let suite = test.parent;
130
+ while (suite && suite.type === 'describe') {
131
+ suites.unshift(suite);
132
+ suite = suite.parent;
133
+ }
134
+ for (const s of suites) {
135
+ if (!s.title)
136
+ continue;
137
+ suitePath.push(s.title);
138
+ const rawMode = s._parallelMode;
139
+ const mode = rawMode === 'parallel' ? 'parallel' : rawMode === 'serial' ? 'serial' : 'default';
140
+ const annotations = s._annotations ?? [];
141
+ suiteConfig.push({ mode, annotations });
142
+ }
143
+ return { suitePath, suiteConfig };
144
+ }
145
+ collectScmInfo(_options) {
146
+ const scm = {};
147
+ try {
148
+ const execOpts = { encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024 };
149
+ scm.commit = (0, child_process_1.execSync)('git rev-parse HEAD', execOpts).trim();
150
+ scm.branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', execOpts).trim();
151
+ scm.author = (0, child_process_1.execSync)('git log -1 --pretty=format:"%an"', execOpts).trim();
152
+ scm.commitMessage = (0, child_process_1.execSync)('git log -1 --pretty=format:"%s"', execOpts).trim();
153
+ try {
154
+ scm.remoteUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', execOpts).trim();
155
+ }
156
+ catch {
157
+ /* optional */
158
+ }
159
+ }
160
+ catch (error) {
161
+ this.logger.debug(`Git info not available: ${error.message}`);
162
+ }
163
+ return Object.keys(scm).length > 0 ? scm : undefined;
164
+ }
165
+ collectCiInfo() {
166
+ const ci = {};
167
+ const env = process.env;
168
+ if (env.JENKINS_URL) {
169
+ ci.provider = 'Jenkins';
170
+ ci.buildNumber = env.BUILD_NUMBER;
171
+ ci.buildUrl = env.BUILD_URL;
172
+ ci.jobName = env.JOB_NAME;
173
+ }
174
+ else if (env.GITHUB_ACTIONS) {
175
+ ci.provider = 'GitHub Actions';
176
+ ci.runId = env.GITHUB_RUN_ID;
177
+ ci.runNumber = env.GITHUB_RUN_NUMBER;
178
+ ci.workflow = env.GITHUB_WORKFLOW;
179
+ ci.actor = env.GITHUB_ACTOR;
180
+ ci.repository = env.GITHUB_REPOSITORY;
181
+ ci.ref = env.GITHUB_REF;
182
+ ci.sha = env.GITHUB_SHA;
183
+ ci.serverUrl = env.GITHUB_SERVER_URL;
184
+ if (ci.serverUrl && ci.repository && ci.runId) {
185
+ ci.buildUrl = `${ci.serverUrl}/${ci.repository}/actions/runs/${ci.runId}`;
186
+ }
187
+ }
188
+ else if (env.GITLAB_CI) {
189
+ ci.provider = 'GitLab CI';
190
+ ci.pipelineId = env.CI_PIPELINE_ID;
191
+ ci.pipelineUrl = env.CI_PIPELINE_URL;
192
+ ci.jobId = env.CI_JOB_ID;
193
+ ci.jobUrl = env.CI_JOB_URL;
194
+ ci.jobName = env.CI_JOB_NAME;
195
+ }
196
+ else if (env.CIRCLECI) {
197
+ ci.provider = 'CircleCI';
198
+ ci.buildNumber = env.CIRCLE_BUILD_NUM;
199
+ ci.buildUrl = env.CIRCLE_BUILD_URL;
200
+ ci.jobName = env.CIRCLE_JOB;
201
+ ci.workflow = env.CIRCLE_WORKFLOW_ID;
202
+ }
203
+ else if (env.TRAVIS) {
204
+ ci.provider = 'Travis CI';
205
+ ci.buildNumber = env.TRAVIS_BUILD_NUMBER;
206
+ ci.buildUrl = env.TRAVIS_BUILD_WEB_URL;
207
+ ci.jobNumber = env.TRAVIS_JOB_NUMBER;
208
+ }
209
+ else if (env.TF_BUILD) {
210
+ ci.provider = 'Azure Pipelines';
211
+ ci.buildNumber = env.BUILD_BUILDNUMBER;
212
+ ci.buildId = env.BUILD_BUILDID;
213
+ if (env.SYSTEM_TEAMFOUNDATIONSERVERURI && env.SYSTEM_TEAMPROJECT && env.BUILD_BUILDID) {
214
+ ci.buildUrl = `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`;
215
+ }
216
+ ci.jobName = env.AGENT_JOBNAME;
217
+ }
218
+ else if (env.CI) {
219
+ ci.provider = 'Unknown CI';
220
+ ci.detected = true;
221
+ }
222
+ return Object.keys(ci).length > 0 ? ci : undefined;
223
+ }
224
+ extractPlaywrightConfigMetadata(config) {
225
+ const meta = {};
226
+ if (config.projects?.length > 0) {
227
+ meta.projects = config.projects.map((p) => ({
228
+ name: p.name,
229
+ testDir: p.testDir,
230
+ use: {
231
+ browserName: p.use?.browserName || p.name,
232
+ viewport: p.use?.viewport,
233
+ deviceScaleFactor: p.use?.deviceScaleFactor,
234
+ },
235
+ }));
236
+ }
237
+ meta.workers = config.workers;
238
+ meta.timeout = config.globalTimeout;
239
+ meta.fullyParallel = config.fullyParallel;
240
+ return meta;
241
+ }
242
+ }
243
+ exports.MetadataCollector = MetadataCollector;
@@ -0,0 +1,65 @@
1
+ import type { FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
+ import { createGlobalSetup } from './helpers.js';
3
+ /**
4
+ * Piwi Dashboard Playwright reporter.
5
+ *
6
+ * Collects test results, metadata, performance metrics and trace files, then
7
+ * hands the collected run to a `RunSubmitter` which drives the JSON / multipart
8
+ * / streaming submit ladder. The reporter itself only owns the Playwright hooks
9
+ * and the running counters.
10
+ */
11
+ export declare class PiwiDashboardReporter {
12
+ private options;
13
+ private testCases;
14
+ private startTime;
15
+ private playwrightVersion;
16
+ private totalTests;
17
+ private passedTests;
18
+ private failedTests;
19
+ private skippedTests;
20
+ private timedOutTests;
21
+ private didNotRunTests;
22
+ /** Full set of tests Playwright planned to run this shard (captured in `onBegin`). */
23
+ private plannedTests;
24
+ /** Ids of tests that actually reported via `onTestEnd`, to find the ones that never ran. */
25
+ private reportedTestIds;
26
+ private instanceId;
27
+ private runLabel;
28
+ private shardInfo;
29
+ private metadata;
30
+ private enabled;
31
+ private isFullRun;
32
+ private filterDetails;
33
+ private httpClient;
34
+ private uploader;
35
+ private fileHandler;
36
+ private metadataCollector;
37
+ private streamManager;
38
+ private recovery;
39
+ private submitter;
40
+ private readonly logger;
41
+ static createGlobalSetup: typeof createGlobalSetup;
42
+ constructor(rawOptions?: Record<string, any>);
43
+ /** Playwright reporter hook: called once at the start of the test run */
44
+ onBegin(config: FullConfig, suite: Suite): void;
45
+ /** Playwright reporter hook: called when an individual test begins */
46
+ onTestBegin(test: TestCase, result: TestResult): void;
47
+ /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
48
+ private setupSteps;
49
+ /** Playwright reporter hook: called when a step (including hook/fixture) begins */
50
+ onStepBegin(test: TestCase | undefined, _result: TestResult | undefined, step: any): void;
51
+ /** Playwright reporter hook: called when a step (including hook/fixture) ends */
52
+ onStepEnd(test: TestCase | undefined, _result: TestResult | undefined, step: any): void;
53
+ /** Playwright reporter hook: called when an individual test finishes */
54
+ onTestEnd(test: TestCase, result: TestResult): void;
55
+ /**
56
+ * Synthesize `didnotrun` cases for tests Playwright planned but never reported
57
+ * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
58
+ * carry no result, so they're emitted with zero duration and no error. In
59
+ * streaming mode they're queued as complete events so the pre-finish drain
60
+ * sends them alongside the rest.
61
+ */
62
+ private materializeUnrunTests;
63
+ /** Playwright reporter hook: called when the full test run finishes */
64
+ onEnd(result: FullResult): Promise<void>;
65
+ }
@@ -0,0 +1,341 @@
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.PiwiDashboardReporter = void 0;
37
+ const path = __importStar(require("path"));
38
+ const config_js_1 = require("./config.js");
39
+ const http_client_js_1 = require("./http-client.js");
40
+ const uploader_js_1 = require("./uploader.js");
41
+ const stream_buffer_js_1 = require("./stream-buffer.js");
42
+ const crash_recovery_js_1 = require("./crash-recovery.js");
43
+ const file_handler_js_1 = require("./file-handler.js");
44
+ const metadata_collector_js_1 = require("./metadata-collector.js");
45
+ const stream_manager_js_1 = require("./stream-manager.js");
46
+ const step_analyzer_js_1 = require("./step-analyzer.js");
47
+ const helpers_js_1 = require("./helpers.js");
48
+ const serializer_js_1 = require("./serializer.js");
49
+ const skip_classify_js_1 = require("./skip-classify.js");
50
+ const run_submitter_js_1 = require("./run-submitter.js");
51
+ const logger_js_1 = require("./logger.js");
52
+ /**
53
+ * Piwi Dashboard Playwright reporter.
54
+ *
55
+ * Collects test results, metadata, performance metrics and trace files, then
56
+ * hands the collected run to a `RunSubmitter` which drives the JSON / multipart
57
+ * / streaming submit ladder. The reporter itself only owns the Playwright hooks
58
+ * and the running counters.
59
+ */
60
+ class PiwiDashboardReporter {
61
+ constructor(rawOptions = {}) {
62
+ this.testCases = [];
63
+ this.startTime = null;
64
+ this.playwrightVersion = null;
65
+ this.totalTests = 0;
66
+ this.passedTests = 0;
67
+ this.failedTests = 0;
68
+ this.skippedTests = 0;
69
+ this.timedOutTests = 0;
70
+ this.didNotRunTests = 0;
71
+ /** Full set of tests Playwright planned to run this shard (captured in `onBegin`). */
72
+ this.plannedTests = [];
73
+ /** Ids of tests that actually reported via `onTestEnd`, to find the ones that never ran. */
74
+ this.reportedTestIds = new Set();
75
+ this.runLabel = null;
76
+ this.shardInfo = null;
77
+ this.metadata = {};
78
+ this.isFullRun = true;
79
+ this.filterDetails = null;
80
+ this.streamManager = null;
81
+ /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
82
+ this.setupSteps = [];
83
+ this.options = (0, config_js_1.resolveOptions)(rawOptions);
84
+ this.enabled = !!this.options.serverUrl;
85
+ this.runLabel = this.options.runLabel || (0, helpers_js_1.detectCiRunLabel)();
86
+ this.instanceId = (0, helpers_js_1.computeInstanceId)(this.options.projectName, this.runLabel);
87
+ const logger = new logger_js_1.Logger(this.options.verbose ?? false);
88
+ this.logger = logger;
89
+ this.httpClient = new http_client_js_1.HttpClient(this.options.serverUrl ?? 'http://localhost:3000', logger);
90
+ this.fileHandler = new file_handler_js_1.FileHandler(logger);
91
+ this.uploader = new uploader_js_1.Uploader(this.httpClient, this.fileHandler, logger);
92
+ this.recovery = new crash_recovery_js_1.CrashRecovery(this.options.projectName, logger);
93
+ this.metadataCollector = new metadata_collector_js_1.MetadataCollector(logger);
94
+ const streamBuffer = new stream_buffer_js_1.StreamBuffer(this.options.projectName);
95
+ streamBuffer.clearStale();
96
+ if (this.options.streaming) {
97
+ this.streamManager = new stream_manager_js_1.StreamManager(this.httpClient, streamBuffer, this.recovery, this.uploader, this.fileHandler, this.options, logger);
98
+ }
99
+ this.submitter = new run_submitter_js_1.RunSubmitter(this.httpClient, this.uploader, this.recovery, this.streamManager, logger);
100
+ }
101
+ /** Playwright reporter hook: called once at the start of the test run */
102
+ onBegin(config, suite) {
103
+ if (!this.enabled) {
104
+ this.logger.info('Not enabled — set PIWI_DASHBOARD_URL or serverUrl to enable.');
105
+ return;
106
+ }
107
+ this.startTime = new Date().toISOString();
108
+ this.playwrightVersion = config.version;
109
+ this.logger.info(`Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`);
110
+ // Detect partial-run filters so the dashboard can distinguish full-suite runs from ad-hoc focused runs.
111
+ const rawConfig = config;
112
+ const grepRe = rawConfig.grep instanceof RegExp ? rawConfig.grep : undefined;
113
+ const grepInvertRe = rawConfig.grepInvert instanceof RegExp ? rawConfig.grepInvert : undefined;
114
+ // Playwright's default grep is /.*/ (matches everything) — only a non-default pattern is a real filter.
115
+ const grep = grepRe && grepRe.source !== '.*' ? grepRe.source : undefined;
116
+ const grepInvert = grepInvertRe?.source;
117
+ // File/path filters come from the CLI invocation, not config.grep.
118
+ const fileFilters = (0, helpers_js_1.detectCliFileFilters)();
119
+ if (grep || grepInvert || fileFilters.length > 0) {
120
+ this.isFullRun = false;
121
+ this.filterDetails = {
122
+ ...(grep ? { grep } : {}),
123
+ ...(grepInvert ? { grepInvert } : {}),
124
+ ...(fileFilters.length > 0 ? { files: fileFilters } : {}),
125
+ };
126
+ this.logger.info('Partial run detected (filter active)');
127
+ }
128
+ this.metadata = this.metadataCollector.collect(config, suite, this.options);
129
+ // Snapshot the planned test list so `onEnd` can materialize tests that
130
+ // never ran (e.g. cut short by `maxFailures`) as `didnotrun` cases. The
131
+ // suite is already filtered/sharded, so this matches what this shard
132
+ // attempts.
133
+ this.plannedTests = suite.allTests();
134
+ // Detect Playwright shard config (--shard=1/3)
135
+ const pwShard = config.shard;
136
+ if (pwShard?.total && pwShard.total > 1) {
137
+ this.shardInfo = { current: pwShard.current, total: pwShard.total };
138
+ this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
139
+ }
140
+ this.streamManager?.start(this.startTime, this.metadata, this.instanceId, this.playwrightVersion, this.shardInfo, this.isFullRun, this.filterDetails);
141
+ }
142
+ /** Playwright reporter hook: called when an individual test begins */
143
+ onTestBegin(test, result) {
144
+ const relativeFilePath = path.relative(process.cwd(), test.location.file);
145
+ const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
146
+ const beginEvent = {
147
+ type: 'begin',
148
+ title: test.title,
149
+ location: `${relativeFilePath}:${test.location.line}:${test.location.column}`,
150
+ workerIndex: (0, helpers_js_1.workerIndexOf)(result),
151
+ shardIndex: this.shardInfo?.current ?? null,
152
+ browser: this.metadataCollector.getBrowserConfig(test) || undefined,
153
+ suitePath,
154
+ suiteConfig,
155
+ };
156
+ if (this.streamManager) {
157
+ this.streamManager.queueBeginEvent((0, serializer_js_1.toWireTestCase)(beginEvent));
158
+ }
159
+ }
160
+ /** Playwright reporter hook: called when a step (including hook/fixture) begins */
161
+ onStepBegin(test, _result, step) {
162
+ if (!this.enabled || !this.streamManager?.enabled)
163
+ return;
164
+ const cat = step.category;
165
+ if (cat !== 'hook' && cat !== 'fixture')
166
+ return;
167
+ const event = {
168
+ type: 'step-begin',
169
+ title: step.title,
170
+ location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : 'unknown',
171
+ stepCategory: cat,
172
+ parentTitle: test?.title || null,
173
+ workerIndex: (0, helpers_js_1.workerIndexOf)(_result),
174
+ startedAt: step.startTime instanceof Date ? step.startTime.getTime() : null,
175
+ };
176
+ this.streamManager?.queueBeginEvent(event);
177
+ }
178
+ /** Playwright reporter hook: called when a step (including hook/fixture) ends */
179
+ onStepEnd(test, _result, step) {
180
+ if (!this.enabled || !this.streamManager?.enabled)
181
+ return;
182
+ const cat = step.category;
183
+ if (cat !== 'hook' && cat !== 'fixture')
184
+ return;
185
+ const workerIndex = (0, helpers_js_1.workerIndexOf)(_result);
186
+ const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
187
+ const event = {
188
+ type: 'step-end',
189
+ title: step.title,
190
+ location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : 'unknown',
191
+ status: step.error ? 'failed' : 'passed',
192
+ duration: step.duration || 0,
193
+ stepCategory: cat,
194
+ parentTitle: test?.title || null,
195
+ workerIndex,
196
+ startedAt,
197
+ };
198
+ this.streamManager?.queueEvent(event);
199
+ // Track suite-level hooks (beforeAll/afterAll) for the timeline
200
+ if (!test && startedAt) {
201
+ this.setupSteps.push({
202
+ title: step.title,
203
+ category: cat,
204
+ startedAt,
205
+ duration: step.duration || 0,
206
+ status: step.error ? 'failed' : 'passed',
207
+ location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : null,
208
+ workerIndex,
209
+ });
210
+ }
211
+ }
212
+ /** Playwright reporter hook: called when an individual test finishes */
213
+ onTestEnd(test, result) {
214
+ this.totalTests++;
215
+ const relativeFilePath = path.relative(process.cwd(), test.location.file);
216
+ this.reportedTestIds.add(test.id);
217
+ const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
218
+ const annotations = (0, skip_classify_js_1.mergeAnnotations)(test, result);
219
+ const status = (0, skip_classify_js_1.classifyStatus)(result.status, annotations);
220
+ const testCase = {
221
+ type: 'complete',
222
+ title: test.title,
223
+ location: `${relativeFilePath}:${test.location.line}:${test.location.column}`,
224
+ status,
225
+ duration: result.duration,
226
+ error: result.error ? result.error.message : null,
227
+ retries: result.retry,
228
+ workerIndex: (0, helpers_js_1.workerIndexOf)(result),
229
+ shardIndex: this.shardInfo?.current ?? null,
230
+ startedAt: result.startTime ? result.startTime.getTime() : null,
231
+ attachments: result.attachments || [],
232
+ browser: this.metadataCollector.getBrowserConfig(test) || undefined,
233
+ suitePath,
234
+ suiteConfig,
235
+ testAnnotations: annotations.length ? annotations : null,
236
+ };
237
+ if (result.status === 'failed' || result.status === 'timedOut') {
238
+ const snippet = (0, helpers_js_1.readSourceSnippet)(test.location.file, test.location.line, 30);
239
+ if (snippet)
240
+ testCase.testSource = snippet;
241
+ }
242
+ if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
243
+ testCase.performanceMetrics = (0, step_analyzer_js_1.collectStepMetrics)(result.steps);
244
+ const stepEvents = (0, step_analyzer_js_1.extractTestStepEvents)(result.steps, result.startTime);
245
+ const waitEvents = (0, step_analyzer_js_1.extractWaitEvents)(result.steps);
246
+ const allEvents = [...stepEvents, ...waitEvents];
247
+ if (allEvents.length > 0)
248
+ testCase.stepEvents = allEvents;
249
+ }
250
+ if (this.options.collectPerformanceMetrics && result.attachments) {
251
+ this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
252
+ }
253
+ switch (status) {
254
+ case 'passed':
255
+ this.passedTests++;
256
+ break;
257
+ case 'failed':
258
+ this.failedTests++;
259
+ break;
260
+ case 'skipped':
261
+ this.skippedTests++;
262
+ break;
263
+ case 'didnotrun':
264
+ this.didNotRunTests++;
265
+ break;
266
+ case 'timedOut':
267
+ this.timedOutTests++;
268
+ break;
269
+ }
270
+ this.testCases.push(testCase);
271
+ if (this.streamManager) {
272
+ this.streamManager.queueEvent((0, serializer_js_1.toWireTestCase)(testCase));
273
+ if (this.options.liveFileUploads)
274
+ this.streamManager.scheduleLiveUpload(testCase);
275
+ }
276
+ }
277
+ /**
278
+ * Synthesize `didnotrun` cases for tests Playwright planned but never reported
279
+ * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
280
+ * carry no result, so they're emitted with zero duration and no error. In
281
+ * streaming mode they're queued as complete events so the pre-finish drain
282
+ * sends them alongside the rest.
283
+ */
284
+ materializeUnrunTests() {
285
+ for (const test of this.plannedTests) {
286
+ if (this.reportedTestIds.has(test.id))
287
+ continue;
288
+ const relativeFilePath = path.relative(process.cwd(), test.location.file);
289
+ const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
290
+ const testCase = {
291
+ type: 'complete',
292
+ title: test.title,
293
+ location: `${relativeFilePath}:${test.location.line}:${test.location.column}`,
294
+ status: 'didnotrun',
295
+ duration: 0,
296
+ error: null,
297
+ retries: 0,
298
+ workerIndex: null,
299
+ shardIndex: this.shardInfo?.current ?? null,
300
+ startedAt: null,
301
+ attachments: [],
302
+ browser: this.metadataCollector.getBrowserConfig(test) || undefined,
303
+ suitePath,
304
+ suiteConfig,
305
+ testAnnotations: test.annotations?.length ? test.annotations : null,
306
+ };
307
+ this.testCases.push(testCase);
308
+ this.totalTests++;
309
+ this.didNotRunTests++;
310
+ if (this.streamManager) {
311
+ this.streamManager.queueEvent((0, serializer_js_1.toWireTestCase)(testCase));
312
+ }
313
+ }
314
+ }
315
+ /** Playwright reporter hook: called when the full test run finishes */
316
+ async onEnd(result) {
317
+ if (!this.enabled)
318
+ return;
319
+ this.materializeUnrunTests();
320
+ await this.submitter.submit({
321
+ options: this.options,
322
+ testCases: this.testCases,
323
+ startTime: this.startTime,
324
+ playwrightVersion: this.playwrightVersion,
325
+ totalTests: this.totalTests,
326
+ passedTests: this.passedTests,
327
+ failedTests: this.failedTests,
328
+ skippedTests: this.skippedTests,
329
+ timedOutTests: this.timedOutTests,
330
+ didNotRunTests: this.didNotRunTests,
331
+ metadata: this.metadata,
332
+ instanceId: this.instanceId,
333
+ shardInfo: this.shardInfo,
334
+ setupSteps: this.setupSteps,
335
+ isFullRun: this.isFullRun,
336
+ filterDetails: this.filterDetails,
337
+ }, result);
338
+ }
339
+ }
340
+ exports.PiwiDashboardReporter = PiwiDashboardReporter;
341
+ PiwiDashboardReporter.createGlobalSetup = helpers_js_1.createGlobalSetup;