@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,288 @@
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
+ }
@@ -0,0 +1,42 @@
1
+ import FormData from 'form-data';
2
+ import { Logger } from './logger.js';
3
+ export { FormData };
4
+ /**
5
+ * Low-level HTTP client for communicating with the Piwi Dashboard server.
6
+ * Supports JSON requests, multipart form-data uploads, and session-based login.
7
+ *
8
+ * All three public methods (`login`, `postJSON`, `postFormData`) delegate to a
9
+ * single `request()` core that owns the transport selection, header/auth
10
+ * application, response accumulation, and socket timeout. The package stays on
11
+ * the single `form-data` runtime dependency — no HTTP client library.
12
+ */
13
+ export declare class HttpClient {
14
+ private readonly serverUrl;
15
+ private readonly logger;
16
+ private readonly timeout;
17
+ /**
18
+ * @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
19
+ * @param logger Prefixed logger for verbose diagnostics.
20
+ * @param timeout Socket inactivity timeout in ms (default 30s). A hung
21
+ * server now fails fast instead of stalling the reporter.
22
+ */
23
+ constructor(serverUrl: string, logger?: Logger, timeout?: number);
24
+ /**
25
+ * Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
26
+ * or return `null` when neither is configured.
27
+ */
28
+ resolveAuth(options: {
29
+ apiKey?: string | null;
30
+ username?: string | null;
31
+ password?: string | null;
32
+ }): Promise<string | null>;
33
+ /** Authenticate with username/password and return the session cookie string */
34
+ login(username: string, password: string): Promise<string>;
35
+ /** Send a JSON POST request. `auth` can be an API key (prefix `pd_`) or a session cookie string. */
36
+ postJSON(pathname: string, payload: unknown, auth?: string | null): Promise<any>;
37
+ /** Send a multipart form-data POST request. Used for report and trace uploads. */
38
+ postFormData(pathname: string, form: FormData, auth?: string | null): Promise<any>;
39
+ /** Unified request core: transport, headers, auth, response accumulation, timeout. */
40
+ private request;
41
+ private applyAuth;
42
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpClient = exports.FormData = void 0;
7
+ const https_1 = __importDefault(require("https"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const url_1 = require("url");
10
+ const form_data_1 = __importDefault(require("form-data"));
11
+ exports.FormData = form_data_1.default;
12
+ const logger_js_1 = require("./logger.js");
13
+ /**
14
+ * Low-level HTTP client for communicating with the Piwi Dashboard server.
15
+ * Supports JSON requests, multipart form-data uploads, and session-based login.
16
+ *
17
+ * All three public methods (`login`, `postJSON`, `postFormData`) delegate to a
18
+ * single `request()` core that owns the transport selection, header/auth
19
+ * application, response accumulation, and socket timeout. The package stays on
20
+ * the single `form-data` runtime dependency — no HTTP client library.
21
+ */
22
+ class HttpClient {
23
+ /**
24
+ * @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
25
+ * @param logger Prefixed logger for verbose diagnostics.
26
+ * @param timeout Socket inactivity timeout in ms (default 30s). A hung
27
+ * server now fails fast instead of stalling the reporter.
28
+ */
29
+ constructor(serverUrl, logger = new logger_js_1.Logger(), timeout = 30000) {
30
+ this.serverUrl = serverUrl;
31
+ this.logger = logger;
32
+ this.timeout = timeout;
33
+ }
34
+ /**
35
+ * Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
36
+ * or return `null` when neither is configured.
37
+ */
38
+ async resolveAuth(options) {
39
+ if (options.apiKey)
40
+ return options.apiKey;
41
+ if (options.username && options.password) {
42
+ this.logger.info(`Authenticating as ${options.username}...`);
43
+ return this.login(options.username, options.password);
44
+ }
45
+ return null;
46
+ }
47
+ /** Authenticate with username/password and return the session cookie string */
48
+ async login(username, password) {
49
+ const body = JSON.stringify({ username, password });
50
+ const res = await this.request('POST', '/api/auth/login', {
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'Content-Length': Buffer.byteLength(body),
54
+ },
55
+ body,
56
+ });
57
+ if (res.status < 200 || res.status >= 300) {
58
+ this.logger.debugError(`Login response: ${res.text}`);
59
+ throw new Error(`Login failed with status ${res.status}`);
60
+ }
61
+ const setCookie = res.headers['set-cookie'];
62
+ if (!setCookie || setCookie.length === 0) {
63
+ throw new Error('Login succeeded but no session cookie was returned');
64
+ }
65
+ const cookie = setCookie.map((c) => c.split(';')[0]).join('; ');
66
+ this.logger.debug('Logged in successfully');
67
+ return cookie;
68
+ }
69
+ /** Send a JSON POST request. `auth` can be an API key (prefix `pd_`) or a session cookie string. */
70
+ async postJSON(pathname, payload, auth) {
71
+ const body = JSON.stringify(payload);
72
+ const res = await this.request('POST', pathname, {
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ 'Content-Length': Buffer.byteLength(body),
76
+ },
77
+ body,
78
+ auth,
79
+ });
80
+ if (res.status < 200 || res.status >= 300) {
81
+ this.logger.debugError(`Response: ${res.text}`);
82
+ throw new Error(`Request failed with status ${res.status}`);
83
+ }
84
+ try {
85
+ return JSON.parse(res.text);
86
+ }
87
+ catch {
88
+ return {};
89
+ }
90
+ }
91
+ /** Send a multipart form-data POST request. Used for report and trace uploads. */
92
+ async postFormData(pathname, form, auth) {
93
+ const headers = form.getHeaders();
94
+ const res = await this.request('POST', pathname, { headers, form, auth });
95
+ if (res.status < 200 || res.status >= 300) {
96
+ throw new Error(`Request failed with status ${res.status}: ${res.text}`);
97
+ }
98
+ try {
99
+ return JSON.parse(res.text);
100
+ }
101
+ catch {
102
+ return {};
103
+ }
104
+ }
105
+ /** Unified request core: transport, headers, auth, response accumulation, timeout. */
106
+ request(method, pathname, opts) {
107
+ return new Promise((resolve, reject) => {
108
+ const url = new url_1.URL(pathname, this.serverUrl);
109
+ const transport = url.protocol === 'https:' ? https_1.default : http_1.default;
110
+ const headers = { ...opts.headers };
111
+ this.applyAuth(headers, opts.auth);
112
+ const req = transport.request({
113
+ hostname: url.hostname,
114
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
115
+ path: url.pathname,
116
+ method,
117
+ headers,
118
+ }, (res) => {
119
+ let data = '';
120
+ res.on('data', (chunk) => {
121
+ data += chunk;
122
+ });
123
+ res.on('end', () => {
124
+ resolve({ status: res.statusCode ?? 0, text: data, headers: res.headers });
125
+ });
126
+ });
127
+ req.on('error', reject);
128
+ req.setTimeout(this.timeout, () => {
129
+ req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
130
+ });
131
+ if (opts.form) {
132
+ opts.form.pipe(req);
133
+ }
134
+ else if (opts.body !== undefined) {
135
+ req.write(opts.body);
136
+ req.end();
137
+ }
138
+ else {
139
+ req.end();
140
+ }
141
+ });
142
+ }
143
+ applyAuth(headers, auth) {
144
+ if (!auth)
145
+ return;
146
+ if (auth.startsWith('pd_')) {
147
+ headers['Authorization'] = `Bearer ${auth}`;
148
+ }
149
+ else {
150
+ headers['Cookie'] = auth;
151
+ }
152
+ }
153
+ }
154
+ exports.HttpClient = HttpClient;
@@ -0,0 +1,8 @@
1
+ import { PiwiDashboardReporter } from './reporter.js';
2
+ import { wrapConfig } from './config-wrapper.js';
3
+ import { createGlobalSetup } from './helpers.js';
4
+ declare const _default: typeof PiwiDashboardReporter & {
5
+ wrapConfig: typeof wrapConfig;
6
+ createGlobalSetup: typeof createGlobalSetup;
7
+ };
8
+ export = _default;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ const reporter_js_1 = require("./reporter.js");
3
+ const config_wrapper_js_1 = require("./config-wrapper.js");
4
+ const helpers_js_1 = require("./helpers.js");
5
+ const _default = Object.assign(reporter_js_1.PiwiDashboardReporter, { wrapConfig: config_wrapper_js_1.wrapConfig, createGlobalSetup: helpers_js_1.createGlobalSetup });
6
+ module.exports = _default;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Tiny prefixed logger. Owns the `[Piwi Dashboard]` prefix and the verbose
3
+ * gate so the prefix isn't typed out ~46× across the package and `verbose`
4
+ * doesn't need to be threaded into every constructor.
5
+ *
6
+ * Channel preservation:
7
+ * - `info` → stdout (always),
8
+ * - `warn` → stderr (always),
9
+ * - `error` → stderr (always),
10
+ * - `debug` → stdout (only when `verbose`),
11
+ * - `debugError` → stderr (only when `verbose`).
12
+ *
13
+ * `debugError` exists so verbose-only diagnostics that previously used
14
+ * `console.error` (e.g. HTTP response bodies on failure) keep going to stderr
15
+ * after the refactor.
16
+ */
17
+ export declare class Logger {
18
+ private readonly verbose;
19
+ private readonly prefix;
20
+ constructor(verbose?: boolean);
21
+ info(msg: string): void;
22
+ warn(msg: string): void;
23
+ error(msg: string): void;
24
+ debug(msg: string): void;
25
+ debugError(msg: string): void;
26
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ /**
5
+ * Tiny prefixed logger. Owns the `[Piwi Dashboard]` prefix and the verbose
6
+ * gate so the prefix isn't typed out ~46× across the package and `verbose`
7
+ * doesn't need to be threaded into every constructor.
8
+ *
9
+ * Channel preservation:
10
+ * - `info` → stdout (always),
11
+ * - `warn` → stderr (always),
12
+ * - `error` → stderr (always),
13
+ * - `debug` → stdout (only when `verbose`),
14
+ * - `debugError` → stderr (only when `verbose`).
15
+ *
16
+ * `debugError` exists so verbose-only diagnostics that previously used
17
+ * `console.error` (e.g. HTTP response bodies on failure) keep going to stderr
18
+ * after the refactor.
19
+ */
20
+ class Logger {
21
+ constructor(verbose = false) {
22
+ this.verbose = verbose;
23
+ this.prefix = '[Piwi Dashboard] ';
24
+ }
25
+ info(msg) {
26
+ console.log(this.prefix + msg);
27
+ }
28
+ warn(msg) {
29
+ console.warn(this.prefix + msg);
30
+ }
31
+ error(msg) {
32
+ console.error(this.prefix + msg);
33
+ }
34
+ debug(msg) {
35
+ if (this.verbose)
36
+ console.log(this.prefix + msg);
37
+ }
38
+ debugError(msg) {
39
+ if (this.verbose)
40
+ console.error(this.prefix + msg);
41
+ }
42
+ }
43
+ exports.Logger = Logger;
@@ -0,0 +1,32 @@
1
+ import type { FullConfig, Suite, TestCase } from '@playwright/test/reporter';
2
+ import { Logger } from './logger.js';
3
+ import type { SuiteConfigEntry } from './types.js';
4
+ /**
5
+ * Collects CI/CD environment metadata, SCM (git) information, and Playwright
6
+ * config metadata to attach to each test run submission.
7
+ *
8
+ * Also owns all reach-through access to Playwright-internal suite fields
9
+ * (`_parallelMode`, `_annotations`, `project()`) so the surface area that
10
+ * breaks when Playwright renames those internals is guarded behind one class.
11
+ */
12
+ export declare class MetadataCollector {
13
+ private readonly logger;
14
+ constructor(logger?: Logger);
15
+ /** Collect all available metadata from the environment, config, and suite */
16
+ collect(config: FullConfig, suite: Suite, options: any): Record<string, unknown>;
17
+ /** Walk the test's parent hierarchy to extract the resolved browser/project configuration */
18
+ getBrowserConfig(test: TestCase): Record<string, any> | null;
19
+ /**
20
+ * Walk the test's parent `describe` suites to extract the suite path (titles)
21
+ * and per-level config (parallel mode + annotations). Reaches into Playwright
22
+ * suite internals (`_parallelMode`, `_annotations`) — kept here next to
23
+ * `getBrowserConfig` so all such access is guarded behind one class.
24
+ */
25
+ getSuiteInfo(test: TestCase): {
26
+ suitePath: string[];
27
+ suiteConfig: SuiteConfigEntry[];
28
+ };
29
+ private collectScmInfo;
30
+ private collectCiInfo;
31
+ private extractPlaywrightConfigMetadata;
32
+ }