@piwitests/reporter 0.18.1 → 0.19.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 CHANGED
@@ -168,7 +168,7 @@ Then import `test` from your fixtures file in every spec — a spec that imports
168
168
  - **Console entries** — `warning`, `error`, and `assert` messages with their source location.
169
169
  - **Browser Web Vitals** — TTFB, DOM Interactive, DOMContentLoaded, Load Complete, First Paint, First Contentful Paint — displayed with color-coded thresholds.
170
170
  - **ARIA snapshot** — captured automatically when a test fails, shown as failure evidence and fed to the AI diagnosis.
171
- - **Locator snapshots** — for each acted-on element, its attributes plus ranked alternative locators, stamped with the call site. These power locator healing; when a failing locator matches nothing, a fresh suggestion is attached as a Playwright annotation.
171
+ - **Locator snapshots** — for each element a test proves resolvable (successful actions and passing `expect(locator)` assertions alike), its attributes plus ranked alternative locators, stamped with the call site. These power locator healing; when a failing locator matches nothing, a fresh suggestion is attached as a Playwright annotation.
172
172
 
173
173
  Capture works for the `page` fixture, `browser.newPage()`, `browser.newContext().newPage()`, and popups. Everything is only collected when `collectPerformanceMetrics` is `true` (the default); locator snapshots can be disabled separately with `captureLocators: false`.
