@piwitests/reporter 0.7.0 → 0.9.1

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
@@ -200,7 +200,7 @@ When `collectCiInfo` is enabled (default), the reporter auto-detects:
200
200
 
201
201
  ## Requirements
202
202
 
203
- - Node.js 18 or higher
203
+ - Node.js 18 or higher (the reporter runs inside your test project — the dashboard *server* itself targets Node 24+, or use its Docker image)
204
204
  - Playwright Test 1.40 or higher
205
205
  - Running Piwi Dashboard server
206
206
 
@@ -217,9 +217,9 @@ npm run reporter:dev # watch mode — auto-recompile on changes
217
217
 
218
218
  ### Source layout
219
219
 
220
- The package keeps its **public API** (`src/index.ts`, `src/fixtures.ts`, `src/public/`) separate from internal plumbing (`src/internal/<domain>/`) and the type model (`src/types/`). See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full map — the public/internal split, the collect-and-submit data flow, the fallback ladder, and the conventions.
220
+ The package keeps its **public API** (`src/index.ts`, `src/public/`) separate from internal plumbing (`src/internal/<domain>/`) and the type model (`src/types/`). See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full map — the public/internal split, the collect-and-submit data flow, the fallback ladder, and the conventions.
221
221
 
222
- The `package.json` `exports` field maps the main entry and `./fixtures` to their `dist/` counterparts.
222
+ Everything public the reporter, config helpers, and the capture fixtures is exported from the package's single entry point (`@piwitests/reporter`).
223
223
 
224
224
  ## Troubleshooting
225
225
 
@@ -1,4 +1,23 @@
1
1
  import type { Fixtures, Locator } from '@playwright/test';
2
+ /** Shape returned by the in-page element probe (see `wrapLocator`). */
3
+ interface CapturedAttrs {
4
+ tagName: string;
5
+ attributes: Record<string, string | null>;
6
+ textContent: string;
7
+ center: {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ /** True when the element has an associated <label> — gates getByLabel. */
12
+ hasLabel: boolean;
13
+ /** querySelectorAll match counts for candidate selectors (uniqueness probe). */
14
+ selectorCounts: {
15
+ testId?: number;
16
+ id?: number;
17
+ name?: number;
18
+ classes?: Record<string, number>;
19
+ };
20
+ }
2
21
  /**
3
22
  * ARIA snapshot that tolerates every Playwright version the reporter supports,
4
23
  * returning null instead of throwing so a capture can never fail the test. The
@@ -13,6 +32,15 @@ import type { Fixtures, Locator } from '@playwright/test';
13
32
  * - < 1.49: `locator.ariaSnapshot` does not exist — returns null up front.
14
33
  */
15
34
  export declare function ariaSnapshotBestEffort(target: Locator, timeout?: number): Promise<string | null>;
35
+ /**
36
+ * Runs inside the browser via `evaluate()` — probes a captured element for its
37
+ * attributes, geometry, label association, and selector-uniqueness counts.
38
+ * Must stay a fully self-contained function (no references to this module's
39
+ * closure): Playwright serializes it and executes it in the page, browser-side.
40
+ * `el` is browser-context (no DOM lib in this Node package), hence `any`.
41
+ * Exported for unit testing; still passed directly to `evaluate()` below.
42
+ */
43
+ export declare function probeElementAttrs(el: any, keep: string[]): CapturedAttrs;
16
44
  /**
17
45
  * Playwright fixtures that collect network requests, console entries,
18
46
  * web vitals, ARIA snapshots, and locator interaction data during a test.
@@ -39,3 +67,4 @@ export declare const dashboardFixtures: Fixtures;
39
67
  * ```
40
68
  */
41
69
  export declare function extendDashboardFixtures<T>(test: T): T;
70
+ export {};
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dashboardFixtures = void 0;
4
4
  exports.ariaSnapshotBestEffort = ariaSnapshotBestEffort;
5
+ exports.probeElementAttrs = probeElementAttrs;
5
6
  exports.extendDashboardFixtures = extendDashboardFixtures;
6
7
  const node_zlib_1 = require("node:zlib");
7
8
  const locator_healing_js_1 = require("./locator-healing.js");
@@ -75,6 +76,75 @@ async function ariaSnapshotBestEffort(target, timeout) {
75
76
  }
76
77
  }
