@piwitests/reporter 0.41.0 → 0.43.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 +3 -3
- package/dist/cli/index.js +1275 -469
- package/dist/global-setup-module.js +68 -24
- package/dist/index.d.ts +18 -5
- package/dist/index.js +418 -103
- package/dist/internal/capture/capture-fixtures.js +50 -7
- package/dist/internal/capture/locator-healing.d.ts +1 -7
- package/dist/internal/capture/locator-healing.js +0 -2
- package/dist/internal/capture/pick-on-failure.js +3 -3
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/setup-piwi/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +30 -15
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ var DEFAULTS = {
|
|
|
89
89
|
streamingBatchSize: 5,
|
|
90
90
|
streamingBatchDelay: 2e3,
|
|
91
91
|
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
92
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
92
93
|
failOnFlakyTests: false,
|
|
93
94
|
username: null,
|
|
94
95
|
password: null,
|
|
@@ -110,6 +111,7 @@ var PIWI_ENV_KEYS = {
|
|
|
110
111
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
111
112
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
112
113
|
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
114
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
113
115
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
114
116
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
115
117
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -143,6 +145,7 @@ var PIWI_SELECTION_ENV = {
|
|
|
143
145
|
hash: "PIWI_SELECTION_HASH",
|
|
144
146
|
count: "PIWI_SELECTION_COUNT"
|
|
145
147
|
};
|
|
148
|
+
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
146
149
|
var PIWI_PROBE_ENV = {
|
|
147
150
|
flag: "PIWI_PROBE",
|
|
148
151
|
plan: "PIWI_PROBE_PLAN",
|
|
@@ -171,6 +174,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
171
174
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
172
175
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
173
176
|
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
177
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
174
178
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
175
179
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
176
180
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -315,13 +319,17 @@ var HttpClient = class {
|
|
|
315
319
|
/**
|
|
316
320
|
* @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
|
|
317
321
|
* @param logger Prefixed logger for verbose diagnostics.
|
|
318
|
-
* @param timeout Socket inactivity timeout in ms (default 30s)
|
|
319
|
-
* server
|
|
322
|
+
* @param timeout Socket inactivity timeout in ms (default 30s), so a hung
|
|
323
|
+
* server fails fast rather than stalling the reporter.
|
|
320
324
|
*/
|
|
321
325
|
constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
|
|
322
326
|
this.serverUrl = serverUrl;
|
|
323
327
|
this.logger = logger;
|
|
324
328
|
this.timeout = timeout;
|
|
329
|
+
/** Requests in flight, so `close` can abort them. */
|
|
330
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
331
|
+
/** Set by `close`: every later request fails with this reason. */
|
|
332
|
+
this.closedReason = null;
|
|
325
333
|
this.warnIfInsecureTransport();
|
|
326
334
|
}
|
|
327
335
|
/**
|
|
@@ -332,7 +340,7 @@ var HttpClient = class {
|
|
|
332
340
|
* endpoint exposes the key and payloads on the wire. Loopback (localhost /
|
|
333
341
|
* 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
|
|
334
342
|
* warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
|
|
335
|
-
*
|
|
343
|
+
* work, while the exposure is still reported.
|
|
336
344
|
*/
|
|
337
345
|
warnIfInsecureTransport() {
|
|
338
346
|
let url;
|
|
@@ -353,6 +361,16 @@ var HttpClient = class {
|
|
|
353
361
|
get baseUrl() {
|
|
354
362
|
return this.serverUrl;
|
|
355
363
|
}
|
|
364
|
+
/** Whether `close` was called. */
|
|
365
|
+
get closed() {
|
|
366
|
+
return this.closedReason !== null;
|
|
367
|
+
}
|
|
368
|
+
/** Abort every request in flight and fail every later one with `reason`. */
|
|
369
|
+
close(reason) {
|
|
370
|
+
this.closedReason = reason;
|
|
371
|
+
for (const req of this.inFlight) req.destroy(new Error(reason));
|
|
372
|
+
this.inFlight.clear();
|
|
373
|
+
}
|
|
356
374
|
/**
|
|
357
375
|
* Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
|
|
358
376
|
* or return `null` when neither is configured.
|
|
@@ -473,6 +491,10 @@ var HttpClient = class {
|
|
|
473
491
|
/** Unified request core: transport, headers, auth, response accumulation, timeout. */
|
|
474
492
|
request(method, pathname, opts) {
|
|
475
493
|
return new Promise((resolve6, reject) => {
|
|
494
|
+
if (this.closedReason !== null) {
|
|
495
|
+
reject(new Error(this.closedReason));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
476
498
|
const url = new import_node_url.URL(pathname, this.serverUrl);
|
|
477
499
|
const transport = url.protocol === "https:" ? https : http;
|
|
478
500
|
const headers = { ...opts.headers };
|
|
@@ -501,11 +523,14 @@ var HttpClient = class {
|
|
|
501
523
|
});
|
|
502
524
|
}
|
|
503
525
|
);
|
|
526
|
+
this.inFlight.add(req);
|
|
527
|
+
req.on("close", () => this.inFlight.delete(req));
|
|
504
528
|
req.on("error", reject);
|
|
505
529
|
req.setTimeout(this.timeout, () => {
|
|
506
530
|
req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
|
|
507
531
|
});
|
|
508
532
|
if (opts.form) {
|
|
533
|
+
opts.form.on("error", (error) => req.destroy(error));
|
|
509
534
|
opts.form.pipe(req);
|
|
510
535
|
} else if (opts.body !== void 0) {
|
|
511
536
|
req.write(opts.body);
|
|
@@ -864,19 +889,25 @@ function computeInstanceId(projectName, runLabel) {
|
|
|
864
889
|
// src/internal/streaming/stream-buffer.ts
|
|
865
890
|
var StreamBuffer = class {
|
|
866
891
|
constructor(projectName) {
|
|
867
|
-
this.filePath =
|
|
892
|
+
this.filePath = null;
|
|
893
|
+
this.prefix = `piwi-dashboard-stream-${hashForProject(projectName)}`;
|
|
894
|
+
}
|
|
895
|
+
/** Scope the buffer to a run. Until then the buffer is empty and appends are dropped. */
|
|
896
|
+
bindRun(runId) {
|
|
897
|
+
this.filePath = path3.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
|
|
868
898
|
}
|
|
869
899
|
/** Append one or more events to the on-disk buffer */
|
|
870
900
|
append(events) {
|
|
871
|
-
if (events.length === 0) return;
|
|
901
|
+
if (!this.filePath || events.length === 0) return;
|
|
872
902
|
try {
|
|
873
903
|
const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
|
|
874
904
|
fs3.appendFileSync(this.filePath, lines, "utf8");
|
|
875
905
|
} catch {
|
|
876
906
|
}
|
|
877
907
|
}
|
|
878
|
-
/** Load all buffered events
|
|
908
|
+
/** Load all buffered events of the bound run from disk */
|
|
879
909
|
load() {
|
|
910
|
+
if (!this.filePath) return [];
|
|
880
911
|
try {
|
|
881
912
|
if (fs3.existsSync(this.filePath)) {
|
|
882
913
|
const content = fs3.readFileSync(this.filePath, "utf8");
|
|
@@ -886,23 +917,29 @@ var StreamBuffer = class {
|
|
|
886
917
|
}
|
|
887
918
|
return [];
|
|
888
919
|
}
|
|
889
|
-
/** Delete the buffer file from disk */
|
|
920
|
+
/** Delete the bound run's buffer file from disk */
|
|
890
921
|
clear() {
|
|
922
|
+
if (!this.filePath) return;
|
|
891
923
|
try {
|
|
892
924
|
if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
|
|
893
925
|
} catch {
|
|
894
926
|
}
|
|
895
927
|
}
|
|
896
|
-
/** Remove
|
|
928
|
+
/** Remove this project's buffer files older than `maxAgeMs` (default 2 hours). Used on startup to discard orphaned data. */
|
|
897
929
|
clearStale(maxAgeMs = 72e5) {
|
|
930
|
+
const dir = os3.tmpdir();
|
|
931
|
+
let names;
|
|
898
932
|
try {
|
|
899
|
-
|
|
900
|
-
const stats = fs3.statSync(this.filePath);
|
|
901
|
-
if (Date.now() - stats.mtimeMs > maxAgeMs) {
|
|
902
|
-
fs3.unlinkSync(this.filePath);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
933
|
+
names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
|
|
905
934
|
} catch {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
for (const name of names) {
|
|
938
|
+
try {
|
|
939
|
+
const file = path3.join(dir, name);
|
|
940
|
+
if (Date.now() - fs3.statSync(file).mtimeMs > maxAgeMs) fs3.unlinkSync(file);
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
906
943
|
}
|
|
907
944
|
}
|
|
908
945
|
};
|
|
@@ -1331,7 +1368,7 @@ var MetadataCollector = class {
|
|
|
1331
1368
|
if (options.tags && Array.isArray(options.tags)) metadata.tags = options.tags;
|
|
1332
1369
|
if (options.customData) metadata.customData = options.customData;
|
|
1333
1370
|
if (options.collectScmInfo) {
|
|
1334
|
-
const scm = this.collectScmInfo(
|
|
1371
|
+
const scm = this.collectScmInfo();
|
|
1335
1372
|
if (scm) metadata.scm = scm;
|
|
1336
1373
|
}
|
|
1337
1374
|
if (options.collectCiInfo) {
|
|
@@ -1425,7 +1462,7 @@ var MetadataCollector = class {
|
|
|
1425
1462
|
}
|
|
1426
1463
|
return { suitePath, suiteConfig };
|
|
1427
1464
|
}
|
|
1428
|
-
collectScmInfo(
|
|
1465
|
+
collectScmInfo() {
|
|
1429
1466
|
const scm = {};
|
|
1430
1467
|
let gitBranch;
|
|
1431
1468
|
try {
|
|
@@ -1546,7 +1583,7 @@ var CRITICAL_TIER = 2;
|
|
|
1546
1583
|
var BoundedEventQueue = class {
|
|
1547
1584
|
/**
|
|
1548
1585
|
* @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
|
|
1549
|
-
* disables the bound (unbounded
|
|
1586
|
+
* disables the bound (unbounded).
|
|
1550
1587
|
*/
|
|
1551
1588
|
constructor(maxBytes) {
|
|
1552
1589
|
this.maxBytes = maxBytes;
|
|
@@ -1709,10 +1746,14 @@ function runUrl(serverUrl, runId) {
|
|
|
1709
1746
|
}
|
|
1710
1747
|
|
|
1711
1748
|
// src/internal/streaming/stream-manager.ts
|
|
1749
|
+
var SERVER_MAX_EVENT_BATCH_BYTES = 10 * 1024 * 1024;
|
|
1750
|
+
var EVENT_ENVELOPE_BYTES = 1024;
|
|
1751
|
+
var MAX_EVENT_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
1752
|
+
var MAX_UPLOAD_CONNECTION_FAILURES = 3;
|
|
1712
1753
|
var StreamManager = class {
|
|
1713
1754
|
/**
|
|
1714
1755
|
* @param httpClient HTTP client for server communication.
|
|
1715
|
-
* @param streamBuffer On-disk buffer for
|
|
1756
|
+
* @param streamBuffer On-disk buffer for the run's undelivered events.
|
|
1716
1757
|
* @param recovery Crash-recovery handler for uploading stale payloads on startup.
|
|
1717
1758
|
* @param uploader Uploader for per-test-case file uploads.
|
|
1718
1759
|
* @param fileHandler File-discovery helper for finding traces and attachments.
|
|
@@ -1748,6 +1789,8 @@ var StreamManager = class {
|
|
|
1748
1789
|
this.heartbeatInterval = 15e3;
|
|
1749
1790
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
1750
1791
|
this.uploadedCaseFiles = /* @__PURE__ */ new WeakSet();
|
|
1792
|
+
/** Set when a test-result event left the live stream undelivered: too large to send, or still queued when the drain gave up. */
|
|
1793
|
+
this.resultsUndelivered = false;
|
|
1751
1794
|
this._enabled = false;
|
|
1752
1795
|
this._runId = null;
|
|
1753
1796
|
this._token = null;
|
|
@@ -1778,13 +1821,15 @@ var StreamManager = class {
|
|
|
1778
1821
|
return this._startPromise;
|
|
1779
1822
|
}
|
|
1780
1823
|
/**
|
|
1781
|
-
* Whether
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1824
|
+
* Whether a test-result (`complete`) event never reached the server through
|
|
1825
|
+
* the live stream: shed under buffer pressure, too large for any request the
|
|
1826
|
+
* server accepts, or still undelivered when the drain gave up. When true the
|
|
1827
|
+
* submitter finalizes via the end-of-run batch (which re-sends the full run
|
|
1828
|
+
* from the reporter's own memory) instead of `/finish`, so the missing
|
|
1829
|
+
* results are not lost.
|
|
1785
1830
|
*/
|
|
1786
|
-
get
|
|
1787
|
-
return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1831
|
+
get lostResults() {
|
|
1832
|
+
return this.resultsUndelivered || this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1788
1833
|
}
|
|
1789
1834
|
/** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
|
|
1790
1835
|
start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails, plannedTotal) {
|
|
@@ -1877,6 +1922,7 @@ var StreamManager = class {
|
|
|
1877
1922
|
this._runId = response.runId;
|
|
1878
1923
|
this._token = response.streamToken;
|
|
1879
1924
|
this._enabled = true;
|
|
1925
|
+
this.streamBuffer.bindRun(response.runId);
|
|
1880
1926
|
this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
|
|
1881
1927
|
if (this.options.serverUrl) {
|
|
1882
1928
|
this.logger.info(`Watch live: ${runUrl(this.options.serverUrl, response.runId)}`);
|
|
@@ -1927,21 +1973,78 @@ var StreamManager = class {
|
|
|
1927
1973
|
this.flushTimer = null;
|
|
1928
1974
|
}
|
|
1929
1975
|
if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
|
|
1976
|
+
const promise = this.sendEvents(this.pendingEvents.takeAll());
|
|
1977
|
+
this.flushPromises.push(promise);
|
|
1978
|
+
return promise;
|
|
1979
|
+
}
|
|
1980
|
+
// Post the events in order, in requests of at most `MAX_EVENT_REQUEST_BYTES`.
|
|
1981
|
+
// A request the server refuses as too large (413) is split in half and resent;
|
|
1982
|
+
// a single event it still refuses leaves the live stream. On any other failure
|
|
1983
|
+
// the unsent events go back to the head of the queue and a retry is scheduled.
|
|
1984
|
+
async sendEvents(events) {
|
|
1985
|
+
const chunks = this.chunkEvents(events);
|
|
1986
|
+
while (chunks.length > 0) {
|
|
1987
|
+
const chunk = chunks.shift();
|
|
1988
|
+
try {
|
|
1989
|
+
await this.httpClient.postJSON(
|
|
1990
|
+
`/api/test-runs/${this._runId}/events`,
|
|
1991
|
+
{ streamToken: this._token, testCases: chunk },
|
|
1992
|
+
this._auth
|
|
1993
|
+
);
|
|
1933
1994
|
this.retryCount = 0;
|
|
1934
1995
|
this.lastActivityAt = Date.now();
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
if (error instanceof HttpError && error.status === 413) {
|
|
1998
|
+
if (chunk.length > 1) {
|
|
1999
|
+
const half = Math.ceil(chunk.length / 2);
|
|
2000
|
+
chunks.unshift(chunk.slice(0, half), chunk.slice(half));
|
|
2001
|
+
} else {
|
|
2002
|
+
this.dropOversized(chunk[0]);
|
|
2003
|
+
}
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
this.pendingEvents.prepend(chunk.concat(...chunks));
|
|
1939
2007
|
this.scheduleRetry(errorMessage(error));
|
|
1940
2008
|
return false;
|
|
1941
2009
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2010
|
+
}
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
// Group events into request-sized chunks, keeping their order. An event
|
|
2014
|
+
// larger than any request the server accepts is dropped here.
|
|
2015
|
+
chunkEvents(events) {
|
|
2016
|
+
const chunks = [];
|
|
2017
|
+
let chunk = [];
|
|
2018
|
+
let chunkBytes = 0;
|
|
2019
|
+
for (const event of events) {
|
|
2020
|
+
const bytes = serializedBytes(event);
|
|
2021
|
+
if (bytes > SERVER_MAX_EVENT_BATCH_BYTES - EVENT_ENVELOPE_BYTES) {
|
|
2022
|
+
this.dropOversized(event);
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
if (chunk.length > 0 && chunkBytes + bytes > MAX_EVENT_REQUEST_BYTES) {
|
|
2026
|
+
chunks.push(chunk);
|
|
2027
|
+
chunk = [];
|
|
2028
|
+
chunkBytes = 0;
|
|
2029
|
+
}
|
|
2030
|
+
chunk.push(event);
|
|
2031
|
+
chunkBytes += bytes;
|
|
2032
|
+
}
|
|
2033
|
+
if (chunk.length > 0) chunks.push(chunk);
|
|
2034
|
+
return chunks;
|
|
2035
|
+
}
|
|
2036
|
+
// Leave an event the server will not accept out of the live stream. A test
|
|
2037
|
+
// result dropped this way sends the run through the end-of-run batch upload,
|
|
2038
|
+
// which carries the full run (see `lostResults`).
|
|
2039
|
+
dropOversized(event) {
|
|
2040
|
+
if (event.type === "complete") {
|
|
2041
|
+
this.resultsUndelivered = true;
|
|
2042
|
+
this.logger.warn(
|
|
2043
|
+
`The live result of "${event.title}" is too large for the dashboard's event stream \u2014 it is sent with the end-of-run upload instead.`
|
|
2044
|
+
);
|
|
2045
|
+
} else {
|
|
2046
|
+
this.logger.debug(`Dropped a ${event.type} event for "${event.title}" that is too large to stream.`);
|
|
2047
|
+
}
|
|
1945
2048
|
}
|
|
1946
2049
|
scheduleRetry(reason) {
|
|
1947
2050
|
if (this.retryTimer) return;
|
|
@@ -2006,7 +2109,7 @@ var StreamManager = class {
|
|
|
2006
2109
|
this.retryTimer = null;
|
|
2007
2110
|
}
|
|
2008
2111
|
}
|
|
2009
|
-
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
2112
|
+
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off, until the HTTP client is closed. */
|
|
2010
2113
|
async drain() {
|
|
2011
2114
|
try {
|
|
2012
2115
|
await this._drain();
|
|
@@ -2039,6 +2142,7 @@ var StreamManager = class {
|
|
|
2039
2142
|
}
|
|
2040
2143
|
return;
|
|
2041
2144
|
}
|
|
2145
|
+
if (this.httpClient.closed) break;
|
|
2042
2146
|
if (attempt === 0) {
|
|
2043
2147
|
this.logger.warn(
|
|
2044
2148
|
`The dashboard has not accepted ${this.pendingEvents.length} live event(s) yet \u2014 retrying delivery before the final submit (this can take a few minutes)...`
|
|
@@ -2051,6 +2155,7 @@ var StreamManager = class {
|
|
|
2051
2155
|
}
|
|
2052
2156
|
if (!this.pendingEvents.isEmpty) {
|
|
2053
2157
|
const remaining = this.pendingEvents.takeAll();
|
|
2158
|
+
if (remaining.some((event) => event.type === "complete")) this.resultsUndelivered = true;
|
|
2054
2159
|
this.logger.warn(
|
|
2055
2160
|
`Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
|
|
2056
2161
|
);
|
|
@@ -2060,6 +2165,10 @@ var StreamManager = class {
|
|
|
2060
2165
|
this.clearRetryTimer();
|
|
2061
2166
|
}
|
|
2062
2167
|
}
|
|
2168
|
+
/** Delete the run's on-disk event buffer. Called once the run's results reached the server. */
|
|
2169
|
+
discardBuffered() {
|
|
2170
|
+
this.streamBuffer.clear();
|
|
2171
|
+
}
|
|
2063
2172
|
// Emit a single summary when buffer pressure shed events, so a full disk or a
|
|
2064
2173
|
// stalled server does not silently swallow data. Reported once per drain.
|
|
2065
2174
|
reportBufferPressure() {
|
|
@@ -2124,13 +2233,18 @@ var StreamManager = class {
|
|
|
2124
2233
|
})();
|
|
2125
2234
|
this.liveUploadPromises.push(promise);
|
|
2126
2235
|
}
|
|
2127
|
-
/**
|
|
2236
|
+
/**
|
|
2237
|
+
* Wait for all live uploads to settle, then upload files for any test cases
|
|
2238
|
+
* that weren't uploaded live. Stops after `MAX_UPLOAD_CONNECTION_FAILURES`
|
|
2239
|
+
* uploads in a row got no response at all.
|
|
2240
|
+
*/
|
|
2128
2241
|
async uploadRemaining(testCases) {
|
|
2129
2242
|
if (this.liveUploadPromises.length > 0) {
|
|
2130
2243
|
await Promise.allSettled(this.liveUploadPromises);
|
|
2131
2244
|
this.liveUploadPromises = [];
|
|
2132
2245
|
}
|
|
2133
2246
|
if (!this._enabled || !this._runId || !this._token) return;
|
|
2247
|
+
let connectionFailures = 0;
|
|
2134
2248
|
for (const tc of testCases) {
|
|
2135
2249
|
if (this.uploadedCaseFiles.has(tc)) continue;
|
|
2136
2250
|
try {
|
|
@@ -2143,12 +2257,25 @@ var StreamManager = class {
|
|
|
2143
2257
|
this._auth
|
|
2144
2258
|
);
|
|
2145
2259
|
if (uploaded) this.uploadedCaseFiles.add(tc);
|
|
2260
|
+
connectionFailures = 0;
|
|
2146
2261
|
} catch (error) {
|
|
2147
2262
|
this.logger.warn(`Failed to upload files for "${tc.title}": ${errorMessage(error)}`);
|
|
2263
|
+
connectionFailures = error instanceof HttpError ? 0 : connectionFailures + 1;
|
|
2264
|
+
if (connectionFailures >= MAX_UPLOAD_CONNECTION_FAILURES) {
|
|
2265
|
+
this.logger.warn(`The dashboard stopped responding \u2014 skipping the file uploads of the remaining tests.`);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2148
2268
|
}
|
|
2149
2269
|
}
|
|
2150
2270
|
}
|
|
2151
2271
|
};
|
|
2272
|
+
function serializedBytes(event) {
|
|
2273
|
+
try {
|
|
2274
|
+
return Buffer.byteLength(JSON.stringify(event)) + 1;
|
|
2275
|
+
} catch {
|
|
2276
|
+
return 0;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2152
2279
|
|
|
2153
2280
|
// ../core/src/locator-chain.ts
|
|
2154
2281
|
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -2600,15 +2727,19 @@ function readChain(region) {
|
|
|
2600
2727
|
const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
|
|
2601
2728
|
return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
|
|
2602
2729
|
}
|
|
2603
|
-
function
|
|
2730
|
+
function extractStackFrames(text) {
|
|
2604
2731
|
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
2732
|
+
const frames = [];
|
|
2605
2733
|
let m;
|
|
2606
2734
|
while ((m = frameRe.exec(text)) !== null) {
|
|
2607
2735
|
const file = m[1].replace(/\\/g, "/");
|
|
2608
2736
|
if (file.includes("node_modules") || file.startsWith("node:")) continue;
|
|
2609
|
-
|
|
2737
|
+
frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
|
|
2610
2738
|
}
|
|
2611
|
-
return
|
|
2739
|
+
return frames;
|
|
2740
|
+
}
|
|
2741
|
+
function extractTopFrame(text) {
|
|
2742
|
+
return extractStackFrames(text)[0] ?? null;
|
|
2612
2743
|
}
|
|
2613
2744
|
function extractTopFrameFile(text) {
|
|
2614
2745
|
return extractTopFrame(text)?.file ?? null;
|
|
@@ -2857,6 +2988,13 @@ function maskTokenLike(text) {
|
|
|
2857
2988
|
return text.replace(DATA_URI_RE, "data:[masked]").replace(JWT_RE, "[masked-token]").replace(LONG_HEX_RE, "[masked-hex]");
|
|
2858
2989
|
}
|
|
2859
2990
|
|
|
2991
|
+
// ../core/src/locator-break.ts
|
|
2992
|
+
function sameFilePath(a, b) {
|
|
2993
|
+
const x = a.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2994
|
+
const y = b.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2995
|
+
return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2860
2998
|
// ../core/src/step-tree.ts
|
|
2861
2999
|
var SPAN_TOLERANCE_MS = 1;
|
|
2862
3000
|
var SETUP_CONTAINER_RE = /^before hooks$/i;
|
|
@@ -2878,9 +3016,26 @@ function errorMessage2(step) {
|
|
|
2878
3016
|
if (typeof error === "string") return error;
|
|
2879
3017
|
return str(error?.message);
|
|
2880
3018
|
}
|
|
3019
|
+
function errorLocation(step) {
|
|
3020
|
+
const error = step.error;
|
|
3021
|
+
return typeof error === "string" ? "" : str(error?.location);
|
|
3022
|
+
}
|
|
3023
|
+
function parseLocation(location) {
|
|
3024
|
+
const m = /^(.+):(\d+):(\d+)$/.exec(location.trim());
|
|
3025
|
+
return m ? { file: m[1], line: Number(m[2]), column: Number(m[3]) } : null;
|
|
3026
|
+
}
|
|
3027
|
+
function sameCodeLocation(a, b) {
|
|
3028
|
+
const x = typeof a === "string" ? parseLocation(a) : a;
|
|
3029
|
+
const y = typeof b === "string" ? parseLocation(b) : b;
|
|
3030
|
+
if (!x || !y) return false;
|
|
3031
|
+
return x.line === y.line && x.column === y.column && sameFilePath(x.file, y.file);
|
|
3032
|
+
}
|
|
2881
3033
|
function isFailedStep(step) {
|
|
2882
3034
|
return step.failed === true || errorMessage2(step).trim().length > 0;
|
|
2883
3035
|
}
|
|
3036
|
+
function isRecoveredStep(step) {
|
|
3037
|
+
return step.recovered === true;
|
|
3038
|
+
}
|
|
2884
3039
|
function isCaptureStep(step) {
|
|
2885
3040
|
const title = str(step.title).trim();
|
|
2886
3041
|
if (CAPTURE_ATTACH_RE.test(title) || CAPTURE_FIXTURE_RE.test(title)) return true;
|
|
@@ -2935,18 +3090,72 @@ function stepPhases(steps, parents = stepParents(steps)) {
|
|
|
2935
3090
|
return "body";
|
|
2936
3091
|
});
|
|
2937
3092
|
}
|
|
2938
|
-
function
|
|
3093
|
+
function firstLine(message) {
|
|
3094
|
+
return stripAnsi(str(message)).split("\n")[0].trim();
|
|
3095
|
+
}
|
|
3096
|
+
function errorHeads(error) {
|
|
3097
|
+
return error.split(/\n---\n/).map(firstLine).filter((head) => head.length > 0);
|
|
3098
|
+
}
|
|
3099
|
+
function sameError(inner, outer) {
|
|
3100
|
+
const a = firstLine(errorMessage2(inner));
|
|
3101
|
+
const b = firstLine(errorMessage2(outer));
|
|
3102
|
+
if (!a || !b) return true;
|
|
3103
|
+
if (a !== b) return false;
|
|
3104
|
+
const at = errorLocation(inner);
|
|
3105
|
+
const bt = errorLocation(outer);
|
|
3106
|
+
return !at || !bt || sameCodeLocation(at, bt);
|
|
3107
|
+
}
|
|
3108
|
+
function lastByPhase(indices, phases) {
|
|
3109
|
+
for (const phase of ["body", "setup", "teardown"]) {
|
|
3110
|
+
const inPhase = indices.filter((i) => phases[i] === phase);
|
|
3111
|
+
if (inPhase.length > 0) return inPhase[inPhase.length - 1];
|
|
3112
|
+
}
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
function failingChainEnds(steps, parents) {
|
|
2939
3116
|
const candidates = [];
|
|
2940
3117
|
steps.forEach((step, i) => {
|
|
2941
|
-
if (isFailedStep(step) && !isCaptureStep(step)) candidates.push(i);
|
|
3118
|
+
if (isFailedStep(step) && !isCaptureStep(step) && !isRecoveredStep(step)) candidates.push(i);
|
|
2942
3119
|
});
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3120
|
+
const isCandidate = new Set(candidates);
|
|
3121
|
+
const hasFailedAncestor = (i) => {
|
|
3122
|
+
for (let p = parents[i] ?? -1; p !== -1; p = parents[p] ?? -1) if (isCandidate.has(p)) return true;
|
|
3123
|
+
return false;
|
|
3124
|
+
};
|
|
3125
|
+
return candidates.filter((root) => !hasFailedAncestor(root)).map((root) => {
|
|
3126
|
+
let current = root;
|
|
3127
|
+
for (; ; ) {
|
|
3128
|
+
const inner = candidates.filter((c) => c > current && isDescendant(c, current, parents));
|
|
3129
|
+
const carrying = inner.filter((c) => sameError(steps[c], steps[current]));
|
|
3130
|
+
if (carrying.length === 0) return current;
|
|
3131
|
+
current = carrying[carrying.length - 1];
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
function failingStepIndex(steps, parents = stepParents(steps), error) {
|
|
3136
|
+
const ends = failingChainEnds(steps, parents);
|
|
3137
|
+
if (ends.length === 0) return null;
|
|
3138
|
+
const phases = stepPhases(steps, parents);
|
|
3139
|
+
if (error) {
|
|
3140
|
+
const heads = errorHeads(error);
|
|
3141
|
+
const headOf = (i) => firstLine(errorMessage2(steps[i]));
|
|
3142
|
+
const frame = extractTopFrame(error);
|
|
3143
|
+
if (frame) {
|
|
3144
|
+
const thrownThere = ends.filter(
|
|
3145
|
+
(i) => heads.includes(headOf(i)) && sameCodeLocation(errorLocation(steps[i]) || str(steps[i].location), frame)
|
|
3146
|
+
);
|
|
3147
|
+
const located = lastByPhase(thrownThere, phases);
|
|
3148
|
+
if (located !== null) return located;
|
|
3149
|
+
}
|
|
3150
|
+
for (const head of heads) {
|
|
3151
|
+
const matching = lastByPhase(
|
|
3152
|
+
ends.filter((i) => headOf(i) === head),
|
|
3153
|
+
phases
|
|
3154
|
+
);
|
|
3155
|
+
if (matching !== null) return matching;
|
|
3156
|
+
}
|
|
2949
3157
|
}
|
|
3158
|
+
return lastByPhase(ends, phases) ?? ends[ends.length - 1];
|
|
2950
3159
|
}
|
|
2951
3160
|
function stepHookName(step) {
|
|
2952
3161
|
const title = str(step.title).trim();
|
|
@@ -2959,11 +3168,8 @@ function stepHookName(step) {
|
|
|
2959
3168
|
if (category === "hook" && title && !isPhaseContainer(step)) return `hook "${title}"`;
|
|
2960
3169
|
return null;
|
|
2961
3170
|
}
|
|
2962
|
-
function firstLine(message) {
|
|
2963
|
-
return stripAnsi(str(message)).split("\n")[0].trim();
|
|
2964
|
-
}
|
|
2965
3171
|
function failureHookContext(steps, error, parents = stepParents(steps)) {
|
|
2966
|
-
const index = failingStepIndex(steps, parents);
|
|
3172
|
+
const index = failingStepIndex(steps, parents, error);
|
|
2967
3173
|
if (index === null) return null;
|
|
2968
3174
|
const phase = stepPhases(steps, parents)[index];
|
|
2969
3175
|
if (phase === "body") return null;
|
|
@@ -3029,6 +3235,28 @@ function normalizeStepParams(raw) {
|
|
|
3029
3235
|
}
|
|
3030
3236
|
return count > 0 ? out : void 0;
|
|
3031
3237
|
}
|
|
3238
|
+
function locationText(location) {
|
|
3239
|
+
if (!location || typeof location.file !== "string" || !location.file) return void 0;
|
|
3240
|
+
return `${location.file}:${location.line}:${location.column}`;
|
|
3241
|
+
}
|
|
3242
|
+
function markRecoveredSteps(steps, errors) {
|
|
3243
|
+
const fatal = errors.map((error) => ({
|
|
3244
|
+
head: firstErrorLine(error.message),
|
|
3245
|
+
location: locationText(error.location)
|
|
3246
|
+
}));
|
|
3247
|
+
for (const step of steps) {
|
|
3248
|
+
if (!step.error) continue;
|
|
3249
|
+
const head = firstErrorLine(step.error.message);
|
|
3250
|
+
const thrownAt = step.error.location;
|
|
3251
|
+
const isFatal = fatal.some(
|
|
3252
|
+
(error) => error.head === head && (!error.location || !thrownAt || sameCodeLocation(error.location, thrownAt))
|
|
3253
|
+
);
|
|
3254
|
+
if (!isFatal) step.recovered = true;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
function firstErrorLine(message) {
|
|
3258
|
+
return stripAnsi(typeof message === "string" ? message : "").split("\n")[0].trim();
|
|
3259
|
+
}
|
|
3032
3260
|
function flattenSteps(steps, depth = 0) {
|
|
3033
3261
|
const result = [];
|
|
3034
3262
|
for (const step of steps) {
|
|
@@ -3043,17 +3271,21 @@ function flattenSteps(steps, depth = 0) {
|
|
|
3043
3271
|
if (params) flat.params = params;
|
|
3044
3272
|
if (step.error?.message) {
|
|
3045
3273
|
flat.error = { message: step.error.message };
|
|
3274
|
+
const thrownAt = locationText(step.error.location);
|
|
3275
|
+
if (thrownAt) flat.error.location = thrownAt;
|
|
3046
3276
|
flat.failed = true;
|
|
3047
3277
|
}
|
|
3048
|
-
|
|
3278
|
+
const location = locationText(step.location);
|
|
3279
|
+
if (location) flat.location = location;
|
|
3049
3280
|
if (step.startTime) flat.startTime = step.startTime instanceof Date ? step.startTime.getTime() : step.startTime;
|
|
3050
3281
|
result.push(flat);
|
|
3051
3282
|
if (step.steps?.length > 0) result.push(...flattenSteps(step.steps, depth + 1));
|
|
3052
3283
|
}
|
|
3053
3284
|
return result;
|
|
3054
3285
|
}
|
|
3055
|
-
function collectStepMetrics(steps) {
|
|
3286
|
+
function collectStepMetrics(steps, errors) {
|
|
3056
3287
|
const flatSteps = flattenSteps(steps);
|
|
3288
|
+
if (errors) markRecoveredSteps(flatSteps, errors);
|
|
3057
3289
|
const totalStepDuration = steps.reduce((sum, s) => sum + (s.duration || 0), 0);
|
|
3058
3290
|
let slowestStep = null;
|
|
3059
3291
|
for (const s of flatSteps) {
|
|
@@ -3292,22 +3524,30 @@ function extractFailingLine(errorText, testFile2, declarationLine) {
|
|
|
3292
3524
|
}
|
|
3293
3525
|
|
|
3294
3526
|
// src/internal/support/ci.ts
|
|
3295
|
-
function
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
if (env.
|
|
3301
|
-
if (env.
|
|
3302
|
-
if (env.
|
|
3303
|
-
if (env.
|
|
3304
|
-
if (env.
|
|
3305
|
-
if (env.
|
|
3306
|
-
if (env.
|
|
3307
|
-
if (env.
|
|
3308
|
-
if (env.
|
|
3527
|
+
function detectCiIds(env) {
|
|
3528
|
+
if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
|
|
3529
|
+
const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
|
|
3530
|
+
return { run, job: env.GITHUB_JOB || null };
|
|
3531
|
+
}
|
|
3532
|
+
if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
|
|
3533
|
+
if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
|
|
3534
|
+
if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
|
|
3535
|
+
if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
|
|
3536
|
+
if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
|
|
3537
|
+
if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
|
|
3538
|
+
if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
|
|
3539
|
+
if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
|
|
3540
|
+
if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
|
|
3541
|
+
if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
|
|
3542
|
+
if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
|
|
3309
3543
|
return null;
|
|
3310
3544
|
}
|
|
3545
|
+
function resolveRunLabel(configured, sharded) {
|
|
3546
|
+
if (configured) return configured;
|
|
3547
|
+
const ci = detectCiIds(process.env);
|
|
3548
|
+
if (!ci) return null;
|
|
3549
|
+
return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
|
|
3550
|
+
}
|
|
3311
3551
|
|
|
3312
3552
|
// src/internal/support/worker-index.ts
|
|
3313
3553
|
function workerIndexOf(result) {
|
|
@@ -3363,6 +3603,20 @@ function readSelectionStamp(env = process.env) {
|
|
|
3363
3603
|
return { key, version, resolvedHash, resolvedCount };
|
|
3364
3604
|
}
|
|
3365
3605
|
|
|
3606
|
+
// src/internal/support/shard-info.ts
|
|
3607
|
+
function parseShardSpec(raw) {
|
|
3608
|
+
const match = raw?.trim().match(/^(\d+)\s*\/\s*(\d+)$/);
|
|
3609
|
+
if (!match) return null;
|
|
3610
|
+
const current = Number(match[1]);
|
|
3611
|
+
const total = Number(match[2]);
|
|
3612
|
+
return current >= 1 && current <= total && total > 1 ? { current, total } : null;
|
|
3613
|
+
}
|
|
3614
|
+
function resolveShardInfo(config, env = process.env) {
|
|
3615
|
+
const pwShard = config.shard;
|
|
3616
|
+
if (pwShard && pwShard.total > 1) return { current: pwShard.current, total: pwShard.total };
|
|
3617
|
+
return parseShardSpec(env[PIWI_SHARD_ENV]);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3366
3620
|
// src/internal/support/run-mode.ts
|
|
3367
3621
|
var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
|
|
3368
3622
|
function isUiMode(argv = process.argv) {
|
|
@@ -3553,10 +3807,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3553
3807
|
const httpClient = new HttpClient(opts.serverUrl, logger);
|
|
3554
3808
|
try {
|
|
3555
3809
|
const auth = await httpClient.resolveAuth(opts);
|
|
3556
|
-
const
|
|
3557
|
-
const pwShard = config.shard;
|
|
3558
|
-
const shardIndex = pwShard?.current;
|
|
3559
|
-
const shardTotal = pwShard?.total;
|
|
3810
|
+
const shard = resolveShardInfo(config);
|
|
3560
3811
|
const response = await httpClient.postJSON(
|
|
3561
3812
|
"/api/test-runs/setup",
|
|
3562
3813
|
{
|
|
@@ -3566,9 +3817,9 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3566
3817
|
label: opts.label || null,
|
|
3567
3818
|
keep: opts.keep === true,
|
|
3568
3819
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3569
|
-
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
3570
|
-
shardIndex,
|
|
3571
|
-
shardTotal
|
|
3820
|
+
instanceId: computeInstanceId(opts.projectName, resolveRunLabel(opts.runLabel, shard !== null)),
|
|
3821
|
+
shardIndex: shard?.current,
|
|
3822
|
+
shardTotal: shard?.total
|
|
3572
3823
|
},
|
|
3573
3824
|
auth
|
|
3574
3825
|
);
|
|
@@ -3745,6 +3996,42 @@ var LONG_HEX_RE2 = /\b[0-9a-f]{8,}\b/gi;
|
|
|
3745
3996
|
var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
|
|
3746
3997
|
var URL_RE2 = /\bhttps?:\/\/[^\s'"`)]+/gi;
|
|
3747
3998
|
var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
|
|
3999
|
+
var SELECTOR_OPTION_KEY_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])/gi;
|
|
4000
|
+
var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\n", "\r", "\u2028", "\u2029"]);
|
|
4001
|
+
function closingQuoteIndex(text, quote) {
|
|
4002
|
+
const close = new Int32Array(text.length + 1).fill(-1);
|
|
4003
|
+
for (let p = text.length - 1; p >= 0; p--) {
|
|
4004
|
+
const ch = text[p];
|
|
4005
|
+
if (ch === quote) {
|
|
4006
|
+
close[p] = p;
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
const escaped = ch === "\\" && p + 1 < text.length && !LINE_TERMINATORS.has(text[p + 1]) ? close[p + 2] : -1;
|
|
4010
|
+
close[p] = escaped !== -1 ? escaped : close[p + 1];
|
|
4011
|
+
}
|
|
4012
|
+
return close;
|
|
4013
|
+
}
|
|
4014
|
+
function maskSelectorOptions(text) {
|
|
4015
|
+
const closers = /* @__PURE__ */ new Map();
|
|
4016
|
+
let out = "";
|
|
4017
|
+
let copied = 0;
|
|
4018
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = 0;
|
|
4019
|
+
let match;
|
|
4020
|
+
while (match = SELECTOR_OPTION_KEY_RE.exec(text)) {
|
|
4021
|
+
const quote = match[2];
|
|
4022
|
+
let close = closers.get(quote);
|
|
4023
|
+
if (!close) closers.set(quote, close = closingQuoteIndex(text, quote));
|
|
4024
|
+
const end = close[SELECTOR_OPTION_KEY_RE.lastIndex];
|
|
4025
|
+
if (end === -1) {
|
|
4026
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = match.index + 1;
|
|
4027
|
+
continue;
|
|
4028
|
+
}
|
|
4029
|
+
out += `${text.slice(copied, match.index)}${match[1]}: <STR>`;
|
|
4030
|
+
copied = end + 1;
|
|
4031
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = copied;
|
|
4032
|
+
}
|
|
4033
|
+
return out + text.slice(copied);
|
|
4034
|
+
}
|
|
3748
4035
|
function classifyError(text) {
|
|
3749
4036
|
if (/strict mode violation/i.test(text)) return "strict-mode";
|
|
3750
4037
|
if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
|
|
@@ -3761,7 +4048,7 @@ function maskVolatile(text) {
|
|
|
3761
4048
|
function extractErrorSignature(rawError) {
|
|
3762
4049
|
const text = stripAnsi(rawError);
|
|
3763
4050
|
const errorType = classifyError(text);
|
|
3764
|
-
const normalizedMessage = maskVolatile(extractMessageHead(text));
|
|
4051
|
+
const normalizedMessage = maskVolatile(maskSelectorOptions(extractMessageHead(text)));
|
|
3765
4052
|
const selector = extractSelector(text);
|
|
3766
4053
|
const topFrameFile = extractTopFrameFile(text);
|
|
3767
4054
|
const signature = (normalizedMessage.split("\n")[0] || "").slice(0, 200) || "Unknown error";
|
|
@@ -4788,6 +5075,7 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
4788
5075
|
}
|
|
4789
5076
|
|
|
4790
5077
|
// src/internal/submit/run-submitter.ts
|
|
5078
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
4791
5079
|
var RunSubmitter = class {
|
|
4792
5080
|
/**
|
|
4793
5081
|
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
@@ -4805,8 +5093,28 @@ var RunSubmitter = class {
|
|
|
4805
5093
|
this.logger = logger;
|
|
4806
5094
|
this.failureLinks = failureLinks;
|
|
4807
5095
|
}
|
|
4808
|
-
/**
|
|
5096
|
+
/**
|
|
5097
|
+
* Run the fallback ladder for a completed test run, within the
|
|
5098
|
+
* `submitTimeout` budget. When the budget runs out, every request in flight
|
|
5099
|
+
* and every later one fails, so the ladder falls through to saving the
|
|
5100
|
+
* recovery copy.
|
|
5101
|
+
*/
|
|
4809
5102
|
async submit(run, result) {
|
|
5103
|
+
const budgetMs = run.options.submitTimeout ?? 0;
|
|
5104
|
+
const timer = budgetMs > 0 && Number.isFinite(budgetMs) ? setTimeout(() => this.expireBudget(budgetMs), Math.min(budgetMs, MAX_TIMER_DELAY_MS)) : null;
|
|
5105
|
+
try {
|
|
5106
|
+
await this.submitRun(run, result);
|
|
5107
|
+
} finally {
|
|
5108
|
+
if (timer) clearTimeout(timer);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
expireBudget(budgetMs) {
|
|
5112
|
+
this.logger.warn(
|
|
5113
|
+
`The dashboard did not take the run within ${Math.round(budgetMs / 1e3)}s (submitTimeout) \u2014 stopping the upload and saving the results locally.`
|
|
5114
|
+
);
|
|
5115
|
+
this.httpClient.close(`the end-of-run time budget (submitTimeout: ${budgetMs} ms) ran out`);
|
|
5116
|
+
}
|
|
5117
|
+
async submitRun(run, result) {
|
|
4810
5118
|
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
4811
5119
|
const duration = new Date(endTime).getTime() - new Date(run.startTime).getTime();
|
|
4812
5120
|
const counts = computeDistinctRunCounts(run.testCases);
|
|
@@ -4843,10 +5151,10 @@ var RunSubmitter = class {
|
|
|
4843
5151
|
}
|
|
4844
5152
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
4845
5153
|
let outcome = { done: false, output: null };
|
|
4846
|
-
if (sm?.enabled && sm?.runId != null && !sm.
|
|
5154
|
+
if (sm?.enabled && sm?.runId != null && !sm.lostResults) {
|
|
4847
5155
|
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
4848
5156
|
}
|
|
4849
|
-
if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
5157
|
+
if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
4850
5158
|
outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
|
|
4851
5159
|
}
|
|
4852
5160
|
if (!outcome.done) {
|
|
@@ -4941,7 +5249,7 @@ var RunSubmitter = class {
|
|
|
4941
5249
|
}
|
|
4942
5250
|
await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
|
|
4943
5251
|
this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
|
|
4944
|
-
this.
|
|
5252
|
+
this.dropLocalCopies();
|
|
4945
5253
|
if (this.hasReports(run)) {
|
|
4946
5254
|
try {
|
|
4947
5255
|
await this.uploader.uploadReportsForStreamingRun(
|
|
@@ -4966,7 +5274,7 @@ var RunSubmitter = class {
|
|
|
4966
5274
|
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
4967
5275
|
try {
|
|
4968
5276
|
const response = await this.uploader.uploadWithFiles(payload, this.reportOptions(run), auth);
|
|
4969
|
-
this.
|
|
5277
|
+
this.dropLocalCopies();
|
|
4970
5278
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
4971
5279
|
} catch (error) {
|
|
4972
5280
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
@@ -4983,7 +5291,7 @@ var RunSubmitter = class {
|
|
|
4983
5291
|
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
4984
5292
|
try {
|
|
4985
5293
|
const response = await this.uploader.uploadJSON(payload, auth);
|
|
4986
|
-
this.
|
|
5294
|
+
this.dropLocalCopies();
|
|
4987
5295
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
4988
5296
|
} catch (error) {
|
|
4989
5297
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
@@ -4999,6 +5307,11 @@ var RunSubmitter = class {
|
|
|
4999
5307
|
return { done: true, output: null };
|
|
5000
5308
|
}
|
|
5001
5309
|
}
|
|
5310
|
+
/** The run reached the server: drop the recovery copy and the run's buffered live events. */
|
|
5311
|
+
dropLocalCopies() {
|
|
5312
|
+
this.recovery.clear();
|
|
5313
|
+
this.streamManager?.discardBuffered();
|
|
5314
|
+
}
|
|
5002
5315
|
/**
|
|
5003
5316
|
* Persist the wire-serialized payload (no raw attachments / internal fields)
|
|
5004
5317
|
* so a later run can retry the submit.
|
|
@@ -5176,7 +5489,7 @@ function countNoun(locator, count) {
|
|
|
5176
5489
|
}
|
|
5177
5490
|
function textOfGetByText(locator) {
|
|
5178
5491
|
if (!locator) return null;
|
|
5179
|
-
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)
|
|
5492
|
+
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
5180
5493
|
return m ? m[2] : null;
|
|
5181
5494
|
}
|
|
5182
5495
|
function receivedDisplay(parsed) {
|
|
@@ -5466,10 +5779,10 @@ function describeFailureText(raw, ctx) {
|
|
|
5466
5779
|
if (!raw || !raw.trim()) return null;
|
|
5467
5780
|
return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
|
|
5468
5781
|
}
|
|
5469
|
-
function headlineStep(steps) {
|
|
5782
|
+
function headlineStep(steps, error) {
|
|
5470
5783
|
if (!steps || steps.length === 0) return null;
|
|
5471
5784
|
const parents = stepParents(steps);
|
|
5472
|
-
const failed = failingStepIndex(steps, parents);
|
|
5785
|
+
const failed = failingStepIndex(steps, parents, error);
|
|
5473
5786
|
if (failed !== null) return steps[failed];
|
|
5474
5787
|
const phases = stepPhases(steps, parents);
|
|
5475
5788
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
@@ -5478,17 +5791,17 @@ function headlineStep(steps) {
|
|
|
5478
5791
|
}
|
|
5479
5792
|
return steps[steps.length - 1];
|
|
5480
5793
|
}
|
|
5481
|
-
function lastStepTitle(steps) {
|
|
5482
|
-
const title = headlineStep(steps)?.title;
|
|
5794
|
+
function lastStepTitle(steps, error) {
|
|
5795
|
+
const title = headlineStep(steps, error)?.title;
|
|
5483
5796
|
return typeof title === "string" ? title : null;
|
|
5484
5797
|
}
|
|
5485
|
-
function failingStepParams(steps) {
|
|
5486
|
-
return headlineStep(steps)?.params ?? null;
|
|
5798
|
+
function failingStepParams(steps, error) {
|
|
5799
|
+
return headlineStep(steps, error)?.params ?? null;
|
|
5487
5800
|
}
|
|
5488
5801
|
function stepHeadlineContext(steps, error) {
|
|
5489
5802
|
return {
|
|
5490
|
-
lastStepTitle: lastStepTitle(steps),
|
|
5491
|
-
stepParams: failingStepParams(steps),
|
|
5803
|
+
lastStepTitle: lastStepTitle(steps, error),
|
|
5804
|
+
stepParams: failingStepParams(steps, error),
|
|
5492
5805
|
failedIn: steps && steps.length > 0 ? failureHookContext(steps, error ?? null) : null
|
|
5493
5806
|
};
|
|
5494
5807
|
}
|
|
@@ -5561,7 +5874,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5561
5874
|
this.reportedTestIds = /* @__PURE__ */ new Set();
|
|
5562
5875
|
/** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
|
|
5563
5876
|
this.attemptsByTest = /* @__PURE__ */ new Map();
|
|
5564
|
-
|
|
5877
|
+
/** Derived in `onBegin`, once the shard is known. */
|
|
5878
|
+
this.instanceId = "";
|
|
5565
5879
|
this.shardInfo = null;
|
|
5566
5880
|
this.metadata = {};
|
|
5567
5881
|
this.isFullRun = true;
|
|
@@ -5575,8 +5889,6 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5575
5889
|
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
5576
5890
|
this.listMode = isListMode();
|
|
5577
5891
|
this.viaDesktopApp = usedDesktopDiscovery();
|
|
5578
|
-
this.runLabel = this.options.runLabel || detectCiRunLabel();
|
|
5579
|
-
this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
|
|
5580
5892
|
const logger = new Logger(this.options.verbose ?? false);
|
|
5581
5893
|
this.logger = logger;
|
|
5582
5894
|
this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
|
|
@@ -5670,11 +5982,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5670
5982
|
this.metadata = flakeRunMetadata(stamp, this.metadata);
|
|
5671
5983
|
}
|
|
5672
5984
|
this.plannedTests = suite.allTests();
|
|
5673
|
-
|
|
5674
|
-
if (
|
|
5675
|
-
this.shardInfo = { current: pwShard.current, total: pwShard.total };
|
|
5985
|
+
this.shardInfo = resolveShardInfo(config);
|
|
5986
|
+
if (this.shardInfo) {
|
|
5676
5987
|
this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
|
|
5677
5988
|
}
|
|
5989
|
+
this.instanceId = computeInstanceId(
|
|
5990
|
+
this.options.projectName,
|
|
5991
|
+
resolveRunLabel(this.options.runLabel, this.shardInfo !== null)
|
|
5992
|
+
);
|
|
5678
5993
|
this.streamManager?.start(
|
|
5679
5994
|
this.startTime,
|
|
5680
5995
|
this.metadata,
|
|
@@ -5827,7 +6142,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5827
6142
|
if (frames.length) testCase.testSourceFrames = frames;
|
|
5828
6143
|
}
|
|
5829
6144
|
if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
|
|
5830
|
-
testCase.performanceMetrics = collectStepMetrics(result.steps);
|
|
6145
|
+
testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors);
|
|
5831
6146
|
const stepEvents = extractTestStepEvents(result.steps, result.startTime);
|
|
5832
6147
|
const waitEvents = extractWaitEvents(result.steps);
|
|
5833
6148
|
const allEvents = [...stepEvents, ...waitEvents];
|
|
@@ -6398,7 +6713,7 @@ function installPickerOverlay(arg) {
|
|
|
6398
6713
|
banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
|
|
6399
6714
|
const hlTokens = (expr) => {
|
|
6400
6715
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6401
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
6716
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
6402
6717
|
let html = "";
|
|
6403
6718
|
let last = 0;
|
|
6404
6719
|
let m;
|
|
@@ -6941,7 +7256,7 @@ function showPickerChoices(arg) {
|
|
|
6941
7256
|
panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
|
|
6942
7257
|
const hlLocator = (expr) => {
|
|
6943
7258
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6944
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
7259
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
6945
7260
|
let html = "";
|
|
6946
7261
|
let last = 0;
|
|
6947
7262
|
let m;
|
|
@@ -8030,7 +8345,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
8030
8345
|
|
|
8031
8346
|
// src/internal/capture/pick-on-failure.ts
|
|
8032
8347
|
var path20 = __toESM(require("path"));
|
|
8033
|
-
var ANSI_RE2 =
|
|
8348
|
+
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
8034
8349
|
function parseLeafLocatorExpression(rawExpr) {
|
|
8035
8350
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
8036
8351
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
@@ -9938,15 +10253,15 @@ function entryPath(params) {
|
|
|
9938
10253
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
9939
10254
|
return path25.join(entryDir(params.specFile, params.dir), name);
|
|
9940
10255
|
}
|
|
9941
|
-
function
|
|
10256
|
+
function parseLocation2(location) {
|
|
9942
10257
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
9943
10258
|
if (!match) return null;
|
|
9944
10259
|
return { line: Number(match[1]), col: Number(match[2]) };
|
|
9945
10260
|
}
|
|
9946
10261
|
function orderByPosition(locations) {
|
|
9947
10262
|
return [...new Set(locations)].sort((a, b) => {
|
|
9948
|
-
const pa =
|
|
9949
|
-
const pb =
|
|
10263
|
+
const pa = parseLocation2(a);
|
|
10264
|
+
const pb = parseLocation2(b);
|
|
9950
10265
|
if (!pa || !pb) return a < b ? -1 : a > b ? 1 : 0;
|
|
9951
10266
|
return pa.line - pb.line || pa.col - pb.col;
|
|
9952
10267
|
});
|