174
174
 
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/gate.ts
27
+ var fs = __toESM(require("fs"));
28
+
29
+ // ../packages/core/src/gate.ts
30
+ function formatGateResult(result) {
31
+ const { facts } = result;
32
+ const lines = [
33
+ result.passed ? `\u2714 Piwi gate passed \u2014 ${facts.projectName} run #${facts.runId}` : `\u2716 Piwi gate failed \u2014 ${facts.projectName} run #${facts.runId}`,
34
+ ` ${facts.totalTests} tests, ${facts.failedTests} failed, ${facts.newRegressions} new, ${facts.newFlaky} newly flaky`
35
+ ];
36
+ if (facts.quarantinedFailures > 0) {
37
+ lines.push(
38
+ ` ${facts.quarantinedFailures} failing ${facts.quarantinedFailures === 1 ? "test is" : "tests are"} quarantined and did not count`
39
+ );
40
+ }
41
+ for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
42
+ lines.push(` ${facts.runUrl}`);
43
+ return lines.join("\n");
44
+ }
45
+
46
+ // src/cli/gate.ts
47
+ var EXIT_OK = 0;
48
+ var EXIT_VIOLATED = 1;
49
+ var EXIT_ERROR = 2;
50
+ var USAGE = `
51
+ piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
52
+
53
+ Usage:
54
+ npx piwi gate [options]
55
+
56
+ Where the run comes from (first match wins):
57
+ --run-id <id> Run id to evaluate
58
+ --from-file <path> Read runId from the reporter's output JSON
59
+ PIWI_OUTPUT_FILE Same, from the environment
60
+ (default) ./piwi-run.json, if it exists
61
+
62
+ Connection:
63
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
64
+ --api-key <key> API key (env PIWI_API_KEY)
65
+
66
+ Policy (at least one is required):
67
+ --require-tag <tags> Comma-separated; every test carrying the tag must pass
68
+ --max-failed <n> Fail when more than n tests failed
69
+ --max-new-regressions <n> Fail when more than n tests newly started failing
70
+ --max-new-flaky <n> Fail when more than n tests newly became flaky
71
+ --max-quarantined <n> Fail when more than n tests are quarantined
72
+ --fail-on-new-cluster Fail when this run introduced a new failure cluster
73
+
74
+ Other:
75
+ --json Print the raw result as JSON instead of a summary
76
+ -h, --help Show this help
77
+
78
+ Exit codes: 0 satisfied, 1 violated, 2 could not evaluate.
79
+ `.trim();
80
+ function readOption(argv, name) {
81
+ const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
82
+ if (withEquals) return withEquals.slice(name.length + 1);
83
+ const index = argv.indexOf(name);
84
+ if (index === -1) return void 0;
85
+ const value = argv[index + 1];
86
+ return value && !value.startsWith("--") ? value : void 0;
87
+ }
88
+ function readCount(argv, name) {
89
+ const raw = readOption(argv, name);
90
+ if (raw === void 0) return void 0;
91
+ const n = Number(raw);
92
+ if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
93
+ return Math.floor(n);
94
+ }
95
+ function readRunIdFromFile(path) {
96
+ try {
97
+ const parsed = JSON.parse(fs.readFileSync(path, "utf-8"));
98
+ const runId = Number(parsed.runId);
99
+ return Number.isFinite(runId) && runId > 0 ? runId : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function parseGateArgs(argv, env) {
105
+ const serverUrl = (readOption(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
106
+ if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
107
+ let runId = Number(readOption(argv, "--run-id") ?? NaN);
108
+ if (!Number.isFinite(runId) || runId <= 0) {
109
+ const candidate = readOption(argv, "--from-file") ?? env.PIWI_OUTPUT_FILE ?? "piwi-run.json";
110
+ runId = readRunIdFromFile(candidate) ?? NaN;
111
+ }
112
+ if (!Number.isFinite(runId) || runId <= 0) {
113
+ throw new Error(
114
+ "No run to evaluate \u2014 pass --run-id, or set PIWI_OUTPUT_FILE so the reporter records the run it submitted"
115
+ );
116
+ }
117
+ const requireTags = (readOption(argv, "--require-tag") ?? "").split(",").map((tag) => tag.trim().replace(/^@+/, "")).filter(Boolean);
118
+ const policy = {
119
+ requireTags,
120
+ maxFailed: readCount(argv, "--max-failed"),
121
+ maxNewRegressions: readCount(argv, "--max-new-regressions"),
122
+ maxNewFlaky: readCount(argv, "--max-new-flaky"),
123
+ maxQuarantined: readCount(argv, "--max-quarantined"),
124
+ failOnNewCluster: argv.includes("--fail-on-new-cluster")
125
+ };
126
+ return { serverUrl, apiKey: readOption(argv, "--api-key") ?? env.PIWI_API_KEY ?? null, runId, policy };
127
+ }
128
+ async function requestGate(args) {
129
+ const headers = { "Content-Type": "application/json" };
130
+ if (args.apiKey) headers["X-API-Key"] = args.apiKey;
131
+ const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
132
+ method: "POST",
133
+ headers,
134
+ body: JSON.stringify(args.policy)
135
+ });
136
+ if (!res.ok) {
137
+ const body = await res.json().catch(() => ({}));
138
+ throw new Error(body.message || `Dashboard returned ${res.status} evaluating the gate`);
139
+ }
140
+ return await res.json();
141
+ }
142
+ async function runGate(argv, env = process.env) {
143
+ if (argv.includes("-h") || argv.includes("--help")) {
144
+ console.log(USAGE);
145
+ return EXIT_OK;
146
+ }
147
+ let args;
148
+ try {
149
+ args = parseGateArgs(argv, env);
150
+ } catch (e) {
151
+ console.error(`piwi gate: ${e.message}
152
+ `);
153
+ console.error(USAGE);
154
+ return EXIT_ERROR;
155
+ }
156
+ let result;
157
+ try {
158
+ result = await requestGate(args);
159
+ } catch (e) {
160
+ console.error(`piwi gate: ${e.message}`);
161
+ return EXIT_ERROR;
162
+ }
163
+ if (argv.includes("--json")) {
164
+ console.log(JSON.stringify(result, null, 2));
165
+ } else {
166
+ console.log(formatGateResult(result));
167
+ }
168
+ return result.passed ? EXIT_OK : EXIT_VIOLATED;
169
+ }
170
+
171
+ // src/cli/index.ts
172
+ var USAGE2 = `
173
+ piwi \u2014 companion commands for the Piwi Dashboard reporter
174
+
175
+ Usage:
176
+ npx piwi <command> [options]
177
+
178
+ Commands:
179
+ gate Fail a CI job on the dashboard's analysis of a run
180
+
181
+ Run \`npx piwi gate --help\` for that command's options.
182
+ `.trim();
183
+ async function main() {
184
+ const [command, ...rest] = process.argv.slice(2);
185
+ switch (command) {
186
+ case "gate":
187
+ return runGate(rest);
188
+ case void 0:
189
+ case "-h":
190
+ case "--help":
191
+ console.log(USAGE2);
192
+ return 0;
193
+ default:
194
+ console.error(`piwi: unknown command "${command}"
195
+ `);
196
+ console.error(USAGE2);
197
+ return 2;
198
+ }
199
+ }
200
+ main().then((code) => {
201
+ process.exitCode = code;
202
+ }).catch((e) => {
203
+ console.error(`piwi: ${e.message}`);
204
+ process.exitCode = 2;
205
+ });
@@ -35,7 +35,7 @@ __export(global_setup_module_exports, {
35
35
  module.exports = __toCommonJS(global_setup_module_exports);
36
36
 
37
37
  // src/public/global-setup.ts
38
- var path2 = __toESM(require("path"));
38
+ var path3 = __toESM(require("path"));
39
39
 
40
40
  // src/internal/support/errors.ts
41
41
  function errorMessage(error) {
@@ -44,7 +44,27 @@ function errorMessage(error) {
44
44
  }
45
45
 
46
46
  // src/public/global-setup.ts
47
- var fs2 = __toESM(require("fs"));
47
+ var fs3 = __toESM(require("fs"));
48
+
49
+ // src/internal/config/desktop.ts
50
+ var fs = __toESM(require("fs"));
51
+ var os = __toESM(require("os"));
52
+ var path = __toESM(require("path"));
53
+ function defaultDesktopConfigPath() {
54
+ return path.join(os.homedir(), ".piwi", "desktop.json");
55
+ }
56
+ function readDesktopConfig(filePath) {
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
59
+ const url = parsed?.url;
60
+ const token = parsed?.token;
61
+ if (typeof url !== "string" || !url) return null;
62
+ if (typeof token !== "string" || !token) return null;
63
+ return { url, token };
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
48
68
 
49
69
  // src/internal/config/env.ts
50
70
  var DEFAULTS = {
@@ -89,6 +109,7 @@ var PIWI_ENV_KEYS = {
89
109
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
90
110
  outputFile: "PIWI_OUTPUT_FILE"
91
111
  };
112
+ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
92
113
  function readBool(val) {
93
114
  if (val === void 0) return void 0;
94
115
  return val === "true";
@@ -115,6 +136,7 @@ var ENV_FALLBACK_SPECS = [
115
136
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
116
137
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
117
138
  ];
139
+ var desktopDiscovered = false;
118
140
  function resolveOptions(raw) {
119
141
  const env = process.env;
120
142
  const mergedRaw = { ...raw };
@@ -127,6 +149,17 @@ function resolveOptions(raw) {
127
149
  mergedRaw[spec.option] = spec.kind === "number" ? Number(value) : value;
128
150
  }
129
151
  }
152
+ desktopDiscovered = false;
153
+ const serverUrlUnset = mergedRaw.serverUrl === void 0;
154
+ const apiKeyUnset = mergedRaw.apiKey === void 0 || mergedRaw.apiKey === null;
155
+ if (serverUrlUnset && apiKeyUnset) {
156
+ const desktop = readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath());
157
+ if (desktop) {
158
+ mergedRaw.serverUrl = desktop.url;
159
+ mergedRaw.apiKey = desktop.token;
160
+ desktopDiscovered = true;
161
+ }
162
+ }
130
163
  const opts = { ...DEFAULTS, ...mergedRaw };
131
164
  if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose] === "true";
132
165
  return opts;
@@ -318,12 +351,12 @@ var HttpClient = class {
318
351
 
319
352
  // src/internal/support/instance-id.ts
320
353
  var crypto = __toESM(require("crypto"));
321
- var os = __toESM(require("os"));
354
+ var os2 = __toESM(require("os"));
322
355
  function hashForProject(projectName) {
323
356
  return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
324
357
  }
325
358
  function computeInstanceId(projectName, runLabel) {
326
- const seed = runLabel ? `${projectName}|${runLabel}` : `${os.hostname()}|${projectName}`;
359
+ const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
327
360
  return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16);
328
361
  }
329
362
 
@@ -346,24 +379,33 @@ function detectCiRunLabel() {
346
379
  }
347
380
 
348
381
  // src/internal/support/setup-file.ts
349
- var path = __toESM(require("path"));
350
- var os2 = __toESM(require("os"));
351
- var fs = __toESM(require("fs"));
382
+ var path2 = __toESM(require("path"));
383
+ var os3 = __toESM(require("os"));
384
+ var fs2 = __toESM(require("fs"));
352
385
  function getSetupFilePath(projectName) {
353
- return path.join(os2.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
386
+ return path2.join(os3.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
387
+ }
388
+
389
+ // src/internal/support/run-mode.ts
390
+ var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
391
+ function isUiMode(argv = process.argv) {
392
+ const args = argv.slice(2);
393
+ const testIdx = args.indexOf("test");
394
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
395
+ return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
354
396
  }
355
397
 
356
398
  // src/public/global-setup.ts
357
399
  function createGlobalSetup(options, userSetup) {
358
400
  return async function globalSetupFn(config) {
359
- const piwiReporterPath = path2.resolve(__dirname, "./index.js");
401
+ const piwiReporterPath = path3.resolve(__dirname, "./index.js");
360
402
  let inlineReporterOptions = {};
361
403
  if (Array.isArray(config?.reporter)) {
362
404
  for (const r of config.reporter) {
363
405
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
364
406
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
365
407
  try {
366
- return path2.resolve(require.resolve(r[0])) === piwiReporterPath;
408
+ return path3.resolve(require.resolve(r[0])) === piwiReporterPath;
367
409
  } catch {
368
410
  return false;
369
411
  }
@@ -376,6 +418,11 @@ function createGlobalSetup(options, userSetup) {
376
418
  }
377
419
  const opts = resolveOptions({ ...inlineReporterOptions, ...options });
378
420
  const logger = new Logger(opts.verbose ?? false);
421
+ if (isUiMode()) {
422
+ logger.debug("UI mode detected \u2014 skipping run registration.");
423
+ if (userSetup) return userSetup(config);
424
+ return;
425
+ }
379
426
  if (opts.enabled === false || !opts.serverUrl) {
380
427
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
381
428
  if (userSetup) return userSetup(config);
@@ -385,7 +432,7 @@ function createGlobalSetup(options, userSetup) {
385
432
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
386
433
  if (r[0].toLowerCase().includes("piwi")) return true;
387
434
  try {
388
- return path2.resolve(require.resolve(r[0])) === piwiReporterPath;
435
+ return path3.resolve(require.resolve(r[0])) === piwiReporterPath;
389
436
  } catch {
390
437
  return false;
391
438
  }
@@ -417,7 +464,7 @@ function createGlobalSetup(options, userSetup) {
417
464
  auth
418
465
  );
419
466
  if (response?.runId && response?.setupToken) {
420
- fs2.writeFileSync(
467
+ fs3.writeFileSync(
421
468
  getSetupFilePath(opts.projectName),
422
469
  JSON.stringify({
423
470
  runId: response.runId,
package/dist/index.d.ts CHANGED
@@ -3,8 +3,14 @@ import { PlaywrightTestConfig } from '@playwright/test';
3
3
  export { PlaywrightTestConfig } from '@playwright/test';
4
4
  export { PiwiFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
5
5
 
6
- /** Options for configuring the Piwi Dashboard reporter */
7
- interface PiwiDashboardOptions extends PlaywrightTestConfig {
6
+ /**
7
+ * Options for configuring the Piwi Dashboard reporter.
8
+ *
9
+ * Piwi options only — the Playwright config belongs in `defineConfig` (i.e. in
10
+ * `wrapConfig`'s first argument). These go in `wrapConfig`'s second argument or
11
+ * in the reporter entry's options (`['@piwitests/reporter', { … }]`).
12
+ */
13
+ interface PiwiDashboardOptions {
8
14
  /** Explicitly enable or disable the reporter. Defaults to `true` when `serverUrl` is set. Set to `false` to disable even if `serverUrl` is provided. */
9
15
  enabled?: boolean;
10
16
  /** URL of the Piwi Dashboard server */
@@ -182,6 +188,8 @@ declare class PiwiDashboardReporter {
182
188
  private shardInfo;
183
189
  private metadata;
184
190
  private enabled;
191
+ /** True when the server URL and API key came from the desktop app, not from config. */
192
+ private viaDesktopApp;
185
193
  private isFullRun;
186
194
  private filterDetails;
187
195
  private httpClient;