77
78
  }
79
+ /**
80
+ * Runs inside the browser via `evaluate()` — probes a captured element for its
81
+ * attributes, geometry, label association, and selector-uniqueness counts.
82
+ * Must stay a fully self-contained function (no references to this module's
83
+ * closure): Playwright serializes it and executes it in the page, browser-side.
84
+ * `el` is browser-context (no DOM lib in this Node package), hence `any`.
85
+ * Exported for unit testing; still passed directly to `evaluate()` below.
86
+ */
87
+ function probeElementAttrs(el, keep) {
88
+ const attrMap = {};
89
+ for (const key of keep) {
90
+ const v = el.getAttribute(key) ?? el[key];
91
+ attrMap[key] = typeof v === 'string' ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
92
+ }
93
+ const r = el.getBoundingClientRect();
94
+ // Uniqueness probe: how many elements each candidate selector matches. A
95
+ // count > 1 marks the alternative as ambiguous (strict-mode violation) so
96
+ // generateAlternatives drops it. All DOM/CSS access goes through `el` (no
97
+ // DOM lib here).
98
+ const selectorCounts = {};
99
+ try {
100
+ const doc = el.ownerDocument;
101
+ const cssEsc = (s) => doc.defaultView.CSS.escape(s);
102
+ const count = (sel) => {
103
+ try {
104
+ return doc.querySelectorAll(sel).length;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ };
110
+ if (attrMap['data-testid']) {
111
+ selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap['data-testid'])}]`);
112
+ }
113
+ if (attrMap['id'])
114
+ selectorCounts.id = count(`#${cssEsc(attrMap['id'])}`);
115
+ if (attrMap['name'])
116
+ selectorCounts.name = count(`[name=${JSON.stringify(attrMap['name'])}]`);
117
+ const classList = (attrMap['class'] || '')
118
+ .split(/\s+/)
119
+ .filter((c) => c.length > 1)
120
+ .slice(0, 10);
121
+ if (classList.length > 0) {
122
+ const classCounts = {};
123
+ for (const cls of classList) {
124
+ const n = count(`.${cssEsc(cls)}`);
125
+ if (n !== undefined)
126
+ classCounts[cls] = n;
127
+ }
128
+ selectorCounts.classes = classCounts;
129
+ }
130
+ }
131
+ catch {
132
+ // Uniqueness probing is best-effort — never fail the capture.
133
+ }
134
+ return {
135
+ tagName: el.tagName?.toLowerCase?.() ?? 'unknown',
136
+ attributes: attrMap,
137
+ // Collapse whitespace so multi-line text can't produce a getByText
138
+ // suggestion with literal newlines in it.
139
+ textContent: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80),
140
+ center: {
141
+ x: Math.round(r.x + r.width / 2),
142
+ y: Math.round(r.y + r.height / 2),
143
+ },
144
+ hasLabel: !!(el.labels && el.labels.length > 0),
145
+ selectorCounts,
146
+ };
147
+ }
78
148
  // Chain methods that take args and define a new locator scope (not just narrow).
79
149
  // Origin method/args update to the chain call, e.g. .locator('.item') → locator('.item').
80
150
  // Positional/filter chains that narrow but don't change locator identity.
