@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
package/dist/fixtures.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dashboardFixtures = void 0;
|
|
4
|
-
exports.extendDashboardFixtures = extendDashboardFixtures;
|
|
5
|
-
const zlib_1 = require("zlib");
|
|
6
|
-
/**
|
|
7
|
-
* Playwright fixtures that collect network requests, console entries,
|
|
8
|
-
* web vitals, and ARIA snapshots during a test.
|
|
9
|
-
*
|
|
10
|
-
* Attaches collected data as `piwi-dashboard-*` test-info attachments
|
|
11
|
-
* which the Piwi Dashboard reporter parses on `onTestEnd`.
|
|
12
|
-
*/
|
|
13
|
-
exports.dashboardFixtures = {
|
|
14
|
-
page: async ({ page }, use, testInfo) => {
|
|
15
|
-
const networkRequests = [];
|
|
16
|
-
const consoleEntries = [];
|
|
17
|
-
const pendingHandlers = [];
|
|
18
|
-
page.on('console', (msg) => {
|
|
19
|
-
const type = msg.type();
|
|
20
|
-
if (['warning', 'error', 'assert'].includes(type)) {
|
|
21
|
-
consoleEntries.push({
|
|
22
|
-
type,
|
|
23
|
-
text: msg.text(),
|
|
24
|
-
timestamp: Date.now(),
|
|
25
|
-
location: msg.location()
|
|
26
|
-
? `${msg.location().url}:${msg.location().lineNumber}:${msg.location().columnNumber}`
|
|
27
|
-
: null,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
page.on('requestfinished', (request) => {
|
|
32
|
-
const p = (async () => {
|
|
33
|
-
try {
|
|
34
|
-
const url = request.url();
|
|
35
|
-
if (url.startsWith('data:') || url.startsWith('blob:'))
|
|
36
|
-
return;
|
|
37
|
-
const timing = request.timing();
|
|
38
|
-
const response = await request.response();
|
|
39
|
-
const resourceType = request.resourceType();
|
|
40
|
-
// Only keep API/document requests; skip static assets (scripts, styles, fonts, images, media)
|
|
41
|
-
if (!['fetch', 'xhr', 'document', 'other'].includes(resourceType))
|
|
42
|
-
return;
|
|
43
|
-
const entry = {
|
|
44
|
-
method: request.method(),
|
|
45
|
-
url,
|
|
46
|
-
status: response ? response.status() : 0,
|
|
47
|
-
duration: timing.responseEnd > 0 ? Math.round(timing.responseEnd - timing.requestStart) : 0,
|
|
48
|
-
startTime: timing.startTime,
|
|
49
|
-
resourceType,
|
|
50
|
-
};
|
|
51
|
-
if (response) {
|
|
52
|
-
const logHeader = response.headers()['x-piwi-logs'];
|
|
53
|
-
if (logHeader) {
|
|
54
|
-
try {
|
|
55
|
-
entry.serverLogs = JSON.parse((0, zlib_1.gunzipSync)(Buffer.from(logHeader, 'base64')).toString('utf-8'));
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
/* ignore malformed header */
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
networkRequests.push(entry);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
/* ignore */
|
|
66
|
-
}
|
|
67
|
-
})();
|
|
68
|
-
pendingHandlers.push(p);
|
|
69
|
-
});
|
|
70
|
-
await use(page);
|
|
71
|
-
// Wait for all in-flight requestfinished handlers before snapshotting
|
|
72
|
-
// networkRequests — the last request (often the one that failed the test)
|
|
73
|
-
// races with fixture teardown and its serverLogs would otherwise be lost.
|
|
74
|
-
await Promise.allSettled(pendingHandlers);
|
|
75
|
-
if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
|
|
76
|
-
try {
|
|
77
|
-
const snapshot = await page.locator(':root').ariaSnapshot();
|
|
78
|
-
if (snapshot) {
|
|
79
|
-
await testInfo.attach('piwi-dashboard-aria-snapshot', {
|
|
80
|
-
contentType: 'text/plain',
|
|
81
|
-
body: snapshot,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
/* ignore */
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (consoleEntries.length > 0) {
|
|
90
|
-
await testInfo.attach('piwi-dashboard-console', {
|
|
91
|
-
contentType: 'application/json',
|
|
92
|
-
body: Buffer.from(JSON.stringify(consoleEntries)),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (networkRequests.length > 0) {
|
|
96
|
-
await testInfo.attach('piwi-dashboard-network', {
|
|
97
|
-
contentType: 'application/json',
|
|
98
|
-
body: Buffer.from(JSON.stringify(networkRequests)),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
const webVitals = await page.evaluate(() => {
|
|
103
|
-
const navEntries = performance.getEntriesByType('navigation');
|
|
104
|
-
const paintEntries = performance.getEntriesByType('paint');
|
|
105
|
-
const nav = navEntries[0];
|
|
106
|
-
const navigation = nav
|
|
107
|
-
? {
|
|
108
|
-
url: nav.name,
|
|
109
|
-
ttfb: Math.round(nav.responseStart - nav.fetchStart),
|
|
110
|
-
domInteractive: Math.round(nav.domInteractive - nav.fetchStart),
|
|
111
|
-
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.fetchStart),
|
|
112
|
-
loadComplete: Math.round(nav.loadEventEnd - nav.fetchStart),
|
|
113
|
-
transferSize: nav.transferSize || 0,
|
|
114
|
-
encodedBodySize: nav.encodedBodySize || 0,
|
|
115
|
-
decodedBodySize: nav.decodedBodySize || 0,
|
|
116
|
-
}
|
|
117
|
-
: null;
|
|
118
|
-
const paint = {};
|
|
119
|
-
for (const entry of paintEntries) {
|
|
120
|
-
const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
121
|
-
paint[key] = Math.round(entry.startTime);
|
|
122
|
-
}
|
|
123
|
-
if (!navigation && Object.keys(paint).length === 0)
|
|
124
|
-
return null;
|
|
125
|
-
return { navigation, paint };
|
|
126
|
-
});
|
|
127
|
-
if (webVitals) {
|
|
128
|
-
await testInfo.attach('piwi-dashboard-web-vitals', {
|
|
129
|
-
contentType: 'application/json',
|
|
130
|
-
body: Buffer.from(JSON.stringify(webVitals)),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
/* ignore */
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* Extend a Playwright `test` object with Piwi Dashboard fixtures.
|
|
141
|
-
*
|
|
142
|
-
* Use this instead of importing `@playwright/test` directly from this package
|
|
143
|
-
* to avoid the "Requiring @playwright/test second time" error caused by
|
|
144
|
-
* duplicate module resolution.
|
|
145
|
-
*
|
|
146
|
-
* @example
|
|
147
|
-
* ```ts
|
|
148
|
-
* import { test as base } from '@playwright/test';
|
|
149
|
-
* import { extendDashboardFixtures } from '@piwitests/reporter/fixtures';
|
|
150
|
-
*
|
|
151
|
-
* export const test = extendDashboardFixtures(base);
|
|
152
|
-
* ```
|
|
153
|
-
*/
|
|
154
|
-
function extendDashboardFixtures(test) {
|
|
155
|
-
return test.extend(exports.dashboardFixtures);
|
|
156
|
-
}
|
package/dist/helpers.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { PiwiDashboardOptions } from './config.js';
|
|
2
|
-
/** Produce a deterministic short SHA-1 hex hash for a project name (used in temp file names) */
|
|
3
|
-
export declare function hashForProject(projectName: string): string;
|
|
4
|
-
/**
|
|
5
|
-
* Extract a worker index from a Playwright `TestResult`, falling back to
|
|
6
|
-
* `parallelIndex` when `workerIndex` is absent. Returns `null` when neither is
|
|
7
|
-
* set. Localizes the `as any` reach into Playwright's result object so the
|
|
8
|
-
* cast lives in exactly one place.
|
|
9
|
-
*/
|
|
10
|
-
export declare function workerIndexOf(result: any): number | null;
|
|
11
|
-
export declare function detectCliFileFilters(argv?: string[]): string[];
|
|
12
|
-
/** Return the temp-file path used to exchange setup info between globalSetup and the reporter instance */
|
|
13
|
-
export declare function getSetupFilePath(projectName: string): string;
|
|
14
|
-
/** Derive a unique instance identifier by hashing hostname + projectName (or runLabel + projectName when sharding) */
|
|
15
|
-
export declare function computeInstanceId(projectName: string, runLabel?: string | null): string;
|
|
16
|
-
/** Detect a stable CI run label from well-known environment variables. Returns null outside CI. */
|
|
17
|
-
export declare function detectCiRunLabel(): string | null;
|
|
18
|
-
/** Information saved by the global setup for the reporter instance to consume */
|
|
19
|
-
export interface SetupInfo {
|
|
20
|
-
/** Server-assigned run ID */
|
|
21
|
-
runId: number;
|
|
22
|
-
/** One-time token used to authenticate the /begin call */
|
|
23
|
-
setupToken: string;
|
|
24
|
-
/** Project name this run belongs to */
|
|
25
|
-
projectName: string;
|
|
26
|
-
}
|
|
27
|
-
/** Read and delete the setup info file for the given project. Returns `null` when no file exists. */
|
|
28
|
-
export declare function readSetupInfo(projectName: string): SetupInfo | null;
|
|
29
|
-
/** Read a snippet of source code surrounding the given line, returning a formatted string with line numbers. Returns `null` on error. */
|
|
30
|
-
export declare function readSourceSnippet(file: string, line: number, context: number): string | null;
|
|
31
|
-
/** Create a concurrency limiter that ensures at most `maxConcurrent` async operations run simultaneously */
|
|
32
|
-
export declare function createLimiter(maxConcurrent: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
33
|
-
/**
|
|
34
|
-
* Create a Playwright `globalSetup` function that registers a test run on the
|
|
35
|
-
* Piwi Dashboard server at the start of the run. An optional `userSetup` is
|
|
36
|
-
* called afterward so existing setup logic is preserved.
|
|
37
|
-
*
|
|
38
|
-
* The server-run ID and a one-time token are written to a temp file so the
|
|
39
|
-
* reporter instance can pick them up during the streaming handshake.
|
|
40
|
-
*
|
|
41
|
-
* @param options Piwi Dashboard options (uses `serverUrl`, `projectName`, …).
|
|
42
|
-
* @param userSetup An existing global setup to chain after the Piwi registration.
|
|
43
|
-
*/
|
|
44
|
-
export declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (config: any) => any): (config: any) => Promise<any>;
|
package/dist/helpers.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
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.hashForProject = hashForProject;
|
|
37
|
-
exports.workerIndexOf = workerIndexOf;
|
|
38
|
-
exports.detectCliFileFilters = detectCliFileFilters;
|
|
39
|
-
exports.getSetupFilePath = getSetupFilePath;
|
|
40
|
-
exports.computeInstanceId = computeInstanceId;
|
|
41
|
-
exports.detectCiRunLabel = detectCiRunLabel;
|
|
42
|
-
exports.readSetupInfo = readSetupInfo;
|
|
43
|
-
exports.readSourceSnippet = readSourceSnippet;
|
|
44
|
-
exports.createLimiter = createLimiter;
|
|
45
|
-
exports.createGlobalSetup = createGlobalSetup;
|
|
46
|
-
const crypto = __importStar(require("crypto"));
|
|
47
|
-
const path = __importStar(require("path"));
|
|
48
|
-
const os = __importStar(require("os"));
|
|
49
|
-
const fs = __importStar(require("fs"));
|
|
50
|
-
const config_js_1 = require("./config.js");
|
|
51
|
-
const http_client_js_1 = require("./http-client.js");
|
|
52
|
-
const logger_js_1 = require("./logger.js");
|
|
53
|
-
/** Produce a deterministic short SHA-1 hex hash for a project name (used in temp file names) */
|
|
54
|
-
function hashForProject(projectName) {
|
|
55
|
-
return crypto.createHash('sha1').update(projectName).digest('hex').slice(0, 16);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Extract a worker index from a Playwright `TestResult`, falling back to
|
|
59
|
-
* `parallelIndex` when `workerIndex` is absent. Returns `null` when neither is
|
|
60
|
-
* set. Localizes the `as any` reach into Playwright's result object so the
|
|
61
|
-
* cast lives in exactly one place.
|
|
62
|
-
*/
|
|
63
|
-
function workerIndexOf(result) {
|
|
64
|
-
if (!result)
|
|
65
|
-
return null;
|
|
66
|
-
return result.workerIndex ?? result.parallelIndex ?? null;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Extract positional file/path filters from a Playwright CLI invocation
|
|
70
|
-
* (e.g. `playwright test tests/login.spec.ts auth/`). These narrow a run to
|
|
71
|
-
* specific files and are NOT reflected in `config.grep`, so they're the only
|
|
72
|
-
* signal that a file-filtered run is partial. Returns an empty array when the
|
|
73
|
-
* full suite was requested.
|
|
74
|
-
*
|
|
75
|
-
* Best-effort: skips the values of known value-taking options so they aren't
|
|
76
|
-
* mistaken for file filters. `--flag=value` forms consume no extra token.
|
|
77
|
-
*/
|
|
78
|
-
const PW_VALUE_FLAGS = new Set([
|
|
79
|
-
'-c',
|
|
80
|
-
'--config',
|
|
81
|
-
'-j',
|
|
82
|
-
'--workers',
|
|
83
|
-
'-g',
|
|
84
|
-
'--grep',
|
|
85
|
-
'--grep-invert',
|
|
86
|
-
'--global-timeout',
|
|
87
|
-
'--max-failures',
|
|
88
|
-
'--output',
|
|
89
|
-
'-p',
|
|
90
|
-
'--project',
|
|
91
|
-
'--repeat-each',
|
|
92
|
-
'--reporter',
|
|
93
|
-
'--retries',
|
|
94
|
-
'--shard',
|
|
95
|
-
'--timeout',
|
|
96
|
-
'--trace',
|
|
97
|
-
]);
|
|
98
|
-
function detectCliFileFilters(argv = process.argv) {
|
|
99
|
-
// Drop the node executable + script path, then start after the `test` subcommand.
|
|
100
|
-
const args = argv.slice(2);
|
|
101
|
-
const testIdx = args.indexOf('test');
|
|
102
|
-
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
103
|
-
const files = [];
|
|
104
|
-
for (let i = 0; i < rest.length; i++) {
|
|
105
|
-
const tok = rest[i];
|
|
106
|
-
if (tok.startsWith('-')) {
|
|
107
|
-
// A bare value-taking flag (no `=`) consumes the next token as its value.
|
|
108
|
-
if (!tok.includes('=') && PW_VALUE_FLAGS.has(tok))
|
|
109
|
-
i++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
files.push(tok);
|
|
113
|
-
}
|
|
114
|
-
return files;
|
|
115
|
-
}
|
|
116
|
-
/** Return the temp-file path used to exchange setup info between globalSetup and the reporter instance */
|
|
117
|
-
function getSetupFilePath(projectName) {
|
|
118
|
-
return path.join(os.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
|
|
119
|
-
}
|
|
120
|
-
/** Derive a unique instance identifier by hashing hostname + projectName (or runLabel + projectName when sharding) */
|
|
121
|
-
function computeInstanceId(projectName, runLabel) {
|
|
122
|
-
const seed = runLabel ? `${projectName}|${runLabel}` : `${os.hostname()}|${projectName}`;
|
|
123
|
-
return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
124
|
-
}
|
|
125
|
-
/** Detect a stable CI run label from well-known environment variables. Returns null outside CI. */
|
|
126
|
-
function detectCiRunLabel() {
|
|
127
|
-
const env = process.env;
|
|
128
|
-
if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID)
|
|
129
|
-
return env.GITHUB_RUN_ID;
|
|
130
|
-
if (env.GITLAB_CI && env.CI_PIPELINE_ID)
|
|
131
|
-
return env.CI_PIPELINE_ID;
|
|
132
|
-
if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID)
|
|
133
|
-
return env.CIRCLE_WORKFLOW_ID;
|
|
134
|
-
if (env.TRAVIS && env.TRAVIS_BUILD_ID)
|
|
135
|
-
return env.TRAVIS_BUILD_ID;
|
|
136
|
-
if (env.TF_BUILD && env.BUILD_BUILDID)
|
|
137
|
-
return env.BUILD_BUILDID;
|
|
138
|
-
if (env.JENKINS_URL && env.BUILD_ID)
|
|
139
|
-
return env.BUILD_ID;
|
|
140
|
-
if (env.BUILDKITE_BUILD_ID)
|
|
141
|
-
return env.BUILDKITE_BUILD_ID;
|
|
142
|
-
if (env.TEAMCITY_BUILD_ID)
|
|
143
|
-
return env.TEAMCITY_BUILD_ID;
|
|
144
|
-
if (env.BITBUCKET_BUILD_NUMBER)
|
|
145
|
-
return env.BITBUCKET_BUILD_NUMBER;
|
|
146
|
-
if (env.SEMAPHORE_WORKFLOW_ID)
|
|
147
|
-
return env.SEMAPHORE_WORKFLOW_ID;
|
|
148
|
-
if (env.APPVEYOR_BUILD_ID)
|
|
149
|
-
return env.APPVEYOR_BUILD_ID;
|
|
150
|
-
if (env.DRONE_BUILD_NUMBER)
|
|
151
|
-
return env.DRONE_BUILD_NUMBER;
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
/** Read and delete the setup info file for the given project. Returns `null` when no file exists. */
|
|
155
|
-
function readSetupInfo(projectName) {
|
|
156
|
-
const setupFile = getSetupFilePath(projectName);
|
|
157
|
-
try {
|
|
158
|
-
if (fs.existsSync(setupFile)) {
|
|
159
|
-
const info = JSON.parse(fs.readFileSync(setupFile, 'utf8'));
|
|
160
|
-
fs.unlinkSync(setupFile);
|
|
161
|
-
if (info.projectName === projectName)
|
|
162
|
-
return info;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
/* ignore */
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
/** Read a snippet of source code surrounding the given line, returning a formatted string with line numbers. Returns `null` on error. */
|
|
171
|
-
function readSourceSnippet(file, line, context) {
|
|
172
|
-
try {
|
|
173
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
174
|
-
const lines = content.split('\n');
|
|
175
|
-
const start = Math.max(0, line - context - 1);
|
|
176
|
-
const end = Math.min(lines.length, line + context);
|
|
177
|
-
return lines
|
|
178
|
-
.slice(start, end)
|
|
179
|
-
.map((l, i) => {
|
|
180
|
-
const lineNum = start + i + 1;
|
|
181
|
-
const marker = lineNum === line ? '> ' : ' ';
|
|
182
|
-
return `${marker}${String(lineNum).padStart(4)} | ${l}`;
|
|
183
|
-
})
|
|
184
|
-
.join('\n');
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/** Create a concurrency limiter that ensures at most `maxConcurrent` async operations run simultaneously */
|
|
191
|
-
function createLimiter(maxConcurrent) {
|
|
192
|
-
const limitValue = Math.max(1, Math.floor(maxConcurrent));
|
|
193
|
-
let active = 0;
|
|
194
|
-
const queue = [];
|
|
195
|
-
const next = () => {
|
|
196
|
-
active--;
|
|
197
|
-
const run = queue.shift();
|
|
198
|
-
if (run)
|
|
199
|
-
run();
|
|
200
|
-
};
|
|
201
|
-
return function limit(fn) {
|
|
202
|
-
return new Promise((resolve, reject) => {
|
|
203
|
-
const run = () => {
|
|
204
|
-
active++;
|
|
205
|
-
Promise.resolve().then(fn).then(resolve, reject).finally(next);
|
|
206
|
-
};
|
|
207
|
-
if (active < limitValue)
|
|
208
|
-
run();
|
|
209
|
-
else
|
|
210
|
-
queue.push(run);
|
|
211
|
-
});
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Create a Playwright `globalSetup` function that registers a test run on the
|
|
216
|
-
* Piwi Dashboard server at the start of the run. An optional `userSetup` is
|
|
217
|
-
* called afterward so existing setup logic is preserved.
|
|
218
|
-
*
|
|
219
|
-
* The server-run ID and a one-time token are written to a temp file so the
|
|
220
|
-
* reporter instance can pick them up during the streaming handshake.
|
|
221
|
-
*
|
|
222
|
-
* @param options Piwi Dashboard options (uses `serverUrl`, `projectName`, …).
|
|
223
|
-
* @param userSetup An existing global setup to chain after the Piwi registration.
|
|
224
|
-
*/
|
|
225
|
-
function createGlobalSetup(options, userSetup) {
|
|
226
|
-
return async function globalSetupFn(config) {
|
|
227
|
-
const opts = (0, config_js_1.resolveOptions)((options ?? {}));
|
|
228
|
-
const logger = new logger_js_1.Logger(opts.verbose ?? false);
|
|
229
|
-
if (!opts.serverUrl) {
|
|
230
|
-
logger.info('Not enabled — set PIWI_DASHBOARD_URL or serverUrl to enable.');
|
|
231
|
-
if (userSetup)
|
|
232
|
-
return userSetup(config);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const piwiReporterPath = path.resolve(__dirname, 'index.js');
|
|
236
|
-
const hasPiwi = Array.isArray(config?.reporter) &&
|
|
237
|
-
config.reporter.some((r) => {
|
|
238
|
-
if (!Array.isArray(r) || typeof r[0] !== 'string')
|
|
239
|
-
return false;
|
|
240
|
-
if (r[0].toLowerCase().includes('piwi'))
|
|
241
|
-
return true;
|
|
242
|
-
try {
|
|
243
|
-
return path.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
if (!hasPiwi) {
|
|
250
|
-
logger.debug('Not reporting — Piwi is not in the Playwright reporters list.');
|
|
251
|
-
if (userSetup)
|
|
252
|
-
return userSetup(config);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
const httpClient = new http_client_js_1.HttpClient(opts.serverUrl, logger);
|
|
256
|
-
try {
|
|
257
|
-
const auth = await httpClient.resolveAuth(opts);
|
|
258
|
-
const runLabel = opts.runLabel || detectCiRunLabel();
|
|
259
|
-
// Detect shard info from Playwright config (--shard=1/3)
|
|
260
|
-
const pwShard = config.shard;
|
|
261
|
-
const shardIndex = pwShard?.current;
|
|
262
|
-
const shardTotal = pwShard?.total;
|
|
263
|
-
const response = await httpClient.postJSON('/api/test-runs/setup', {
|
|
264
|
-
projectName: opts.projectName,
|
|
265
|
-
projectDescription: opts.projectDescription,
|
|
266
|
-
environment: opts.environment || null,
|
|
267
|
-
label: opts.label || null,
|
|
268
|
-
startTime: new Date().toISOString(),
|
|
269
|
-
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
270
|
-
shardIndex,
|
|
271
|
-
shardTotal,
|
|
272
|
-
}, auth);
|
|
273
|
-
if (response?.runId && response?.setupToken) {
|
|
274
|
-
fs.writeFileSync(getSetupFilePath(opts.projectName), JSON.stringify({
|
|
275
|
-
runId: response.runId,
|
|
276
|
-
setupToken: response.setupToken,
|
|
277
|
-
projectName: opts.projectName,
|
|
278
|
-
}));
|
|
279
|
-
logger.debug(`Global setup: initialising run #${response.runId}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
logger.warn(`Could not register global setup: ${error.message}`);
|
|
284
|
-
}
|
|
285
|
-
if (userSetup)
|
|
286
|
-
return userSetup(config);
|
|
287
|
-
};
|
|
288
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|