@piwitests/reporter 0.4.3 → 0.5.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/dist/config.d.ts +12 -0
- package/dist/config.js +11 -0
- package/dist/fixtures.d.ts +1 -1
- package/dist/fixtures.js +182 -2
- package/dist/helpers.js +40 -16
- package/dist/index.d.ts +5 -7
- package/dist/index.js +8 -4
- package/dist/locator-healing.d.ts +152 -0
- package/dist/locator-healing.js +663 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +45 -3
- package/dist/run-submitter.js +2 -0
- package/dist/serializer.js +2 -0
- package/dist/stream-manager.d.ts +14 -0
- package/dist/stream-manager.js +51 -0
- package/dist/types.d.ts +5 -0
- package/dist/uploader.d.ts +1 -0
- package/package.json +1 -1
package/dist/reporter.js
CHANGED
|
@@ -45,10 +45,35 @@ const metadata_collector_js_1 = require("./metadata-collector.js");
|
|
|
45
45
|
const stream_manager_js_1 = require("./stream-manager.js");
|
|
46
46
|
const step_analyzer_js_1 = require("./step-analyzer.js");
|
|
47
47
|
const helpers_js_1 = require("./helpers.js");
|
|
48
|
+
const config_wrapper_js_1 = require("./config-wrapper.js");
|
|
48
49
|
const serializer_js_1 = require("./serializer.js");
|
|
49
50
|
const skip_classify_js_1 = require("./skip-classify.js");
|
|
50
51
|
const run_submitter_js_1 = require("./run-submitter.js");
|
|
51
52
|
const logger_js_1 = require("./logger.js");
|
|
53
|
+
/**
|
|
54
|
+
* Build the stored error text for a test result.
|
|
55
|
+
*
|
|
56
|
+
* Playwright's `error.message` carries the failure message and call log but no
|
|
57
|
+
* stack frames — those live on `error.stack`/`error.location`. The server's
|
|
58
|
+
* locator-healing lookup needs the failing call site to find the pre-captured
|
|
59
|
+
* snapshot for that locator, so when the message has no `at …` frame we append
|
|
60
|
+
* a synthetic one from `error.location`, relativized to the cwd so it matches
|
|
61
|
+
* the format the fixture records at capture time. The frame is appended after
|
|
62
|
+
* the message, where `extractMessageHead` already trims it off before
|
|
63
|
+
* fingerprinting — so failure clustering is unaffected.
|
|
64
|
+
*/
|
|
65
|
+
function buildErrorText(result) {
|
|
66
|
+
const err = result.error;
|
|
67
|
+
if (!err)
|
|
68
|
+
return null;
|
|
69
|
+
let text = err.message ?? '';
|
|
70
|
+
const loc = err.location;
|
|
71
|
+
if (loc?.file && !/\n\s+at\s/.test(text)) {
|
|
72
|
+
const rel = path.relative(process.cwd(), loc.file).split(path.sep).join('/');
|
|
73
|
+
text += `\n at ${rel}:${loc.line}:${loc.column}`;
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
52
77
|
/**
|
|
53
78
|
* Piwi Dashboard Playwright reporter.
|
|
54
79
|
*
|
|
@@ -81,7 +106,7 @@ class PiwiDashboardReporter {
|
|
|
81
106
|
/** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
|
|
82
107
|
this.setupSteps = [];
|
|
83
108
|
this.options = (0, config_js_1.resolveOptions)(rawOptions);
|
|
84
|
-
this.enabled = !!this.options.serverUrl;
|
|
109
|
+
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
85
110
|
this.runLabel = this.options.runLabel || (0, helpers_js_1.detectCiRunLabel)();
|
|
86
111
|
this.instanceId = (0, helpers_js_1.computeInstanceId)(this.options.projectName, this.runLabel);
|
|
87
112
|
const logger = new logger_js_1.Logger(this.options.verbose ?? false);
|
|
@@ -177,9 +202,11 @@ class PiwiDashboardReporter {
|
|
|
177
202
|
}
|
|
178
203
|
/** Playwright reporter hook: called when a step (including hook/fixture) ends */
|
|
179
204
|
onStepEnd(test, _result, step) {
|
|
205
|
+
const cat = step.category;
|
|
206
|
+
if (cat === 'pw:api')
|
|
207
|
+
return; // not surfaced as stream events; locator locations are captured in the fixture
|
|
180
208
|
if (!this.enabled || !this.streamManager?.enabled)
|
|
181
209
|
return;
|
|
182
|
-
const cat = step.category;
|
|
183
210
|
if (cat !== 'hook' && cat !== 'fixture')
|
|
184
211
|
return;
|
|
185
212
|
const workerIndex = (0, helpers_js_1.workerIndexOf)(_result);
|
|
@@ -223,7 +250,7 @@ class PiwiDashboardReporter {
|
|
|
223
250
|
location: `${relativeFilePath}:${test.location.line}:${test.location.column}`,
|
|
224
251
|
status,
|
|
225
252
|
duration: result.duration,
|
|
226
|
-
error: result
|
|
253
|
+
error: buildErrorText(result),
|
|
227
254
|
retries: result.retry,
|
|
228
255
|
workerIndex: (0, helpers_js_1.workerIndexOf)(result),
|
|
229
256
|
shardIndex: this.shardInfo?.current ?? null,
|
|
@@ -249,6 +276,20 @@ class PiwiDashboardReporter {
|
|
|
249
276
|
}
|
|
250
277
|
if (this.options.collectPerformanceMetrics && result.attachments) {
|
|
251
278
|
this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
|
|
279
|
+
// Locator snapshots arrive pre-stamped with their call-site `location`
|
|
280
|
+
// (captured in the fixture at action call time). No index correlation
|
|
281
|
+
// with pw:api steps — that was unreliable across workers/concurrent calls.
|
|
282
|
+
const locatorAttachment = this.options.captureLocators !== false
|
|
283
|
+
? result.attachments.find((a) => a.name === 'piwi-dashboard-locators')
|
|
284
|
+
: undefined;
|
|
285
|
+
if (locatorAttachment?.body) {
|
|
286
|
+
try {
|
|
287
|
+
testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
/* ignore parse errors */
|
|
291
|
+
}
|
|
292
|
+
}
|
|
252
293
|
}
|
|
253
294
|
switch (status) {
|
|
254
295
|
case 'passed':
|
|
@@ -338,4 +379,5 @@ class PiwiDashboardReporter {
|
|
|
338
379
|
}
|
|
339
380
|
}
|
|
340
381
|
exports.PiwiDashboardReporter = PiwiDashboardReporter;
|
|
382
|
+
PiwiDashboardReporter.wrapConfig = config_wrapper_js_1.wrapConfig;
|
|
341
383
|
PiwiDashboardReporter.createGlobalSetup = helpers_js_1.createGlobalSetup;
|
package/dist/run-submitter.js
CHANGED
|
@@ -87,6 +87,7 @@ class RunSubmitter {
|
|
|
87
87
|
totalTests: run.totalTests,
|
|
88
88
|
passedTests: run.passedTests,
|
|
89
89
|
failedTests: run.failedTests,
|
|
90
|
+
timedOutTests: run.timedOutTests,
|
|
90
91
|
skippedTests: run.skippedTests,
|
|
91
92
|
didNotRunTests: run.didNotRunTests,
|
|
92
93
|
environment: run.options.environment,
|
|
@@ -114,6 +115,7 @@ class RunSubmitter {
|
|
|
114
115
|
totalTests: run.totalTests,
|
|
115
116
|
passedTests: run.passedTests,
|
|
116
117
|
failedTests: run.failedTests,
|
|
118
|
+
timedOutTests: run.timedOutTests,
|
|
117
119
|
skippedTests: run.skippedTests,
|
|
118
120
|
didNotRunTests: run.didNotRunTests,
|
|
119
121
|
flakyTests,
|
package/dist/serializer.js
CHANGED
|
@@ -64,6 +64,7 @@ function toWireTestCase(tc) {
|
|
|
64
64
|
suitePath: rest.suitePath ?? null,
|
|
65
65
|
suiteConfig: rest.suiteConfig ?? null,
|
|
66
66
|
testAnnotations: rest.testAnnotations ?? null,
|
|
67
|
+
locatorSnapshots: rest.locatorSnapshots || null,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
@@ -85,6 +86,7 @@ function serializeRun(payload, opts) {
|
|
|
85
86
|
totalTests: payload.totalTests,
|
|
86
87
|
passedTests: payload.passedTests,
|
|
87
88
|
failedTests: payload.failedTests,
|
|
89
|
+
timedOutTests: payload.timedOutTests ?? 0,
|
|
88
90
|
skippedTests: payload.skippedTests,
|
|
89
91
|
didNotRunTests: payload.didNotRunTests ?? 0,
|
|
90
92
|
environment: payload.environment ?? null,
|
package/dist/stream-manager.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export declare class StreamManager {
|
|
|
28
28
|
private retryCount;
|
|
29
29
|
private retryTimer;
|
|
30
30
|
private readonly maxRetryDelay;
|
|
31
|
+
/**
|
|
32
|
+
* Idle heartbeat: while the run is open but no events are flowing (e.g. a single
|
|
33
|
+
* long test, or `beforeAll` setup), a periodic ping keeps the server's activity
|
|
34
|
+
* timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
|
|
35
|
+
* The interval must stay well below the server's stale timeout.
|
|
36
|
+
*/
|
|
37
|
+
private heartbeatTimer;
|
|
38
|
+
private heartbeatStopped;
|
|
39
|
+
private lastActivityAt;
|
|
40
|
+
private readonly heartbeatInterval;
|
|
31
41
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
32
42
|
private readonly uploadedCaseFiles;
|
|
33
43
|
private _enabled;
|
|
@@ -65,6 +75,10 @@ export declare class StreamManager {
|
|
|
65
75
|
/** Flush all pending events to the server. Returns a promise that resolves to `true` on success or `false` on failure (events are re-queued for retry). */
|
|
66
76
|
flush(): Promise<boolean> | null;
|
|
67
77
|
private scheduleRetry;
|
|
78
|
+
private scheduleHeartbeat;
|
|
79
|
+
private sendHeartbeat;
|
|
80
|
+
/** Stop the idle heartbeat permanently (called when the run is wrapping up). */
|
|
81
|
+
private stopHeartbeat;
|
|
68
82
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
69
83
|
drain(): Promise<void>;
|
|
70
84
|
/** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
|
package/dist/stream-manager.js
CHANGED
|
@@ -89,6 +89,16 @@ class StreamManager {
|
|
|
89
89
|
this.retryCount = 0;
|
|
90
90
|
this.retryTimer = null;
|
|
91
91
|
this.maxRetryDelay = 30000;
|
|
92
|
+
/**
|
|
93
|
+
* Idle heartbeat: while the run is open but no events are flowing (e.g. a single
|
|
94
|
+
* long test, or `beforeAll` setup), a periodic ping keeps the server's activity
|
|
95
|
+
* timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
|
|
96
|
+
* The interval must stay well below the server's stale timeout.
|
|
97
|
+
*/
|
|
98
|
+
this.heartbeatTimer = null;
|
|
99
|
+
this.heartbeatStopped = false;
|
|
100
|
+
this.lastActivityAt = 0;
|
|
101
|
+
this.heartbeatInterval = 15000;
|
|
92
102
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
93
103
|
this.uploadedCaseFiles = new WeakSet();
|
|
94
104
|
this._enabled = false;
|
|
@@ -165,6 +175,8 @@ class StreamManager {
|
|
|
165
175
|
this._token = response.streamToken;
|
|
166
176
|
this._enabled = true;
|
|
167
177
|
this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
|
|
178
|
+
this.lastActivityAt = Date.now();
|
|
179
|
+
this.scheduleHeartbeat();
|
|
168
180
|
if (this.pendingBeginEvents.length > 0) {
|
|
169
181
|
this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
|
|
170
182
|
this.pendingBeginEvents = [];
|
|
@@ -213,6 +225,7 @@ class StreamManager {
|
|
|
213
225
|
.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth)
|
|
214
226
|
.then(() => {
|
|
215
227
|
this.retryCount = 0;
|
|
228
|
+
this.lastActivityAt = Date.now();
|
|
216
229
|
return true;
|
|
217
230
|
}, () => {
|
|
218
231
|
this.pendingEvents = events.concat(this.pendingEvents);
|
|
@@ -239,8 +252,46 @@ class StreamManager {
|
|
|
239
252
|
this.flush();
|
|
240
253
|
}, delay);
|
|
241
254
|
}
|
|
255
|
+
// Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
|
|
256
|
+
scheduleHeartbeat() {
|
|
257
|
+
if (this.heartbeatStopped || this.heartbeatTimer)
|
|
258
|
+
return;
|
|
259
|
+
this.heartbeatTimer = setTimeout(() => {
|
|
260
|
+
this.heartbeatTimer = null;
|
|
261
|
+
void this.sendHeartbeat();
|
|
262
|
+
}, this.heartbeatInterval);
|
|
263
|
+
}
|
|
264
|
+
// Ping the server only when the run has actually been idle. Real event traffic
|
|
265
|
+
// already bumps the server's activity timestamp, so a recent flush (or pending
|
|
266
|
+
// events about to flush) makes the ping redundant — skip it and reschedule.
|
|
267
|
+
async sendHeartbeat() {
|
|
268
|
+
if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token)
|
|
269
|
+
return;
|
|
270
|
+
const idleFor = Date.now() - this.lastActivityAt;
|
|
271
|
+
if (idleFor < this.heartbeatInterval || this.pendingEvents.length > 0) {
|
|
272
|
+
this.scheduleHeartbeat();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await this.httpClient.postJSON(`/api/test-runs/${this._runId}/heartbeat`, { streamToken: this._token }, this._auth);
|
|
277
|
+
this.lastActivityAt = Date.now();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
this.logger.debug(`Heartbeat failed: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
this.scheduleHeartbeat();
|
|
283
|
+
}
|
|
284
|
+
/** Stop the idle heartbeat permanently (called when the run is wrapping up). */
|
|
285
|
+
stopHeartbeat() {
|
|
286
|
+
this.heartbeatStopped = true;
|
|
287
|
+
if (this.heartbeatTimer) {
|
|
288
|
+
clearTimeout(this.heartbeatTimer);
|
|
289
|
+
this.heartbeatTimer = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
242
292
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
243
293
|
async drain() {
|
|
294
|
+
this.stopHeartbeat();
|
|
244
295
|
if (!this._enabled) {
|
|
245
296
|
this.pendingEvents = [];
|
|
246
297
|
this.flushPromises = [];
|
package/dist/types.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ export interface CollectedTestCase {
|
|
|
142
142
|
consoleLogs?: unknown;
|
|
143
143
|
/** Parsed from `piwi-dashboard-aria-snapshot` attachment. */
|
|
144
144
|
ariaSnapshot?: string;
|
|
145
|
+
/** Parsed from `piwi-dashboard-locators` attachment. */
|
|
146
|
+
locatorSnapshots?: import('./locator-healing.js').LocatorSnapshot[];
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
149
|
* The per-case wire shape that `toWireTestCase` produces and the server
|
|
@@ -176,6 +178,8 @@ export interface WireTestCase {
|
|
|
176
178
|
/** Step-event discriminant (only for `step-begin`/`step-end` events). */
|
|
177
179
|
stepCategory?: string | null;
|
|
178
180
|
parentTitle?: string | null;
|
|
181
|
+
/** Per-element locator snapshots with ranked alternatives (transient — not stored per-run). */
|
|
182
|
+
locatorSnapshots?: unknown;
|
|
179
183
|
}
|
|
180
184
|
export interface BeginStreamEvent {
|
|
181
185
|
type: 'begin';
|
|
@@ -211,6 +215,7 @@ export interface CompleteStreamEvent {
|
|
|
211
215
|
consoleLogs?: unknown;
|
|
212
216
|
ariaSnapshot?: unknown;
|
|
213
217
|
testSource?: string | null;
|
|
218
|
+
locatorSnapshots?: unknown;
|
|
214
219
|
}
|
|
215
220
|
export interface StepBeginStreamEvent {
|
|
216
221
|
type: 'step-begin';
|
package/dist/uploader.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface RunPayload {
|
|
|
17
17
|
totalTests: number;
|
|
18
18
|
passedTests: number;
|
|
19
19
|
failedTests: number;
|
|
20
|
+
timedOutTests?: number;
|
|
20
21
|
skippedTests: number;
|
|
21
22
|
/** Tests that never executed (cut short by `maxFailures` or a serial-group failure) */
|
|
22
23
|
didNotRunTests?: number;
|
package/package.json
CHANGED