@@ -140,70 +210,8 @@ function wrapLocator(page, locator, originMethod, originArgs) {
140
210
  const resolveAttrs = (async () => {
141
211
  let deadline;
142
212
  try {
143
- // `el` is browser-context (no DOM lib in this Node package), so it
144
- // stays `any`; the callback's return type pins `attrs` to CapturedAttrs.
145
213
  const attrs = await Promise.race([
146
- target.evaluate((el, keep) => {
147
- const attrMap = {};
148
- for (const key of keep) {
149
- const v = el.getAttribute(key) ?? el[key];
150
- attrMap[key] = typeof v === 'string' ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
151
- }
152
- const r = el.getBoundingClientRect();
153
- // Uniqueness probe: how many elements each candidate selector
154
- // matches. A count > 1 marks the alternative as ambiguous
155
- // (strict-mode violation) so generateAlternatives drops it.
156
- // All DOM/CSS access goes through `el` (no DOM lib here).
157
- const selectorCounts = {};
158
- try {
159
- const doc = el.ownerDocument;
160
- const cssEsc = (s) => doc.defaultView.CSS.escape(s);
161
- const count = (sel) => {
162
- try {
163
- return doc.querySelectorAll(sel).length;
164
- }
165
- catch {
166
- return undefined;
167
- }
168
- };
169
- if (attrMap['data-testid']) {
170
- selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap['data-testid'])}]`);
171
- }
172
- if (attrMap['id'])
173
- selectorCounts.id = count(`#${cssEsc(attrMap['id'])}`);
174
- if (attrMap['name'])
175
- selectorCounts.name = count(`[name=${JSON.stringify(attrMap['name'])}]`);
176
- const classList = (attrMap['class'] || '')
177
- .split(/\s+/)
178
- .filter((c) => c.length > 1)
179
- .slice(0, 10);
180
- if (classList.length > 0) {
181
- const classCounts = {};
182
- for (const cls of classList) {
183
- const n = count(`.${cssEsc(cls)}`);
184
- if (n !== undefined)
185
- classCounts[cls] = n;
186
- }
187
- selectorCounts.classes = classCounts;
188
- }
189
- }
190
- catch {
191
- // Uniqueness probing is best-effort — never fail the capture.
192
- }
193
- return {
194
- tagName: el.tagName?.toLowerCase?.() ?? 'unknown',
195
- attributes: attrMap,
196
- // Collapse whitespace so multi-line text can't produce a
197
- // getByText suggestion with literal newlines in it.
198
- textContent: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80),
199
- center: {
200
- x: Math.round(r.x + r.width / 2),
201
- y: Math.round(r.y + r.height / 2),
202
- },
203
- hasLabel: !!(el.labels && el.labels.length > 0),
204
- selectorCounts,
205
- };
206
- }, CAPTURED_ATTRS_ARG),
214
+ target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG),
207
215
  new Promise((_, reject) => {
208
216
  deadline = setTimeout(() => reject(new Error('locator capture timeout')), 500);
209
217
  }),
@@ -66,7 +66,7 @@ export declare class StreamManager {
66
66
  */
67
67
  constructor(httpClient: HttpClient, streamBuffer: StreamBuffer, recovery: CrashRecovery, uploader: Uploader, fileHandler: FileHandler, options: PiwiDashboardOptions, logger?: Logger);
68
68
  /** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
69
- start(startTime: string, metadata: Record<string, any>, instanceId: string, playwrightVersion?: string | null, shardInfo?: ShardInfo | null, isFullRun?: boolean, filterDetails?: FilterDetails | null): void;
69
+ start(startTime: string, metadata: Record<string, any>, instanceId: string, playwrightVersion?: string | null, reporterVersion?: string | null, shardInfo?: ShardInfo | null, isFullRun?: boolean, filterDetails?: FilterDetails | null): void;
70
70
  private _doStart;
71
71
  /** Queue a test-case `begin` event. Held in a pre-start buffer if the stream is not yet open, then prepended so it arrives before the matching `complete` event. */
72
72
  queueBeginEvent(event: StreamEvent): void;
@@ -111,10 +111,10 @@ class StreamManager {
111
111
  this._startPromise = null;
112
112
  }
113
113
  /** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
114
- start(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
115
- this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails);
114
+ start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
115
+ this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails);
116
116
  }
117
- async _doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
117
+ async _doStart(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
118
118
  const setupInfo = (0, setup_file_js_1.readSetupInfo)(this.options.projectName);
119
119
  try {
120
120
  this._auth = await this.httpClient.resolveAuth(this.options);
@@ -129,6 +129,7 @@ class StreamManager {
129
129
  totalTests: 0,
130
130
  metadata,
131
131
  playwrightVersion,
132
+ reporterVersion,
132
133
  shardIndex,
133
134
  shardTotal,
134
135
  isFullRun,
@@ -150,6 +151,7 @@ class StreamManager {
150
151
  metadata,
151
152
  instanceId,
152
153
  playwrightVersion,
154
+ reporterVersion,
153
155
  shardIndex,
154
156
  shardTotal,
155
157
  isFullRun,
@@ -167,6 +169,7 @@ class StreamManager {
167
169
  metadata,
168
170
  instanceId,
169
171
  playwrightVersion,
172
+ reporterVersion,
170
173
  shardIndex,
171
174
  shardTotal,
172
175
  isFullRun,
@@ -16,6 +16,7 @@ export interface CollectedRun {
16
16
  testCases: CollectedTestCase[];
17
17
  startTime: string | null;
18
18
  playwrightVersion: string | null;
19
+ reporterVersion: string | null;
19
20
  totalTests: number;
20
21
  passedTests: number;
21
22
  failedTests: number;
@@ -97,6 +97,7 @@ class RunSubmitter {
97
97
  metadata: run.metadata,
98
98
  instanceId: run.instanceId,
99
99
  playwrightVersion: run.playwrightVersion ?? undefined,
100
+ reporterVersion: run.reporterVersion ?? undefined,
100
101
  testCases: run.testCases,
101
102
  shardIndex: run.shardInfo?.current,
102
103
  shardTotal: run.shardInfo?.total,
@@ -126,6 +127,7 @@ class RunSubmitter {
126
127
  metadata: run.metadata,
127
128
  hasPendingUploads: this.hasReports(run),
128
129
  playwrightVersion: run.playwrightVersion ?? undefined,
130
+ reporterVersion: run.reporterVersion ?? undefined,
129
131
  setupSteps: run.setupSteps.length > 0 ? run.setupSteps : undefined,
130
132
  isFullRun: run.isFullRun,
131
133
  filterDetails: run.filterDetails ?? null,
@@ -94,6 +94,7 @@ function serializeRun(payload, opts) {
94
94
  metadata: payload.metadata,
95
95
  instanceId: payload.instanceId,
96
96
  playwrightVersion: payload.playwrightVersion,
97
+ reporterVersion: payload.reporterVersion,
97
98
  shardIndex: payload.shardIndex,
98
99
  shardTotal: payload.shardTotal,
99
100
  isFullRun: payload.isFullRun ?? true,
@@ -33,6 +33,8 @@ export interface RunPayload {
33
33
  testCases: CollectedTestCase[];
34
34
  /** Playwright framework version used for this run */
35
35
  playwrightVersion?: string;
36
+ /** Piwi reporter package version that produced this run */
37
+ reporterVersion?: string;
36
38
  /** 1-based shard index (e.g. 1, 2, 3) */
37
39
  shardIndex?: number;
38
40
  /** Total number of shards (e.g. 3) */
@@ -0,0 +1,2 @@
1
+ /** Read the reporter package's own version from `package.json`, memoized. Falls back to `'unknown'` if it can't be read (e.g. an unusual install layout). */
2
+ export declare function getReporterVersion(): string;
@@ -0,0 +1,54 @@
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.getReporterVersion = getReporterVersion;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ let cachedVersion = null;
40
+ /** Read the reporter package's own version from `package.json`, memoized. Falls back to `'unknown'` if it can't be read (e.g. an unusual install layout). */
41
+ function getReporterVersion() {
42
+ if (cachedVersion)
43
+ return cachedVersion;
44
+ try {
45
+ const pkgPath = path.resolve(__dirname, '../../../package.json');
46
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
47
+ const version = pkg.version;
48
+ cachedVersion = typeof version === 'string' ? version : 'unknown';
49
+ }
50
+ catch {
51
+ cachedVersion = 'unknown';
52
+ }
53
+ return cachedVersion;
54
+ }
@@ -14,6 +14,7 @@ export declare class PiwiDashboardReporter {
14
14
  private testCases;
15
15
  private startTime;
16
16
  private playwrightVersion;
17
+ private readonly reporterVersion;
17
18
  private totalTests;
18
19
  private passedTests;
19
20
  private failedTests;
@@ -46,6 +46,7 @@ const metadata_collector_js_1 = require("../internal/collect/metadata-collector.
46
46
  const stream_manager_js_1 = require("../internal/streaming/stream-manager.js");
47
47
  const step_analyzer_js_1 = require("../internal/collect/step-analyzer.js");
48
48
  const instance_id_js_1 = require("../internal/support/instance-id.js");
49
+ const reporter_version_js_1 = require("../internal/support/reporter-version.js");
49
50
  const source_snippet_js_1 = require("../internal/support/source-snippet.js");
50
51
  const ci_js_1 = require("../internal/support/ci.js");
51
52
  const worker_index_js_1 = require("../internal/support/worker-index.js");
@@ -79,6 +80,7 @@ class PiwiDashboardReporter {
79
80
  this.testCases = [];
80
81
  this.startTime = null;
81
82
  this.playwrightVersion = null;
83
+ this.reporterVersion = (0, reporter_version_js_1.getReporterVersion)();
82
84
  this.totalTests = 0;
83
85
  this.passedTests = 0;
84
86
  this.failedTests = 0;
@@ -154,7 +156,7 @@ class PiwiDashboardReporter {
154
156
  this.shardInfo = { current: pwShard.current, total: pwShard.total };
155
157
  this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
156
158
  }
157
- this.streamManager?.start(this.startTime, this.metadata, this.instanceId, this.playwrightVersion, this.shardInfo, this.isFullRun, this.filterDetails);
159
+ this.streamManager?.start(this.startTime, this.metadata, this.instanceId, this.playwrightVersion, this.reporterVersion, this.shardInfo, this.isFullRun, this.filterDetails);
158
160
  }
159
161
  /** Playwright reporter hook: called when an individual test begins */
160
162
  onTestBegin(test, result) {
@@ -353,6 +355,7 @@ class PiwiDashboardReporter {
353
355
  testCases: this.testCases,
354
356
  startTime: this.startTime,
355
357
  playwrightVersion: this.playwrightVersion,
358
+ reporterVersion: this.reporterVersion,
356
359
  totalTests: this.totalTests,
357
360
  passedTests: this.passedTests,
358
361
  failedTests: this.failedTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.7.0",
3
+ "version": "0.9.1",
4
4
  "description": "Playwright reporter for sending test results to Piwi Dashboard",
5
5
  "url": "https://github.com/PiwiTests/platform",
6
6
  "homepage": "https://piwitests.github.io",
@@ -41,7 +41,9 @@
41
41
  "reporter:lint": "oxlint --config oxlint.config.mts .",
42
42
  "reporter:lint:fix": "oxlint --config oxlint.config.mts . --fix",
43
43
  "reporter:test": "vitest run",
44
+ "reporter:test:coverage": "vitest run --coverage",
44
45
  "reporter:test:watch": "vitest",
46
+ "reporter:test:integration": "npm run reporter:build && playwright test --config=tests/integration/playwright.config.ts",
45
47
  "test": "npm run reporter:test",
46
48
  "prepublishOnly": "npm run reporter:build"
47
49
  },