@piwitests/reporter 0.42.0 → 0.44.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/cli/index.js +78 -18
- package/dist/global-setup-module.js +50 -19
- package/dist/index.d.ts +18 -5
- package/dist/index.js +270 -72
- package/dist/internal/capture/capture-fixtures.js +49 -10
- 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/dist/cli/index.js
CHANGED
|
@@ -1752,8 +1752,12 @@ function samePage(a, b) {
|
|
|
1752
1752
|
const kb = pageKey(b);
|
|
1753
1753
|
return ka === null || kb === null || ka === kb;
|
|
1754
1754
|
}
|
|
1755
|
+
function viewportLine(viewport) {
|
|
1756
|
+
return ` await page.setViewportSize({ width: ${Math.round(viewport.width)}, height: ${Math.round(viewport.height)} });`;
|
|
1757
|
+
}
|
|
1755
1758
|
function renderSpec(session, options = {}) {
|
|
1756
1759
|
const { steps } = session;
|
|
1760
|
+
const viewportAt = new Map((session.viewports ?? []).map((v) => [v.step, v]));
|
|
1757
1761
|
const title = options.title ?? "recorded flow";
|
|
1758
1762
|
const catalog = (options.catalog ?? []).filter(callIdentifiersAreSafe);
|
|
1759
1763
|
const ctx = {
|
|
@@ -1781,6 +1785,8 @@ function renderSpec(session, options = {}) {
|
|
|
1781
1785
|
let sawGoto = false;
|
|
1782
1786
|
while (pos < steps.length) {
|
|
1783
1787
|
const step = steps[pos];
|
|
1788
|
+
const viewport = pos > 0 ? viewportAt.get(pos) : void 0;
|
|
1789
|
+
if (viewport) bodyLines.push(viewportLine(viewport));
|
|
1784
1790
|
stepStarts[pos] = bodyLines.length;
|
|
1785
1791
|
if (step.action === "goto") {
|
|
1786
1792
|
bodyLines.push(...renderRawStep(step, pos, ctx));
|
|
@@ -1788,7 +1794,8 @@ function renderSpec(session, options = {}) {
|
|
|
1788
1794
|
pos++;
|
|
1789
1795
|
continue;
|
|
1790
1796
|
}
|
|
1791
|
-
const
|
|
1797
|
+
const found = catalog.length > 0 ? matchFunctionAt(steps, pos, catalog) : null;
|
|
1798
|
+
const match = found && ![...viewportAt.keys()].some((at) => at > pos && at <= Math.max(...found.matchedIndices)) ? found : null;
|
|
1792
1799
|
if (match) {
|
|
1793
1800
|
bodyLines.push(renderFunctionCall(match));
|
|
1794
1801
|
usedEntries.push(match.entry);
|
|
@@ -1803,11 +1810,13 @@ function renderSpec(session, options = {}) {
|
|
|
1803
1810
|
checkNextPage(pos);
|
|
1804
1811
|
pos++;
|
|
1805
1812
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1813
|
+
const start = viewportAt.get(0);
|
|
1814
|
+
const opening = [
|
|
1815
|
+
...start ? [viewportLine(start)] : [],
|
|
1816
|
+
...!sawGoto && session.startUrl ? [` await page.goto(${quote2(urlForCode(session.startUrl, ctx))});`] : []
|
|
1817
|
+
];
|
|
1818
|
+
bodyLines.unshift(...opening);
|
|
1819
|
+
const bodyOffset = opening.length;
|
|
1811
1820
|
const importLines = renderImports(usedEntries);
|
|
1812
1821
|
const instantiationLines = renderInstantiations(usedEntries);
|
|
1813
1822
|
const failLine = expectFailLine(options);
|
|
@@ -1825,7 +1834,7 @@ function renderSpec(session, options = {}) {
|
|
|
1825
1834
|
};
|
|
1826
1835
|
}
|
|
1827
1836
|
const details = renderDetails(options);
|
|
1828
|
-
const
|
|
1837
|
+
const testOpening = details.length > 0 ? [`test(${quote2(title)}, {`, ...details, `}, async ({ page }) => {`] : [`test(${quote2(title)}, async ({ page }) => {`];
|
|
1829
1838
|
const lines = [
|
|
1830
1839
|
// Package name interpolated rather than adjoining the preceding keyword
|
|
1831
1840
|
// directly, so this generated-code line does not itself look like a real
|
|
@@ -1833,12 +1842,12 @@ function renderSpec(session, options = {}) {
|
|
|
1833
1842
|
`import { test, expect } from ${quote2(options.testImport?.trim() || "@playwright/test")};`,
|
|
1834
1843
|
...importLines,
|
|
1835
1844
|
``,
|
|
1836
|
-
...
|
|
1845
|
+
...testOpening,
|
|
1837
1846
|
...testBody,
|
|
1838
1847
|
`});`,
|
|
1839
1848
|
``
|
|
1840
1849
|
];
|
|
1841
|
-
const header = 1 + importLines.length + 1 +
|
|
1850
|
+
const header = 1 + importLines.length + 1 + testOpening.length;
|
|
1842
1851
|
return { code: lines.join("\n"), matchedSpans, warnings: ctx.warnings, stepLines: linesFrom(header) };
|
|
1843
1852
|
}
|
|
1844
1853
|
|
|
@@ -1850,7 +1859,12 @@ var STEPS_LIMITS = {
|
|
|
1850
1859
|
/** Typed values, expected values, URLs, notes. */
|
|
1851
1860
|
valueLength: 2e3,
|
|
1852
1861
|
/** Titles, names, texts, locators. */
|
|
1853
|
-
textLength: 500
|
|
1862
|
+
textLength: 500,
|
|
1863
|
+
viewports: 50,
|
|
1864
|
+
/** The widest and tallest viewport, in CSS pixels. */
|
|
1865
|
+
viewportSize: 1e4,
|
|
1866
|
+
/** The browser's zoom factors a viewport may name, as browsers allow them. */
|
|
1867
|
+
zoom: { min: 0.25, max: 5 }
|
|
1854
1868
|
};
|
|
1855
1869
|
var ACTIONS = /* @__PURE__ */ new Set([
|
|
1856
1870
|
"goto",
|
|
@@ -1895,13 +1909,14 @@ function toStepsDocument(session, meta = {}) {
|
|
|
1895
1909
|
origin,
|
|
1896
1910
|
recordedAt: session.startedAt,
|
|
1897
1911
|
note: meta.note ?? null,
|
|
1898
|
-
steps: session.steps.map((step) => ({
|
|
1912
|
+
steps: session.steps.map(({ view: _view, ...step }) => ({
|
|
1899
1913
|
...step,
|
|
1900
1914
|
target: step.target ? withoutElementKey(step.target) : null,
|
|
1901
1915
|
...step.dropTarget ? { dropTarget: withoutElementKey(step.dropTarget) } : {},
|
|
1902
1916
|
pageUrl: toPath(step.pageUrl, origin),
|
|
1903
1917
|
value: step.action === "goto" && step.value ? toPath(step.value, origin) : step.value
|
|
1904
|
-
}))
|
|
1918
|
+
})),
|
|
1919
|
+
...session.viewports?.length ? { viewports: session.viewports.map((v) => ({ ...v })) } : {}
|
|
1905
1920
|
};
|
|
1906
1921
|
}
|
|
1907
1922
|
function withoutElementKey(target) {
|
|
@@ -1915,7 +1930,12 @@ function sessionFromSteps(doc, origin = doc.origin) {
|
|
|
1915
1930
|
value: step.action === "goto" && step.value ? toAbsolute(step.value, origin) : step.value
|
|
1916
1931
|
}));
|
|
1917
1932
|
const firstGoto = steps.find((s) => s.action === "goto");
|
|
1918
|
-
return {
|
|
1933
|
+
return {
|
|
1934
|
+
steps,
|
|
1935
|
+
startedAt: doc.recordedAt,
|
|
1936
|
+
startUrl: firstGoto?.value ?? steps[0]?.pageUrl ?? origin ?? "",
|
|
1937
|
+
...doc.viewports?.length ? { viewports: doc.viewports } : {}
|
|
1938
|
+
};
|
|
1919
1939
|
}
|
|
1920
1940
|
function isObject(v) {
|
|
1921
1941
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -2001,6 +2021,44 @@ function checkAssertion(c, v, path19) {
|
|
|
2001
2021
|
note: c.text(v.note, `${path19}.note`, STEPS_LIMITS.valueLength, true)
|
|
2002
2022
|
};
|
|
2003
2023
|
}
|
|
2024
|
+
function checkViewports(c, v, stepCount) {
|
|
2025
|
+
if (v == null) return [];
|
|
2026
|
+
if (!Array.isArray(v)) {
|
|
2027
|
+
c.fail("viewports", "must be a list");
|
|
2028
|
+
return [];
|
|
2029
|
+
}
|
|
2030
|
+
if (v.length > STEPS_LIMITS.viewports) {
|
|
2031
|
+
c.fail("viewports", `holds more than ${STEPS_LIMITS.viewports}`);
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
const out = [];
|
|
2035
|
+
v.forEach((entry, i) => {
|
|
2036
|
+
const at = `viewports[${i}]`;
|
|
2037
|
+
if (!isObject(entry)) return c.fail(at, "must be an object");
|
|
2038
|
+
const { step, width, height } = entry;
|
|
2039
|
+
if (!Number.isInteger(step) || step < 0 || step >= stepCount)
|
|
2040
|
+
return c.fail(`${at}.step`, "must be the index of a step");
|
|
2041
|
+
for (const [name, size] of [
|
|
2042
|
+
["width", width],
|
|
2043
|
+
["height", height]
|
|
2044
|
+
]) {
|
|
2045
|
+
if (!Number.isInteger(size) || size < 1 || size > STEPS_LIMITS.viewportSize)
|
|
2046
|
+
return c.fail(`${at}.${name}`, `must be a whole number of pixels from 1 to ${STEPS_LIMITS.viewportSize}`);
|
|
2047
|
+
}
|
|
2048
|
+
const { zoom } = entry;
|
|
2049
|
+
if (zoom != null && (typeof zoom !== "number" || !(zoom >= STEPS_LIMITS.zoom.min && zoom <= STEPS_LIMITS.zoom.max)))
|
|
2050
|
+
return c.fail(`${at}.zoom`, `must be a zoom factor from ${STEPS_LIMITS.zoom.min} to ${STEPS_LIMITS.zoom.max}`);
|
|
2051
|
+
const last = out[out.length - 1];
|
|
2052
|
+
if (last && last.step >= step) return c.fail(`${at}.step`, "must come after the step before it");
|
|
2053
|
+
out.push({
|
|
2054
|
+
step,
|
|
2055
|
+
width,
|
|
2056
|
+
height,
|
|
2057
|
+
...zoom != null && zoom !== 1 ? { zoom } : {}
|
|
2058
|
+
});
|
|
2059
|
+
});
|
|
2060
|
+
return out;
|
|
2061
|
+
}
|
|
2004
2062
|
function checkStep(c, v, path19) {
|
|
2005
2063
|
if (!isObject(v)) {
|
|
2006
2064
|
c.fail(path19, "must be an object");
|
|
@@ -2072,13 +2130,15 @@ function parseSteps(input) {
|
|
|
2072
2130
|
const originText = c.text(value.origin, "origin", STEPS_LIMITS.textLength, true);
|
|
2073
2131
|
const origin = originText ? originOf2(originText) : null;
|
|
2074
2132
|
if (originText && !origin) c.fail("origin", "must be an http(s) origin");
|
|
2133
|
+
const viewports = checkViewports(c, value.viewports, steps.length);
|
|
2075
2134
|
doc = {
|
|
2076
2135
|
v: STEPS_VERSION,
|
|
2077
2136
|
title: c.text(value.title, "title", STEPS_LIMITS.textLength, true),
|
|
2078
2137
|
origin,
|
|
2079
2138
|
recordedAt: value.recordedAt == null ? 0 : c.number(value.recordedAt, "recordedAt"),
|
|
2080
2139
|
note: c.text(value.note, "note", STEPS_LIMITS.valueLength, true),
|
|
2081
|
-
steps
|
|
2140
|
+
steps,
|
|
2141
|
+
...viewports.length > 0 ? { viewports } : {}
|
|
2082
2142
|
};
|
|
2083
2143
|
}
|
|
2084
2144
|
return c.errors.length > 0 ? { ok: false, errors: c.errors } : { ok: true, steps: doc };
|
|
@@ -2461,6 +2521,7 @@ var DEFAULTS = {
|
|
|
2461
2521
|
streamingBatchSize: 5,
|
|
2462
2522
|
streamingBatchDelay: 2e3,
|
|
2463
2523
|
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
2524
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
2464
2525
|
failOnFlakyTests: false,
|
|
2465
2526
|
username: null,
|
|
2466
2527
|
password: null,
|
|
@@ -2482,6 +2543,7 @@ var PIWI_ENV_KEYS = {
|
|
|
2482
2543
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
2483
2544
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
2484
2545
|
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
2546
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
2485
2547
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
2486
2548
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
2487
2549
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -2533,6 +2595,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
2533
2595
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
2534
2596
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
2535
2597
|
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
2598
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
2536
2599
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
2537
2600
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
2538
2601
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -4693,10 +4756,7 @@ function parseUnifiedDiff(text) {
|
|
|
4693
4756
|
}
|
|
4694
4757
|
return files.filter((f) => f.path);
|
|
4695
4758
|
}
|
|
4696
|
-
var DEFAULT_TRANSLATION_GLOBS = [
|
|
4697
|
-
"**/{locales,locale,i18n,lang,translations}/**",
|
|
4698
|
-
"**/*.resx"
|
|
4699
|
-
];
|
|
4759
|
+
var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
|
|
4700
4760
|
var ANCHOR_ATTRIBUTES = [
|
|
4701
4761
|
"id",
|
|
4702
4762
|
"name",
|
|
@@ -90,6 +90,7 @@ var DEFAULTS = {
|
|
|
90
90
|
streamingBatchSize: 5,
|
|
91
91
|
streamingBatchDelay: 2e3,
|
|
92
92
|
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
93
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
93
94
|
failOnFlakyTests: false,
|
|
94
95
|
username: null,
|
|
95
96
|
password: null,
|
|
@@ -111,6 +112,7 @@ var PIWI_ENV_KEYS = {
|
|
|
111
112
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
112
113
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
113
114
|
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
115
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
114
116
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
115
117
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
116
118
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -156,6 +158,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
156
158
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
157
159
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
158
160
|
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
161
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
159
162
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
160
163
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
161
164
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -251,13 +254,17 @@ var HttpClient = class {
|
|
|
251
254
|
/**
|
|
252
255
|
* @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
|
|
253
256
|
* @param logger Prefixed logger for verbose diagnostics.
|
|
254
|
-
* @param timeout Socket inactivity timeout in ms (default 30s)
|
|
255
|
-
* server
|
|
257
|
+
* @param timeout Socket inactivity timeout in ms (default 30s), so a hung
|
|
258
|
+
* server fails fast rather than stalling the reporter.
|
|
256
259
|
*/
|
|
257
260
|
constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
|
|
258
261
|
this.serverUrl = serverUrl;
|
|
259
262
|
this.logger = logger;
|
|
260
263
|
this.timeout = timeout;
|
|
264
|
+
/** Requests in flight, so `close` can abort them. */
|
|
265
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
266
|
+
/** Set by `close`: every later request fails with this reason. */
|
|
267
|
+
this.closedReason = null;
|
|
261
268
|
this.warnIfInsecureTransport();
|
|
262
269
|
}
|
|
263
270
|
/**
|
|
@@ -268,7 +275,7 @@ var HttpClient = class {
|
|
|
268
275
|
* endpoint exposes the key and payloads on the wire. Loopback (localhost /
|
|
269
276
|
* 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
|
|
270
277
|
* warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
|
|
271
|
-
*
|
|
278
|
+
* work, while the exposure is still reported.
|
|
272
279
|
*/
|
|
273
280
|
warnIfInsecureTransport() {
|
|
274
281
|
let url;
|
|
@@ -289,6 +296,16 @@ var HttpClient = class {
|
|
|
289
296
|
get baseUrl() {
|
|
290
297
|
return this.serverUrl;
|
|
291
298
|
}
|
|
299
|
+
/** Whether `close` was called. */
|
|
300
|
+
get closed() {
|
|
301
|
+
return this.closedReason !== null;
|
|
302
|
+
}
|
|
303
|
+
/** Abort every request in flight and fail every later one with `reason`. */
|
|
304
|
+
close(reason) {
|
|
305
|
+
this.closedReason = reason;
|
|
306
|
+
for (const req of this.inFlight) req.destroy(new Error(reason));
|
|
307
|
+
this.inFlight.clear();
|
|
308
|
+
}
|
|
292
309
|
/**
|
|
293
310
|
* Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
|
|
294
311
|
* or return `null` when neither is configured.
|
|
@@ -409,6 +426,10 @@ var HttpClient = class {
|
|
|
409
426
|
/** Unified request core: transport, headers, auth, response accumulation, timeout. */
|
|
410
427
|
request(method, pathname, opts) {
|
|
411
428
|
return new Promise((resolve2, reject) => {
|
|
429
|
+
if (this.closedReason !== null) {
|
|
430
|
+
reject(new Error(this.closedReason));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
412
433
|
const url = new import_node_url.URL(pathname, this.serverUrl);
|
|
413
434
|
const transport = url.protocol === "https:" ? https : http;
|
|
414
435
|
const headers = { ...opts.headers };
|
|
@@ -437,11 +458,14 @@ var HttpClient = class {
|
|
|
437
458
|
});
|
|
438
459
|
}
|
|
439
460
|
);
|
|
461
|
+
this.inFlight.add(req);
|
|
462
|
+
req.on("close", () => this.inFlight.delete(req));
|
|
440
463
|
req.on("error", reject);
|
|
441
464
|
req.setTimeout(this.timeout, () => {
|
|
442
465
|
req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
|
|
443
466
|
});
|
|
444
467
|
if (opts.form) {
|
|
468
|
+
opts.form.on("error", (error) => req.destroy(error));
|
|
445
469
|
opts.form.pipe(req);
|
|
446
470
|
} else if (opts.body !== void 0) {
|
|
447
471
|
req.write(opts.body);
|
|
@@ -473,22 +497,30 @@ function computeInstanceId(projectName, runLabel) {
|
|
|
473
497
|
}
|
|
474
498
|
|
|
475
499
|
// src/internal/support/ci.ts
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (env.
|
|
482
|
-
if (env.
|
|
483
|
-
if (env.
|
|
484
|
-
if (env.
|
|
485
|
-
if (env.
|
|
486
|
-
if (env.
|
|
487
|
-
if (env.
|
|
488
|
-
if (env.
|
|
489
|
-
if (env.
|
|
500
|
+
function detectCiIds(env) {
|
|
501
|
+
if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
|
|
502
|
+
const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
|
|
503
|
+
return { run, job: env.GITHUB_JOB || null };
|
|
504
|
+
}
|
|
505
|
+
if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
|
|
506
|
+
if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
|
|
507
|
+
if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
|
|
508
|
+
if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
|
|
509
|
+
if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
|
|
510
|
+
if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
|
|
511
|
+
if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
|
|
512
|
+
if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
|
|
513
|
+
if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
|
|
514
|
+
if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
|
|
515
|
+
if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
|
|
490
516
|
return null;
|
|
491
517
|
}
|
|
518
|
+
function resolveRunLabel(configured, sharded) {
|
|
519
|
+
if (configured) return configured;
|
|
520
|
+
const ci = detectCiIds(process.env);
|
|
521
|
+
if (!ci) return null;
|
|
522
|
+
return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
|
|
523
|
+
}
|
|
492
524
|
|
|
493
525
|
// src/internal/support/shard-info.ts
|
|
494
526
|
function parseShardSpec(raw) {
|
|
@@ -702,7 +734,6 @@ function createGlobalSetup(options, userSetup) {
|
|
|
702
734
|
const httpClient = new HttpClient(opts.serverUrl, logger);
|
|
703
735
|
try {
|
|
704
736
|
const auth = await httpClient.resolveAuth(opts);
|
|
705
|
-
const runLabel = opts.runLabel || detectCiRunLabel();
|
|
706
737
|
const shard = resolveShardInfo(config);
|
|
707
738
|
const response = await httpClient.postJSON(
|
|
708
739
|
"/api/test-runs/setup",
|
|
@@ -713,7 +744,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
713
744
|
label: opts.label || null,
|
|
714
745
|
keep: opts.keep === true,
|
|
715
746
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
716
|
-
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
747
|
+
instanceId: computeInstanceId(opts.projectName, resolveRunLabel(opts.runLabel, shard !== null)),
|
|
717
748
|
shardIndex: shard?.current,
|
|
718
749
|
shardTotal: shard?.total
|
|
719
750
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ interface PiwiDashboardOptions {
|
|
|
22
22
|
projectName?: string;
|
|
23
23
|
/** Optional description of the project */
|
|
24
24
|
projectDescription?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Time budget (ms) for the reporter's end-of-run work: delivering the last
|
|
27
|
+
* live events, uploading the remaining traces and attachments, and submitting
|
|
28
|
+
* the run. When it runs out, the reporter stops waiting for the dashboard and
|
|
29
|
+
* saves the run's results locally; the next run for the project uploads them,
|
|
30
|
+
* without traces or attachments. Defaults to `900000` (15 minutes). Set to `0`
|
|
31
|
+
* for no limit. Can also be set with `PIWI_SUBMIT_TIMEOUT`.
|
|
32
|
+
*/
|
|
33
|
+
submitTimeout?: number;
|
|
25
34
|
/** Upload trace files to the dashboard. Defaults to `true`. */
|
|
26
35
|
uploadTraces?: boolean;
|
|
27
36
|
/** Upload the Playwright HTML report. Defaults to `true`. */
|
|
@@ -189,7 +198,12 @@ interface PiwiDashboardOptions {
|
|
|
189
198
|
dir?: string;
|
|
190
199
|
label?: string;
|
|
191
200
|
}>;
|
|
192
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Stable label that ties shards together (e.g. CI run ID). Auto-detected from
|
|
203
|
+
* CI env, with the CI job's id added for a run that is not sharded; a label
|
|
204
|
+
* set here is used as it is. Override it when your CI is not detected, or to
|
|
205
|
+
* keep matrix legs that report to one project apart.
|
|
206
|
+
*/
|
|
193
207
|
runLabel?: string;
|
|
194
208
|
/** Deployment environment for this run, e.g. `"production"`, `"staging"`, `"integration"` */
|
|
195
209
|
environment?: string;
|
|
@@ -303,9 +317,8 @@ declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (
|
|
|
303
317
|
* executed after it.
|
|
304
318
|
*
|
|
305
319
|
* Playwright options required in `globalSetup` are forwarded via `PIWI_*`
|
|
306
|
-
* environment variables (see `applyOptionsToEnv` in `config.ts`
|
|
307
|
-
* supported set
|
|
308
|
-
* `username`, `password`, `environment`, `label`, `runLabel`).
|
|
320
|
+
* environment variables (see `applyOptionsToEnv` in `internal/config/env.ts`
|
|
321
|
+
* for the supported set).
|
|
309
322
|
*
|
|
310
323
|
* The top-level `use` block's `screenshot` and `trace` are defaulted to
|
|
311
324
|
* `'only-on-failure'` / `'retain-on-failure'` when unset so failure evidence is
|
|
@@ -345,8 +358,8 @@ declare class PiwiDashboardReporter {
|
|
|
345
358
|
private reportedTestIds;
|
|
346
359
|
/** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
|
|
347
360
|
private attemptsByTest;
|
|
361
|
+
/** Derived in `onBegin`, once the shard is known. */
|
|
348
362
|
private instanceId;
|
|
349
|
-
private runLabel;
|
|
350
363
|
private shardInfo;
|
|
351
364
|
private metadata;
|
|
352
365
|
private enabled;
|
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",
|
|
@@ -172,6 +174,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
172
174
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
173
175
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
174
176
|
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
177
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
175
178
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
176
179
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
177
180
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -316,13 +319,17 @@ var HttpClient = class {
|
|
|
316
319
|
/**
|
|
317
320
|
* @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
|
|
318
321
|
* @param logger Prefixed logger for verbose diagnostics.
|
|
319
|
-
* @param timeout Socket inactivity timeout in ms (default 30s)
|
|
320
|
-
* server
|
|
322
|
+
* @param timeout Socket inactivity timeout in ms (default 30s), so a hung
|
|
323
|
+
* server fails fast rather than stalling the reporter.
|
|
321
324
|
*/
|
|
322
325
|
constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
|
|
323
326
|
this.serverUrl = serverUrl;
|
|
324
327
|
this.logger = logger;
|
|
325
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;
|
|
326
333
|
this.warnIfInsecureTransport();
|
|
327
334
|
}
|
|
328
335
|
/**
|
|
@@ -333,7 +340,7 @@ var HttpClient = class {
|
|
|
333
340
|
* endpoint exposes the key and payloads on the wire. Loopback (localhost /
|
|
334
341
|
* 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
|
|
335
342
|
* warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
|
|
336
|
-
*
|
|
343
|
+
* work, while the exposure is still reported.
|
|
337
344
|
*/
|
|
338
345
|
warnIfInsecureTransport() {
|
|
339
346
|
let url;
|
|
@@ -354,6 +361,16 @@ var HttpClient = class {
|
|
|
354
361
|
get baseUrl() {
|
|
355
362
|
return this.serverUrl;
|
|
356
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
|
+
}
|
|
357
374
|
/**
|
|
358
375
|
* Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
|
|
359
376
|
* or return `null` when neither is configured.
|
|
@@ -474,6 +491,10 @@ var HttpClient = class {
|
|
|
474
491
|
/** Unified request core: transport, headers, auth, response accumulation, timeout. */
|
|
475
492
|
request(method, pathname, opts) {
|
|
476
493
|
return new Promise((resolve6, reject) => {
|
|
494
|
+
if (this.closedReason !== null) {
|
|
495
|
+
reject(new Error(this.closedReason));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
477
498
|
const url = new import_node_url.URL(pathname, this.serverUrl);
|
|
478
499
|
const transport = url.protocol === "https:" ? https : http;
|
|
479
500
|
const headers = { ...opts.headers };
|
|
@@ -502,11 +523,14 @@ var HttpClient = class {
|
|
|
502
523
|
});
|
|
503
524
|
}
|
|
504
525
|
);
|
|
526
|
+
this.inFlight.add(req);
|
|
527
|
+
req.on("close", () => this.inFlight.delete(req));
|
|
505
528
|
req.on("error", reject);
|
|
506
529
|
req.setTimeout(this.timeout, () => {
|
|
507
530
|
req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
|
|
508
531
|
});
|
|
509
532
|
if (opts.form) {
|
|
533
|
+
opts.form.on("error", (error) => req.destroy(error));
|
|
510
534
|
opts.form.pipe(req);
|
|
511
535
|
} else if (opts.body !== void 0) {
|
|
512
536
|
req.write(opts.body);
|
|
@@ -865,19 +889,25 @@ function computeInstanceId(projectName, runLabel) {
|
|
|
865
889
|
// src/internal/streaming/stream-buffer.ts
|
|
866
890
|
var StreamBuffer = class {
|
|
867
891
|
constructor(projectName) {
|
|
868
|
-
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`);
|
|
869
898
|
}
|
|
870
899
|
/** Append one or more events to the on-disk buffer */
|
|
871
900
|
append(events) {
|
|
872
|
-
if (events.length === 0) return;
|
|
901
|
+
if (!this.filePath || events.length === 0) return;
|
|
873
902
|
try {
|
|
874
903
|
const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
|
|
875
904
|
fs3.appendFileSync(this.filePath, lines, "utf8");
|
|
876
905
|
} catch {
|
|
877
906
|
}
|
|
878
907
|
}
|
|
879
|
-
/** Load all buffered events
|
|
908
|
+
/** Load all buffered events of the bound run from disk */
|
|
880
909
|
load() {
|
|
910
|
+
if (!this.filePath) return [];
|
|
881
911
|
try {
|
|
882
912
|
if (fs3.existsSync(this.filePath)) {
|
|
883
913
|
const content = fs3.readFileSync(this.filePath, "utf8");
|
|
@@ -887,23 +917,29 @@ var StreamBuffer = class {
|
|
|
887
917
|
}
|
|
888
918
|
return [];
|
|
889
919
|
}
|
|
890
|
-
/** Delete the buffer file from disk */
|
|
920
|
+
/** Delete the bound run's buffer file from disk */
|
|
891
921
|
clear() {
|
|
922
|
+
if (!this.filePath) return;
|
|
892
923
|
try {
|
|
893
924
|
if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
|
|
894
925
|
} catch {
|
|
895
926
|
}
|
|
896
927
|
}
|
|
897
|
-
/** Remove
|
|
928
|
+
/** Remove this project's buffer files older than `maxAgeMs` (default 2 hours). Used on startup to discard orphaned data. */
|
|
898
929
|
clearStale(maxAgeMs = 72e5) {
|
|
930
|
+
const dir = os3.tmpdir();
|
|
931
|
+
let names;
|
|
899
932
|
try {
|
|
900
|
-
|
|
901
|
-
const stats = fs3.statSync(this.filePath);
|
|
902
|
-
if (Date.now() - stats.mtimeMs > maxAgeMs) {
|
|
903
|
-
fs3.unlinkSync(this.filePath);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
933
|
+
names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
|
|
906
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
|
+
}
|
|
907
943
|
}
|
|
908
944
|
}
|
|
909
945
|
};
|
|
@@ -1332,7 +1368,7 @@ var MetadataCollector = class {
|
|
|
1332
1368
|
if (options.tags && Array.isArray(options.tags)) metadata.tags = options.tags;
|
|
1333
1369
|
if (options.customData) metadata.customData = options.customData;
|
|
1334
1370
|
if (options.collectScmInfo) {
|
|
1335
|
-
const scm = this.collectScmInfo(
|
|
1371
|
+
const scm = this.collectScmInfo();
|
|
1336
1372
|
if (scm) metadata.scm = scm;
|
|
1337
1373
|
}
|
|
1338
1374
|
if (options.collectCiInfo) {
|
|
@@ -1426,7 +1462,7 @@ var MetadataCollector = class {
|
|
|
1426
1462
|
}
|
|
1427
1463
|
return { suitePath, suiteConfig };
|
|
1428
1464
|
}
|
|
1429
|
-
collectScmInfo(
|
|
1465
|
+
collectScmInfo() {
|
|
1430
1466
|
const scm = {};
|
|
1431
1467
|
let gitBranch;
|
|
1432
1468
|
try {
|
|
@@ -1547,7 +1583,7 @@ var CRITICAL_TIER = 2;
|
|
|
1547
1583
|
var BoundedEventQueue = class {
|
|
1548
1584
|
/**
|
|
1549
1585
|
* @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
|
|
1550
|
-
* disables the bound (unbounded
|
|
1586
|
+
* disables the bound (unbounded).
|
|
1551
1587
|
*/
|
|
1552
1588
|
constructor(maxBytes) {
|
|
1553
1589
|
this.maxBytes = maxBytes;
|
|
@@ -1710,10 +1746,14 @@ function runUrl(serverUrl, runId) {
|
|
|
1710
1746
|
}
|
|
1711
1747
|
|
|
1712
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;
|
|
1713
1753
|
var StreamManager = class {
|
|
1714
1754
|
/**
|
|
1715
1755
|
* @param httpClient HTTP client for server communication.
|
|
1716
|
-
* @param streamBuffer On-disk buffer for
|
|
1756
|
+
* @param streamBuffer On-disk buffer for the run's undelivered events.
|
|
1717
1757
|
* @param recovery Crash-recovery handler for uploading stale payloads on startup.
|
|
1718
1758
|
* @param uploader Uploader for per-test-case file uploads.
|
|
1719
1759
|
* @param fileHandler File-discovery helper for finding traces and attachments.
|
|
@@ -1749,6 +1789,8 @@ var StreamManager = class {
|
|
|
1749
1789
|
this.heartbeatInterval = 15e3;
|
|
1750
1790
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
1751
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;
|
|
1752
1794
|
this._enabled = false;
|
|
1753
1795
|
this._runId = null;
|
|
1754
1796
|
this._token = null;
|
|
@@ -1779,13 +1821,15 @@ var StreamManager = class {
|
|
|
1779
1821
|
return this._startPromise;
|
|
1780
1822
|
}
|
|
1781
1823
|
/**
|
|
1782
|
-
* Whether
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
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.
|
|
1786
1830
|
*/
|
|
1787
|
-
get
|
|
1788
|
-
return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1831
|
+
get lostResults() {
|
|
1832
|
+
return this.resultsUndelivered || this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1789
1833
|
}
|
|
1790
1834
|
/** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
|
|
1791
1835
|
start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails, plannedTotal) {
|
|
@@ -1878,6 +1922,7 @@ var StreamManager = class {
|
|
|
1878
1922
|
this._runId = response.runId;
|
|
1879
1923
|
this._token = response.streamToken;
|
|
1880
1924
|
this._enabled = true;
|
|
1925
|
+
this.streamBuffer.bindRun(response.runId);
|
|
1881
1926
|
this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
|
|
1882
1927
|
if (this.options.serverUrl) {
|
|
1883
1928
|
this.logger.info(`Watch live: ${runUrl(this.options.serverUrl, response.runId)}`);
|
|
@@ -1928,21 +1973,78 @@ var StreamManager = class {
|
|
|
1928
1973
|
this.flushTimer = null;
|
|
1929
1974
|
}
|
|
1930
1975
|
if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
+
);
|
|
1934
1994
|
this.retryCount = 0;
|
|
1935
1995
|
this.lastActivityAt = Date.now();
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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));
|
|
1940
2007
|
this.scheduleRetry(errorMessage(error));
|
|
1941
2008
|
return false;
|
|
1942
2009
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
+
}
|
|
1946
2048
|
}
|
|
1947
2049
|
scheduleRetry(reason) {
|
|
1948
2050
|
if (this.retryTimer) return;
|
|
@@ -2007,7 +2109,7 @@ var StreamManager = class {
|
|
|
2007
2109
|
this.retryTimer = null;
|
|
2008
2110
|
}
|
|
2009
2111
|
}
|
|
2010
|
-
/** 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. */
|
|
2011
2113
|
async drain() {
|
|
2012
2114
|
try {
|
|
2013
2115
|
await this._drain();
|
|
@@ -2040,6 +2142,7 @@ var StreamManager = class {
|
|
|
2040
2142
|
}
|
|
2041
2143
|
return;
|
|
2042
2144
|
}
|
|
2145
|
+
if (this.httpClient.closed) break;
|
|
2043
2146
|
if (attempt === 0) {
|
|
2044
2147
|
this.logger.warn(
|
|
2045
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)...`
|
|
@@ -2052,6 +2155,7 @@ var StreamManager = class {
|
|
|
2052
2155
|
}
|
|
2053
2156
|
if (!this.pendingEvents.isEmpty) {
|
|
2054
2157
|
const remaining = this.pendingEvents.takeAll();
|
|
2158
|
+
if (remaining.some((event) => event.type === "complete")) this.resultsUndelivered = true;
|
|
2055
2159
|
this.logger.warn(
|
|
2056
2160
|
`Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
|
|
2057
2161
|
);
|
|
@@ -2061,6 +2165,10 @@ var StreamManager = class {
|
|
|
2061
2165
|
this.clearRetryTimer();
|
|
2062
2166
|
}
|
|
2063
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
|
+
}
|
|
2064
2172
|
// Emit a single summary when buffer pressure shed events, so a full disk or a
|
|
2065
2173
|
// stalled server does not silently swallow data. Reported once per drain.
|
|
2066
2174
|
reportBufferPressure() {
|
|
@@ -2125,13 +2233,18 @@ var StreamManager = class {
|
|
|
2125
2233
|
})();
|
|
2126
2234
|
this.liveUploadPromises.push(promise);
|
|
2127
2235
|
}
|
|
2128
|
-
/**
|
|
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
|
+
*/
|
|
2129
2241
|
async uploadRemaining(testCases) {
|
|
2130
2242
|
if (this.liveUploadPromises.length > 0) {
|
|
2131
2243
|
await Promise.allSettled(this.liveUploadPromises);
|
|
2132
2244
|
this.liveUploadPromises = [];
|
|
2133
2245
|
}
|
|
2134
2246
|
if (!this._enabled || !this._runId || !this._token) return;
|
|
2247
|
+
let connectionFailures = 0;
|
|
2135
2248
|
for (const tc of testCases) {
|
|
2136
2249
|
if (this.uploadedCaseFiles.has(tc)) continue;
|
|
2137
2250
|
try {
|
|
@@ -2144,12 +2257,25 @@ var StreamManager = class {
|
|
|
2144
2257
|
this._auth
|
|
2145
2258
|
);
|
|
2146
2259
|
if (uploaded) this.uploadedCaseFiles.add(tc);
|
|
2260
|
+
connectionFailures = 0;
|
|
2147
2261
|
} catch (error) {
|
|
2148
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
|
+
}
|
|
2149
2268
|
}
|
|
2150
2269
|
}
|
|
2151
2270
|
}
|
|
2152
2271
|
};
|
|
2272
|
+
function serializedBytes(event) {
|
|
2273
|
+
try {
|
|
2274
|
+
return Buffer.byteLength(JSON.stringify(event)) + 1;
|
|
2275
|
+
} catch {
|
|
2276
|
+
return 0;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2153
2279
|
|
|
2154
2280
|
// ../core/src/locator-chain.ts
|
|
2155
2281
|
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -2601,15 +2727,19 @@ function readChain(region) {
|
|
|
2601
2727
|
const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
|
|
2602
2728
|
return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
|
|
2603
2729
|
}
|
|
2604
|
-
function
|
|
2730
|
+
function extractStackFrames(text) {
|
|
2605
2731
|
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
2732
|
+
const frames = [];
|
|
2606
2733
|
let m;
|
|
2607
2734
|
while ((m = frameRe.exec(text)) !== null) {
|
|
2608
2735
|
const file = m[1].replace(/\\/g, "/");
|
|
2609
2736
|
if (file.includes("node_modules") || file.startsWith("node:")) continue;
|
|
2610
|
-
|
|
2737
|
+
frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
|
|
2611
2738
|
}
|
|
2612
|
-
return
|
|
2739
|
+
return frames;
|
|
2740
|
+
}
|
|
2741
|
+
function extractTopFrame(text) {
|
|
2742
|
+
return extractStackFrames(text)[0] ?? null;
|
|
2613
2743
|
}
|
|
2614
2744
|
function extractTopFrameFile(text) {
|
|
2615
2745
|
return extractTopFrame(text)?.file ?? null;
|
|
@@ -3394,22 +3524,30 @@ function extractFailingLine(errorText, testFile2, declarationLine) {
|
|
|
3394
3524
|
}
|
|
3395
3525
|
|
|
3396
3526
|
// src/internal/support/ci.ts
|
|
3397
|
-
function
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
if (env.
|
|
3403
|
-
if (env.
|
|
3404
|
-
if (env.
|
|
3405
|
-
if (env.
|
|
3406
|
-
if (env.
|
|
3407
|
-
if (env.
|
|
3408
|
-
if (env.
|
|
3409
|
-
if (env.
|
|
3410
|
-
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 };
|
|
3411
3543
|
return null;
|
|
3412
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
|
+
}
|
|
3413
3551
|
|
|
3414
3552
|
// src/internal/support/worker-index.ts
|
|
3415
3553
|
function workerIndexOf(result) {
|
|
@@ -3669,7 +3807,6 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3669
3807
|
const httpClient = new HttpClient(opts.serverUrl, logger);
|
|
3670
3808
|
try {
|
|
3671
3809
|
const auth = await httpClient.resolveAuth(opts);
|
|
3672
|
-
const runLabel = opts.runLabel || detectCiRunLabel();
|
|
3673
3810
|
const shard = resolveShardInfo(config);
|
|
3674
3811
|
const response = await httpClient.postJSON(
|
|
3675
3812
|
"/api/test-runs/setup",
|
|
@@ -3680,7 +3817,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3680
3817
|
label: opts.label || null,
|
|
3681
3818
|
keep: opts.keep === true,
|
|
3682
3819
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3683
|
-
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
3820
|
+
instanceId: computeInstanceId(opts.projectName, resolveRunLabel(opts.runLabel, shard !== null)),
|
|
3684
3821
|
shardIndex: shard?.current,
|
|
3685
3822
|
shardTotal: shard?.total
|
|
3686
3823
|
},
|
|
@@ -3859,7 +3996,42 @@ var LONG_HEX_RE2 = /\b[0-9a-f]{8,}\b/gi;
|
|
|
3859
3996
|
var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
|
|
3860
3997
|
var URL_RE2 = /\bhttps?:\/\/[^\s'"`)]+/gi;
|
|
3861
3998
|
var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
|
|
3862
|
-
var
|
|
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
|
+
}
|
|
3863
4035
|
function classifyError(text) {
|
|
3864
4036
|
if (/strict mode violation/i.test(text)) return "strict-mode";
|
|
3865
4037
|
if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
|
|
@@ -3873,9 +4045,6 @@ function classifyError(text) {
|
|
|
3873
4045
|
function maskVolatile(text) {
|
|
3874
4046
|
return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE2, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE2, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
|
|
3875
4047
|
}
|
|
3876
|
-
function maskSelectorOptions(text) {
|
|
3877
|
-
return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
|
|
3878
|
-
}
|
|
3879
4048
|
function extractErrorSignature(rawError) {
|
|
3880
4049
|
const text = stripAnsi(rawError);
|
|
3881
4050
|
const errorType = classifyError(text);
|
|
@@ -4906,6 +5075,7 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
4906
5075
|
}
|
|
4907
5076
|
|
|
4908
5077
|
// src/internal/submit/run-submitter.ts
|
|
5078
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
4909
5079
|
var RunSubmitter = class {
|
|
4910
5080
|
/**
|
|
4911
5081
|
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
@@ -4923,8 +5093,28 @@ var RunSubmitter = class {
|
|
|
4923
5093
|
this.logger = logger;
|
|
4924
5094
|
this.failureLinks = failureLinks;
|
|
4925
5095
|
}
|
|
4926
|
-
/**
|
|
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
|
+
*/
|
|
4927
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) {
|
|
4928
5118
|
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
4929
5119
|
const duration = new Date(endTime).getTime() - new Date(run.startTime).getTime();
|
|
4930
5120
|
const counts = computeDistinctRunCounts(run.testCases);
|
|
@@ -4961,10 +5151,10 @@ var RunSubmitter = class {
|
|
|
4961
5151
|
}
|
|
4962
5152
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
4963
5153
|
let outcome = { done: false, output: null };
|
|
4964
|
-
if (sm?.enabled && sm?.runId != null && !sm.
|
|
5154
|
+
if (sm?.enabled && sm?.runId != null && !sm.lostResults) {
|
|
4965
5155
|
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
4966
5156
|
}
|
|
4967
|
-
if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
5157
|
+
if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
4968
5158
|
outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
|
|
4969
5159
|
}
|
|
4970
5160
|
if (!outcome.done) {
|
|
@@ -5059,7 +5249,7 @@ var RunSubmitter = class {
|
|
|
5059
5249
|
}
|
|
5060
5250
|
await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
|
|
5061
5251
|
this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
|
|
5062
|
-
this.
|
|
5252
|
+
this.dropLocalCopies();
|
|
5063
5253
|
if (this.hasReports(run)) {
|
|
5064
5254
|
try {
|
|
5065
5255
|
await this.uploader.uploadReportsForStreamingRun(
|
|
@@ -5084,7 +5274,7 @@ var RunSubmitter = class {
|
|
|
5084
5274
|
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
5085
5275
|
try {
|
|
5086
5276
|
const response = await this.uploader.uploadWithFiles(payload, this.reportOptions(run), auth);
|
|
5087
|
-
this.
|
|
5277
|
+
this.dropLocalCopies();
|
|
5088
5278
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
5089
5279
|
} catch (error) {
|
|
5090
5280
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
@@ -5101,7 +5291,7 @@ var RunSubmitter = class {
|
|
|
5101
5291
|
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
5102
5292
|
try {
|
|
5103
5293
|
const response = await this.uploader.uploadJSON(payload, auth);
|
|
5104
|
-
this.
|
|
5294
|
+
this.dropLocalCopies();
|
|
5105
5295
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
5106
5296
|
} catch (error) {
|
|
5107
5297
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
@@ -5117,6 +5307,11 @@ var RunSubmitter = class {
|
|
|
5117
5307
|
return { done: true, output: null };
|
|
5118
5308
|
}
|
|
5119
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
|
+
}
|
|
5120
5315
|
/**
|
|
5121
5316
|
* Persist the wire-serialized payload (no raw attachments / internal fields)
|
|
5122
5317
|
* so a later run can retry the submit.
|
|
@@ -5294,7 +5489,7 @@ function countNoun(locator, count) {
|
|
|
5294
5489
|
}
|
|
5295
5490
|
function textOfGetByText(locator) {
|
|
5296
5491
|
if (!locator) return null;
|
|
5297
|
-
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)
|
|
5492
|
+
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
5298
5493
|
return m ? m[2] : null;
|
|
5299
5494
|
}
|
|
5300
5495
|
function receivedDisplay(parsed) {
|
|
@@ -5679,7 +5874,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5679
5874
|
this.reportedTestIds = /* @__PURE__ */ new Set();
|
|
5680
5875
|
/** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
|
|
5681
5876
|
this.attemptsByTest = /* @__PURE__ */ new Map();
|
|
5682
|
-
|
|
5877
|
+
/** Derived in `onBegin`, once the shard is known. */
|
|
5878
|
+
this.instanceId = "";
|
|
5683
5879
|
this.shardInfo = null;
|
|
5684
5880
|
this.metadata = {};
|
|
5685
5881
|
this.isFullRun = true;
|
|
@@ -5693,8 +5889,6 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5693
5889
|
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
5694
5890
|
this.listMode = isListMode();
|
|
5695
5891
|
this.viaDesktopApp = usedDesktopDiscovery();
|
|
5696
|
-
this.runLabel = this.options.runLabel || detectCiRunLabel();
|
|
5697
|
-
this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
|
|
5698
5892
|
const logger = new Logger(this.options.verbose ?? false);
|
|
5699
5893
|
this.logger = logger;
|
|
5700
5894
|
this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
|
|
@@ -5792,6 +5986,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5792
5986
|
if (this.shardInfo) {
|
|
5793
5987
|
this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
|
|
5794
5988
|
}
|
|
5989
|
+
this.instanceId = computeInstanceId(
|
|
5990
|
+
this.options.projectName,
|
|
5991
|
+
resolveRunLabel(this.options.runLabel, this.shardInfo !== null)
|
|
5992
|
+
);
|
|
5795
5993
|
this.streamManager?.start(
|
|
5796
5994
|
this.startTime,
|
|
5797
5995
|
this.metadata,
|
|
@@ -6515,7 +6713,7 @@ function installPickerOverlay(arg) {
|
|
|
6515
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);`;
|
|
6516
6714
|
const hlTokens = (expr) => {
|
|
6517
6715
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6518
|
-
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;
|
|
6519
6717
|
let html = "";
|
|
6520
6718
|
let last = 0;
|
|
6521
6719
|
let m;
|
|
@@ -7058,7 +7256,7 @@ function showPickerChoices(arg) {
|
|
|
7058
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);";
|
|
7059
7257
|
const hlLocator = (expr) => {
|
|
7060
7258
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7061
|
-
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;
|
|
7062
7260
|
let html = "";
|
|
7063
7261
|
let last = 0;
|
|
7064
7262
|
let m;
|
|
@@ -8147,7 +8345,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
8147
8345
|
|
|
8148
8346
|
// src/internal/capture/pick-on-failure.ts
|
|
8149
8347
|
var path20 = __toESM(require("path"));
|
|
8150
|
-
var ANSI_RE2 =
|
|
8348
|
+
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
8151
8349
|
function parseLeafLocatorExpression(rawExpr) {
|
|
8152
8350
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
8153
8351
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
@@ -399,7 +399,7 @@ function installPickerOverlay(arg) {
|
|
|
399
399
|
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);`;
|
|
400
400
|
const hlTokens = (expr) => {
|
|
401
401
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
402
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
402
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
403
403
|
let html = "";
|
|
404
404
|
let last = 0;
|
|
405
405
|
let m;
|
|
@@ -942,7 +942,7 @@ function showPickerChoices(arg) {
|
|
|
942
942
|
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);";
|
|
943
943
|
const hlLocator = (expr) => {
|
|
944
944
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
945
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
945
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
946
946
|
let html = "";
|
|
947
947
|
let last = 0;
|
|
948
948
|
let m;
|
|
@@ -2045,6 +2045,7 @@ var DEFAULTS = {
|
|
|
2045
2045
|
streamingBatchSize: 5,
|
|
2046
2046
|
streamingBatchDelay: 2e3,
|
|
2047
2047
|
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
2048
|
+
submitTimeout: 15 * 60 * 1e3,
|
|
2048
2049
|
failOnFlakyTests: false,
|
|
2049
2050
|
username: null,
|
|
2050
2051
|
password: null,
|
|
@@ -2066,6 +2067,7 @@ var PIWI_ENV_KEYS = {
|
|
|
2066
2067
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
2067
2068
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
2068
2069
|
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
2070
|
+
submitTimeout: "PIWI_SUBMIT_TIMEOUT",
|
|
2069
2071
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
2070
2072
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
2071
2073
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -2115,6 +2117,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
2115
2117
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
2116
2118
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
2117
2119
|
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
2120
|
+
{ option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
|
|
2118
2121
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
2119
2122
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
2120
2123
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -2791,15 +2794,19 @@ function extractSelector(text) {
|
|
|
2791
2794
|
return text.slice(start, start + 80);
|
|
2792
2795
|
}
|
|
2793
2796
|
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
2794
|
-
function
|
|
2797
|
+
function extractStackFrames(text) {
|
|
2795
2798
|
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
2799
|
+
const frames = [];
|
|
2796
2800
|
let m;
|
|
2797
2801
|
while ((m = frameRe.exec(text)) !== null) {
|
|
2798
2802
|
const file = m[1].replace(/\\/g, "/");
|
|
2799
2803
|
if (file.includes("node_modules") || file.startsWith("node:")) continue;
|
|
2800
|
-
|
|
2804
|
+
frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
|
|
2801
2805
|
}
|
|
2802
|
-
return
|
|
2806
|
+
return frames;
|
|
2807
|
+
}
|
|
2808
|
+
function extractTopFrame(text) {
|
|
2809
|
+
return extractStackFrames(text)[0] ?? null;
|
|
2803
2810
|
}
|
|
2804
2811
|
function extractTopFrameFile(text) {
|
|
2805
2812
|
return extractTopFrame(text)?.file ?? null;
|
|
@@ -2813,7 +2820,42 @@ var LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
|
|
|
2813
2820
|
var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
|
|
2814
2821
|
var URL_RE = /\bhttps?:\/\/[^\s'"`)]+/gi;
|
|
2815
2822
|
var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
|
|
2816
|
-
var
|
|
2823
|
+
var SELECTOR_OPTION_KEY_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])/gi;
|
|
2824
|
+
var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\n", "\r", "\u2028", "\u2029"]);
|
|
2825
|
+
function closingQuoteIndex(text, quote) {
|
|
2826
|
+
const close = new Int32Array(text.length + 1).fill(-1);
|
|
2827
|
+
for (let p = text.length - 1; p >= 0; p--) {
|
|
2828
|
+
const ch = text[p];
|
|
2829
|
+
if (ch === quote) {
|
|
2830
|
+
close[p] = p;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
const escaped = ch === "\\" && p + 1 < text.length && !LINE_TERMINATORS.has(text[p + 1]) ? close[p + 2] : -1;
|
|
2834
|
+
close[p] = escaped !== -1 ? escaped : close[p + 1];
|
|
2835
|
+
}
|
|
2836
|
+
return close;
|
|
2837
|
+
}
|
|
2838
|
+
function maskSelectorOptions(text) {
|
|
2839
|
+
const closers = /* @__PURE__ */ new Map();
|
|
2840
|
+
let out = "";
|
|
2841
|
+
let copied = 0;
|
|
2842
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = 0;
|
|
2843
|
+
let match;
|
|
2844
|
+
while (match = SELECTOR_OPTION_KEY_RE.exec(text)) {
|
|
2845
|
+
const quote = match[2];
|
|
2846
|
+
let close = closers.get(quote);
|
|
2847
|
+
if (!close) closers.set(quote, close = closingQuoteIndex(text, quote));
|
|
2848
|
+
const end = close[SELECTOR_OPTION_KEY_RE.lastIndex];
|
|
2849
|
+
if (end === -1) {
|
|
2850
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = match.index + 1;
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
out += `${text.slice(copied, match.index)}${match[1]}: <STR>`;
|
|
2854
|
+
copied = end + 1;
|
|
2855
|
+
SELECTOR_OPTION_KEY_RE.lastIndex = copied;
|
|
2856
|
+
}
|
|
2857
|
+
return out + text.slice(copied);
|
|
2858
|
+
}
|
|
2817
2859
|
function classifyError(text) {
|
|
2818
2860
|
if (/strict mode violation/i.test(text)) return "strict-mode";
|
|
2819
2861
|
if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
|
|
@@ -2827,9 +2869,6 @@ function classifyError(text) {
|
|
|
2827
2869
|
function maskVolatile(text) {
|
|
2828
2870
|
return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
|
|
2829
2871
|
}
|
|
2830
|
-
function maskSelectorOptions(text) {
|
|
2831
|
-
return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
|
|
2832
|
-
}
|
|
2833
2872
|
function extractErrorSignature(rawError) {
|
|
2834
2873
|
const text = stripAnsi(rawError);
|
|
2835
2874
|
const errorType = classifyError(text);
|
|
@@ -3216,7 +3255,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
3216
3255
|
|
|
3217
3256
|
// src/internal/capture/pick-on-failure.ts
|
|
3218
3257
|
var path5 = __toESM(require("path"));
|
|
3219
|
-
var ANSI_RE2 =
|
|
3258
|
+
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
3220
3259
|
function parseLeafLocatorExpression(rawExpr) {
|
|
3221
3260
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
3222
3261
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
@@ -85,12 +85,6 @@ declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
|
|
|
85
85
|
declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
|
|
86
86
|
/** Tags whose accessible name comes from an associated `<label>`. */
|
|
87
87
|
declare const FORM_FIELD_TAGS: ReadonlySet<string>;
|
|
88
|
-
/**
|
|
89
|
-
* Roles whose accessible name is computed from the element's own content. For
|
|
90
|
-
* these, an element carrying neither `aria-label` nor `aria-labelledby` is
|
|
91
|
-
* named by its trimmed text — which the probe already returns.
|
|
92
|
-
*/
|
|
93
|
-
declare const NAME_FROM_CONTENT_ROLES: ReadonlySet<string>;
|
|
94
88
|
/** The subset of a probe result the accessible-name shortcut reads. */
|
|
95
89
|
interface NameSource {
|
|
96
90
|
tagName: string;
|
|
@@ -153,4 +147,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
|
|
|
153
147
|
*/
|
|
154
148
|
declare function captureCallerLocation(stack?: string): string | null;
|
|
155
149
|
|
|
156
|
-
export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion,
|
|
150
|
+
export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
|
|
@@ -39,7 +39,6 @@ __export(locator_healing_exports, {
|
|
|
39
39
|
INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
|
|
40
40
|
LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
|
|
41
41
|
LOCATOR_METHODS: () => LOCATOR_METHODS,
|
|
42
|
-
NAME_FROM_CONTENT_ROLES: () => NAME_FROM_CONTENT_ROLES,
|
|
43
42
|
TAG_TO_ROLE: () => TAG_TO_ROLE,
|
|
44
43
|
approximateAccessibleName: () => approximateAccessibleName,
|
|
45
44
|
captureCallerLocation: () => captureCallerLocation,
|
|
@@ -760,7 +759,6 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
760
759
|
INPUT_TYPE_TO_ROLE,
|
|
761
760
|
LOCATOR_CREATING_CHAINS,
|
|
762
761
|
LOCATOR_METHODS,
|
|
763
|
-
NAME_FROM_CONTENT_ROLES,
|
|
764
762
|
TAG_TO_ROLE,
|
|
765
763
|
approximateAccessibleName,
|
|
766
764
|
captureCallerLocation,
|
|
@@ -343,7 +343,7 @@ function installPickerOverlay(arg) {
|
|
|
343
343
|
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);`;
|
|
344
344
|
const hlTokens = (expr) => {
|
|
345
345
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
346
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
346
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
347
347
|
let html = "";
|
|
348
348
|
let last = 0;
|
|
349
349
|
let m;
|
|
@@ -886,7 +886,7 @@ function showPickerChoices(arg) {
|
|
|
886
886
|
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);";
|
|
887
887
|
const hlLocator = (expr) => {
|
|
888
888
|
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
889
|
-
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
889
|
+
const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
890
890
|
let html = "";
|
|
891
891
|
let last = 0;
|
|
892
892
|
let m;
|
|
@@ -1412,7 +1412,7 @@ function renderFailing(failed) {
|
|
|
1412
1412
|
}
|
|
1413
1413
|
|
|
1414
1414
|
// src/internal/capture/pick-on-failure.ts
|
|
1415
|
-
var ANSI_RE =
|
|
1415
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
1416
1416
|
function parseLeafLocatorExpression(rawExpr) {
|
|
1417
1417
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
1418
1418
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|