@piwitests/reporter 0.36.0 → 0.39.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 +420 -8
- package/dist/global-setup-module.js +168 -15
- package/dist/index.d.ts +33 -4
- package/dist/index.js +1092 -153
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +1 -0
- package/dist/internal/capture/capture-fixtures.js +911 -85
- package/dist/internal/capture/pick-on-failure.d.ts +5 -1
- package/dist/internal/capture/pick-on-failure.js +284 -79
- package/package.json +2 -2
- package/templates/skills/write-the-missing-test/SKILL.md +69 -0
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ __export(index_exports, {
|
|
|
43
43
|
module.exports = __toCommonJS(index_exports);
|
|
44
44
|
|
|
45
45
|
// src/public/reporter.ts
|
|
46
|
-
var
|
|
46
|
+
var path17 = __toESM(require("path"));
|
|
47
47
|
|
|
48
48
|
// src/internal/config/desktop.ts
|
|
49
49
|
var fs = __toESM(require("fs"));
|
|
@@ -76,8 +76,12 @@ var DEFAULTS = {
|
|
|
76
76
|
collectPerformanceMetrics: true,
|
|
77
77
|
captureLocators: true,
|
|
78
78
|
capturePageState: true,
|
|
79
|
+
// Off by default: the page inventory reads control and link names from every
|
|
80
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
81
|
+
capturePageInventory: false,
|
|
79
82
|
captureServerTraces: true,
|
|
80
83
|
sampleAriaOnPass: true,
|
|
84
|
+
uploadManifest: true,
|
|
81
85
|
defaultCapture: true,
|
|
82
86
|
streaming: true,
|
|
83
87
|
streamingBatchSize: 5,
|
|
@@ -99,6 +103,7 @@ var PIWI_ENV_KEYS = {
|
|
|
99
103
|
environment: "PIWI_ENVIRONMENT",
|
|
100
104
|
label: "PIWI_LABEL",
|
|
101
105
|
runLabel: "PIWI_RUN_LABEL",
|
|
106
|
+
keep: "PIWI_KEEP",
|
|
102
107
|
streaming: "PIWI_STREAMING",
|
|
103
108
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
104
109
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
@@ -109,8 +114,10 @@ var PIWI_ENV_KEYS = {
|
|
|
109
114
|
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
110
115
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
111
116
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
117
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
112
118
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
113
119
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
120
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
114
121
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
115
122
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
116
123
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
@@ -132,6 +139,12 @@ var PIWI_SELECTION_ENV = {
|
|
|
132
139
|
hash: "PIWI_SELECTION_HASH",
|
|
133
140
|
count: "PIWI_SELECTION_COUNT"
|
|
134
141
|
};
|
|
142
|
+
var PIWI_PROBE_ENV = {
|
|
143
|
+
flag: "PIWI_PROBE",
|
|
144
|
+
plan: "PIWI_PROBE_PLAN",
|
|
145
|
+
results: "PIWI_PROBE_RESULTS",
|
|
146
|
+
secret: "PIWI_PROBE_SECRET"
|
|
147
|
+
};
|
|
135
148
|
function readBool(val) {
|
|
136
149
|
if (val === void 0) return void 0;
|
|
137
150
|
return val === "true";
|
|
@@ -145,6 +158,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
145
158
|
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
146
159
|
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
147
160
|
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
161
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
148
162
|
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
149
163
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
150
164
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
@@ -155,8 +169,10 @@ var ENV_FALLBACK_SPECS = [
|
|
|
155
169
|
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
156
170
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
157
171
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
172
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
158
173
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
159
174
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
175
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
160
176
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
161
177
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
162
178
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
@@ -204,12 +220,16 @@ function applyOptionsToEnv(options) {
|
|
|
204
220
|
if (options.environment) env[PIWI_ENV_KEYS.environment] = options.environment;
|
|
205
221
|
if (options.label) env[PIWI_ENV_KEYS.label] = options.label;
|
|
206
222
|
if (options.runLabel) env[PIWI_ENV_KEYS.runLabel] = options.runLabel;
|
|
223
|
+
if (options.keep) env[PIWI_ENV_KEYS.keep] = "true";
|
|
207
224
|
if (options.captureLocators === false || options.collectPerformanceMetrics === false)
|
|
208
225
|
env[PIWI_ENV_KEYS.captureLocators] = "false";
|
|
209
226
|
else if (options.captureLocators === true) env[PIWI_ENV_KEYS.captureLocators] = "true";
|
|
210
227
|
if (options.capturePageState === false || options.collectPerformanceMetrics === false)
|
|
211
228
|
env[PIWI_ENV_KEYS.capturePageState] = "false";
|
|
212
229
|
else if (options.capturePageState === true) env[PIWI_ENV_KEYS.capturePageState] = "true";
|
|
230
|
+
if (options.capturePageInventory === false || options.collectPerformanceMetrics === false)
|
|
231
|
+
env[PIWI_ENV_KEYS.capturePageInventory] = "false";
|
|
232
|
+
else if (options.capturePageInventory === true) env[PIWI_ENV_KEYS.capturePageInventory] = "true";
|
|
213
233
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
214
234
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
215
235
|
else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
|
|
@@ -398,6 +418,27 @@ var HttpClient = class {
|
|
|
398
418
|
return {};
|
|
399
419
|
}
|
|
400
420
|
}
|
|
421
|
+
/** Send a JSON PUT request. Used for the declared-surface manifest upload. */
|
|
422
|
+
async putJSON(pathname, payload, auth) {
|
|
423
|
+
const body = JSON.stringify(payload);
|
|
424
|
+
const res = await this.request("PUT", pathname, {
|
|
425
|
+
headers: {
|
|
426
|
+
"Content-Type": "application/json",
|
|
427
|
+
"Content-Length": Buffer.byteLength(body)
|
|
428
|
+
},
|
|
429
|
+
body,
|
|
430
|
+
auth
|
|
431
|
+
});
|
|
432
|
+
if (res.status < 200 || res.status >= 300) {
|
|
433
|
+
this.logger.debugError(`Response: ${res.text}`);
|
|
434
|
+
throw new HttpError(res.status);
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
return JSON.parse(res.text);
|
|
438
|
+
} catch {
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
401
442
|
/** Send a multipart form-data POST request. Used for report and trace uploads. */
|
|
402
443
|
async postFormData(pathname, form, auth) {
|
|
403
444
|
const headers = form.getHeaders();
|
|
@@ -511,6 +552,7 @@ function toWireTestCase(tc) {
|
|
|
511
552
|
networkRequests: rest.networkRequests || null,
|
|
512
553
|
webVitals: rest.webVitals || null,
|
|
513
554
|
pageState: rest.pageState || null,
|
|
555
|
+
pageInventory: rest.pageInventory || null,
|
|
514
556
|
aiUsage: rest.aiUsage || null,
|
|
515
557
|
consoleLogs: rest.consoleLogs || null,
|
|
516
558
|
dialogs: rest.dialogs || null,
|
|
@@ -579,6 +621,9 @@ function serializeRun(payload, opts) {
|
|
|
579
621
|
didNotRunTests: payload.didNotRunTests ?? 0,
|
|
580
622
|
environment: payload.environment ?? null,
|
|
581
623
|
label: payload.label ?? null,
|
|
624
|
+
keep: payload.keep === true,
|
|
625
|
+
// A probe run is stamped in `onBegin` (see the reporter), so the marker
|
|
626
|
+
// already rides in `payload.metadata` here and on every other submit path.
|
|
582
627
|
metadata: payload.metadata,
|
|
583
628
|
instanceId: payload.instanceId,
|
|
584
629
|
playwrightVersion: payload.playwrightVersion,
|
|
@@ -950,6 +995,7 @@ var ATTACHMENT_NAMES = {
|
|
|
950
995
|
console: "piwi-console",
|
|
951
996
|
dialogs: "piwi-dialogs",
|
|
952
997
|
network: "piwi-network",
|
|
998
|
+
pageInventory: "piwi-page-inventory",
|
|
953
999
|
webVitals: "piwi-web-vitals",
|
|
954
1000
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
955
1001
|
pageState: "piwi-page-state",
|
|
@@ -1152,6 +1198,13 @@ var FileHandler = class {
|
|
|
1152
1198
|
} catch {
|
|
1153
1199
|
}
|
|
1154
1200
|
}
|
|
1201
|
+
const pageInventory = find(ATTACHMENT_NAMES.pageInventory);
|
|
1202
|
+
if (pageInventory?.body) {
|
|
1203
|
+
try {
|
|
1204
|
+
testCase.pageInventory = JSON.parse(pageInventory.body.toString());
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1155
1208
|
const aiUsage = find(ATTACHMENT_NAMES.aiUsage);
|
|
1156
1209
|
if (aiUsage?.body) {
|
|
1157
1210
|
try {
|
|
@@ -1253,7 +1306,7 @@ var MetadataCollector = class {
|
|
|
1253
1306
|
}
|
|
1254
1307
|
/** Collect all available metadata from the environment, config, and suite */
|
|
1255
1308
|
collect(config, suite, options) {
|
|
1256
|
-
const metadata = {};
|
|
1309
|
+
const metadata = { workingDir: process.cwd() };
|
|
1257
1310
|
if (options.projectDescription) metadata.projectDescription = options.projectDescription;
|
|
1258
1311
|
if (options.relatedIssue) metadata.relatedIssue = options.relatedIssue;
|
|
1259
1312
|
if (options.ciInfo) metadata.ciInfo = options.ciInfo;
|
|
@@ -1329,7 +1382,7 @@ var MetadataCollector = class {
|
|
|
1329
1382
|
/**
|
|
1330
1383
|
* Walk the test's parent `describe` suites to extract the suite path (titles)
|
|
1331
1384
|
* and per-level config (parallel mode + annotations). Reaches into Playwright
|
|
1332
|
-
* suite internals (`_parallelMode`, `
|
|
1385
|
+
* suite internals (`_parallelMode`, `_staticAnnotations`) — kept here next to
|
|
1333
1386
|
* `getBrowserConfig` so all such access is guarded behind one class.
|
|
1334
1387
|
*/
|
|
1335
1388
|
getSuiteInfo(test) {
|
|
@@ -1346,7 +1399,10 @@ var MetadataCollector = class {
|
|
|
1346
1399
|
suitePath.push(s.title);
|
|
1347
1400
|
const rawMode = s._parallelMode;
|
|
1348
1401
|
const mode = rawMode === "parallel" ? "parallel" : rawMode === "serial" ? "serial" : "default";
|
|
1349
|
-
const
|
|
1402
|
+
const staticAnnotations = s._staticAnnotations ?? [];
|
|
1403
|
+
const annotations = staticAnnotations.map(
|
|
1404
|
+
(a) => a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description }
|
|
1405
|
+
);
|
|
1350
1406
|
suiteConfig.push({ mode, annotations });
|
|
1351
1407
|
}
|
|
1352
1408
|
return { suitePath, suiteConfig };
|
|
@@ -1441,6 +1497,11 @@ var MetadataCollector = class {
|
|
|
1441
1497
|
testDir: p.testDir,
|
|
1442
1498
|
use: {
|
|
1443
1499
|
browserName: p.use?.browserName || p.name,
|
|
1500
|
+
// The origin of `baseURL` is what the graph treats as the run's own
|
|
1501
|
+
// surface, so only requests to it become route nodes.
|
|
1502
|
+
baseURL: p.use?.baseURL,
|
|
1503
|
+
// The attribute `getByTestId` reads, so the locator index can resolve those locators on a live page.
|
|
1504
|
+
testIdAttribute: p.use?.testIdAttribute,
|
|
1444
1505
|
viewport: p.use?.viewport,
|
|
1445
1506
|
deviceScaleFactor: p.use?.deviceScaleFactor
|
|
1446
1507
|
}
|
|
@@ -1757,6 +1818,7 @@ var StreamManager = class {
|
|
|
1757
1818
|
startTime,
|
|
1758
1819
|
environment: this.options.environment || null,
|
|
1759
1820
|
label: this.options.label || null,
|
|
1821
|
+
keep: this.options.keep === true,
|
|
1760
1822
|
metadata,
|
|
1761
1823
|
instanceId,
|
|
1762
1824
|
playwrightVersion,
|
|
@@ -1779,6 +1841,7 @@ var StreamManager = class {
|
|
|
1779
1841
|
startTime,
|
|
1780
1842
|
environment: this.options.environment || null,
|
|
1781
1843
|
label: this.options.label || null,
|
|
1844
|
+
keep: this.options.keep === true,
|
|
1782
1845
|
metadata,
|
|
1783
1846
|
instanceId,
|
|
1784
1847
|
playwrightVersion,
|
|
@@ -2080,6 +2143,9 @@ function maskTokenLike(text) {
|
|
|
2080
2143
|
// ../core/src/step-analysis.ts
|
|
2081
2144
|
var MAX_STEP_PARAM_KEYS = 20;
|
|
2082
2145
|
var MAX_STEP_PARAM_VALUE_CHARS = 200;
|
|
2146
|
+
function capStepValue(value, max) {
|
|
2147
|
+
return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
|
|
2148
|
+
}
|
|
2083
2149
|
function categorizeStep(title, pwCategory, params) {
|
|
2084
2150
|
if (!title) return "other";
|
|
2085
2151
|
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
@@ -2110,11 +2176,11 @@ function normalizeStepParams(raw) {
|
|
|
2110
2176
|
out[key] = value;
|
|
2111
2177
|
count++;
|
|
2112
2178
|
} else if (typeof value === "string") {
|
|
2113
|
-
out[key] = maskTokenLike(value)
|
|
2179
|
+
out[key] = capStepValue(maskTokenLike(value), MAX_STEP_PARAM_VALUE_CHARS);
|
|
2114
2180
|
count++;
|
|
2115
2181
|
} else if (value != null) {
|
|
2116
2182
|
try {
|
|
2117
|
-
out[key] = maskTokenLike(JSON.stringify(value))
|
|
2183
|
+
out[key] = capStepValue(maskTokenLike(JSON.stringify(value)), MAX_STEP_PARAM_VALUE_CHARS);
|
|
2118
2184
|
count++;
|
|
2119
2185
|
} catch {
|
|
2120
2186
|
}
|
|
@@ -2437,8 +2503,8 @@ function isListMode(argv = process.argv) {
|
|
|
2437
2503
|
}
|
|
2438
2504
|
|
|
2439
2505
|
// src/public/global-setup.ts
|
|
2440
|
-
var
|
|
2441
|
-
var
|
|
2506
|
+
var path12 = __toESM(require("path"));
|
|
2507
|
+
var fs13 = __toESM(require("fs"));
|
|
2442
2508
|
|
|
2443
2509
|
// src/internal/support/aria-sampling.ts
|
|
2444
2510
|
var path10 = __toESM(require("path"));
|
|
@@ -2489,17 +2555,83 @@ function isDueForAriaSample(testInfo) {
|
|
|
2489
2555
|
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
2490
2556
|
}
|
|
2491
2557
|
|
|
2558
|
+
// src/internal/manifest/collect.ts
|
|
2559
|
+
var fs12 = __toESM(require("fs"));
|
|
2560
|
+
var path11 = __toESM(require("path"));
|
|
2561
|
+
var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
2562
|
+
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
2563
|
+
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
2564
|
+
function parseManifestJson(text) {
|
|
2565
|
+
let parsed;
|
|
2566
|
+
try {
|
|
2567
|
+
parsed = JSON.parse(text);
|
|
2568
|
+
} catch {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2572
|
+
const obj = parsed;
|
|
2573
|
+
const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
|
|
2574
|
+
method: r.method,
|
|
2575
|
+
pattern: r.pattern,
|
|
2576
|
+
handler: typeof r.handler === "string" ? r.handler : void 0,
|
|
2577
|
+
responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
|
|
2578
|
+
})) : void 0;
|
|
2579
|
+
const pages = Array.isArray(obj.pages) ? obj.pages.filter((p) => !!p && typeof p === "object").filter((p) => typeof p.pattern === "string").map((p) => ({ pattern: p.pattern, name: typeof p.name === "string" ? p.name : void 0 })) : void 0;
|
|
2580
|
+
if (!routes && !pages) return null;
|
|
2581
|
+
return { routes, pages };
|
|
2582
|
+
}
|
|
2583
|
+
function hasInstrumentationHeader(headers) {
|
|
2584
|
+
const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
|
|
2585
|
+
return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
|
|
2586
|
+
}
|
|
2587
|
+
function readCommittedManifest(configDir) {
|
|
2588
|
+
const file = path11.join(configDir, COMMITTED_MANIFEST_FILE);
|
|
2589
|
+
try {
|
|
2590
|
+
if (!fs12.existsSync(file)) return null;
|
|
2591
|
+
return parseManifestJson(fs12.readFileSync(file, "utf8"));
|
|
2592
|
+
} catch {
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function configDirFromConfig(config) {
|
|
2597
|
+
if (config?.configFile) return path11.dirname(config.configFile);
|
|
2598
|
+
if (config?.rootDir) return config.rootDir;
|
|
2599
|
+
return process.cwd();
|
|
2600
|
+
}
|
|
2601
|
+
function baseUrlFromConfig(config) {
|
|
2602
|
+
const top = config?.use?.baseURL;
|
|
2603
|
+
if (typeof top === "string" && top) return top;
|
|
2604
|
+
for (const project of config?.projects ?? []) {
|
|
2605
|
+
const url = project?.use?.baseURL;
|
|
2606
|
+
if (typeof url === "string" && url) return url;
|
|
2607
|
+
}
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
|
|
2611
|
+
async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
|
|
2612
|
+
try {
|
|
2613
|
+
const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
2614
|
+
if (!hasInstrumentationHeader(probe.headers)) return null;
|
|
2615
|
+
const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
|
|
2616
|
+
const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
2617
|
+
if (!res.ok) return null;
|
|
2618
|
+
return parseManifestJson(await res.text());
|
|
2619
|
+
} catch {
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2492
2624
|
// src/public/global-setup.ts
|
|
2493
2625
|
function createGlobalSetup(options, userSetup) {
|
|
2494
2626
|
return async function globalSetupFn(config) {
|
|
2495
|
-
const piwiReporterPath =
|
|
2627
|
+
const piwiReporterPath = path12.resolve(__dirname, "./index.js");
|
|
2496
2628
|
let inlineReporterOptions = {};
|
|
2497
2629
|
if (Array.isArray(config?.reporter)) {
|
|
2498
2630
|
for (const r of config.reporter) {
|
|
2499
2631
|
if (!Array.isArray(r) || typeof r[0] !== "string") continue;
|
|
2500
2632
|
const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
|
|
2501
2633
|
try {
|
|
2502
|
-
return
|
|
2634
|
+
return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2503
2635
|
} catch {
|
|
2504
2636
|
return false;
|
|
2505
2637
|
}
|
|
@@ -2531,7 +2663,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2531
2663
|
if (!Array.isArray(r) || typeof r[0] !== "string") return false;
|
|
2532
2664
|
if (r[0].toLowerCase().includes("piwi")) return true;
|
|
2533
2665
|
try {
|
|
2534
|
-
return
|
|
2666
|
+
return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2535
2667
|
} catch {
|
|
2536
2668
|
return false;
|
|
2537
2669
|
}
|
|
@@ -2555,6 +2687,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2555
2687
|
projectDescription: opts.projectDescription,
|
|
2556
2688
|
environment: opts.environment || null,
|
|
2557
2689
|
label: opts.label || null,
|
|
2690
|
+
keep: opts.keep === true,
|
|
2558
2691
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2559
2692
|
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
2560
2693
|
shardIndex,
|
|
@@ -2563,7 +2696,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2563
2696
|
auth
|
|
2564
2697
|
);
|
|
2565
2698
|
if (response?.runId && response?.setupToken) {
|
|
2566
|
-
|
|
2699
|
+
fs13.writeFileSync(
|
|
2567
2700
|
getSetupFilePath(opts.projectName),
|
|
2568
2701
|
JSON.stringify({
|
|
2569
2702
|
runId: response.runId,
|
|
@@ -2574,18 +2707,43 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2574
2707
|
logger.debug(`Global setup: initializing run #${response.runId}`);
|
|
2575
2708
|
}
|
|
2576
2709
|
if (opts.projectName) clearAriaSampleFile(opts.projectName);
|
|
2577
|
-
|
|
2710
|
+
const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
|
|
2711
|
+
const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
|
|
2712
|
+
const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
|
|
2713
|
+
const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
|
|
2714
|
+
let projectId;
|
|
2715
|
+
if ((wantsAria || wantsManifest) && opts.projectName) {
|
|
2578
2716
|
const menu = await httpClient.getJSON("/api/projects/menu", auth);
|
|
2579
|
-
|
|
2717
|
+
projectId = menu?.items?.find(
|
|
2580
2718
|
(p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
|
|
2581
2719
|
)?.id;
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2720
|
+
}
|
|
2721
|
+
if (wantsAria && projectId != null) {
|
|
2722
|
+
const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
|
|
2723
|
+
const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
|
|
2724
|
+
if (tests) {
|
|
2725
|
+
const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
|
|
2726
|
+
writeAriaSampleFile(opts.projectName, identities);
|
|
2727
|
+
logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
if (wantsManifest && projectId != null) {
|
|
2731
|
+
const branch = resolveScmBranch(process.env) ?? null;
|
|
2732
|
+
if (committedManifest) {
|
|
2733
|
+
await httpClient.putJSON(
|
|
2734
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
2735
|
+
{ source: "committed", branch, manifest: committedManifest },
|
|
2736
|
+
auth
|
|
2737
|
+
).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
|
|
2738
|
+
}
|
|
2739
|
+
if (manifestBaseUrl) {
|
|
2740
|
+
const declared = await fetchInstrumentationManifest(manifestBaseUrl);
|
|
2741
|
+
if (declared) {
|
|
2742
|
+
await httpClient.putJSON(
|
|
2743
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
2744
|
+
{ source: "instrumentation", branch, manifest: declared },
|
|
2745
|
+
auth
|
|
2746
|
+
).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
|
|
2589
2747
|
}
|
|
2590
2748
|
}
|
|
2591
2749
|
}
|
|
@@ -2597,8 +2755,110 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2597
2755
|
}
|
|
2598
2756
|
|
|
2599
2757
|
// src/public/config-wrapper.ts
|
|
2600
|
-
var
|
|
2601
|
-
var
|
|
2758
|
+
var path14 = __toESM(require("path"));
|
|
2759
|
+
var fs15 = __toESM(require("fs"));
|
|
2760
|
+
|
|
2761
|
+
// src/internal/probe/mode.ts
|
|
2762
|
+
var fs14 = __toESM(require("fs"));
|
|
2763
|
+
var path13 = __toESM(require("path"));
|
|
2764
|
+
|
|
2765
|
+
// src/internal/probe/plan.ts
|
|
2766
|
+
function probeRunMetadata(base = {}) {
|
|
2767
|
+
return { ...base, piwiProbe: true };
|
|
2768
|
+
}
|
|
2769
|
+
function parseRouteKey(routeKey) {
|
|
2770
|
+
const space = routeKey.indexOf(" ");
|
|
2771
|
+
if (space < 0) return { method: "", pattern: routeKey };
|
|
2772
|
+
return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
|
|
2773
|
+
}
|
|
2774
|
+
function patternToRegex(pattern) {
|
|
2775
|
+
const path23 = pattern.split("?")[0] ?? pattern;
|
|
2776
|
+
const escaped = path23.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2777
|
+
return new RegExp(`^${escaped}$`);
|
|
2778
|
+
}
|
|
2779
|
+
function suitePathEqual(a, b) {
|
|
2780
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
2781
|
+
return a.every((title, i) => title === b[i]);
|
|
2782
|
+
}
|
|
2783
|
+
function matchProbeItem(plan, test) {
|
|
2784
|
+
const byFileTitle = plan.items.filter(
|
|
2785
|
+
(item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
|
|
2786
|
+
);
|
|
2787
|
+
if (byFileTitle.length === 1) return byFileTitle[0];
|
|
2788
|
+
if (byFileTitle.length > 1) {
|
|
2789
|
+
return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
|
|
2790
|
+
}
|
|
2791
|
+
for (const item of plan.items) {
|
|
2792
|
+
if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
|
|
2793
|
+
}
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
function requestMatchesRoute(method, url, routeKey) {
|
|
2797
|
+
const { method: rm, pattern } = parseRouteKey(routeKey);
|
|
2798
|
+
if (rm && rm !== method.toUpperCase()) return false;
|
|
2799
|
+
let path23;
|
|
2800
|
+
try {
|
|
2801
|
+
path23 = new URL(url).pathname;
|
|
2802
|
+
} catch {
|
|
2803
|
+
path23 = url.split("?")[0] ?? url;
|
|
2804
|
+
}
|
|
2805
|
+
return patternToRegex(pattern).test(path23);
|
|
2806
|
+
}
|
|
2807
|
+
function createProbeState() {
|
|
2808
|
+
return { navigated: false, matches: 0 };
|
|
2809
|
+
}
|
|
2810
|
+
function markNavigated(state) {
|
|
2811
|
+
state.navigated = true;
|
|
2812
|
+
}
|
|
2813
|
+
function shouldMutate(state, item) {
|
|
2814
|
+
if (!state.navigated) return false;
|
|
2815
|
+
state.matches += 1;
|
|
2816
|
+
return state.matches === Math.max(1, item.nth ?? 1);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// src/internal/probe/mode.ts
|
|
2820
|
+
var planCache;
|
|
2821
|
+
function isProbeMode() {
|
|
2822
|
+
return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
|
|
2823
|
+
}
|
|
2824
|
+
function loadPlan() {
|
|
2825
|
+
if (planCache !== void 0) return planCache;
|
|
2826
|
+
planCache = null;
|
|
2827
|
+
const path23 = process.env[PIWI_PROBE_ENV.plan];
|
|
2828
|
+
if (path23) {
|
|
2829
|
+
try {
|
|
2830
|
+
planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
|
|
2831
|
+
} catch {
|
|
2832
|
+
planCache = null;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
return planCache;
|
|
2836
|
+
}
|
|
2837
|
+
function probeItemForTest(test) {
|
|
2838
|
+
const plan = loadPlan();
|
|
2839
|
+
if (!plan) return null;
|
|
2840
|
+
const filePath = test.file ? path13.relative(process.cwd(), test.file).split(path13.sep).join("/") : null;
|
|
2841
|
+
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
2842
|
+
return matchProbeItem(plan, { title: test.title, filePath, suitePath });
|
|
2843
|
+
}
|
|
2844
|
+
function classifyProbeHandled(signals) {
|
|
2845
|
+
return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
|
|
2846
|
+
}
|
|
2847
|
+
function outcomeFromStatus(status, applied) {
|
|
2848
|
+
if (!applied) return "inconclusive";
|
|
2849
|
+
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2850
|
+
}
|
|
2851
|
+
function recordProbeOutcome(line) {
|
|
2852
|
+
const path23 = process.env[PIWI_PROBE_ENV.results];
|
|
2853
|
+
if (!path23) return;
|
|
2854
|
+
try {
|
|
2855
|
+
fs14.appendFileSync(path23, `${JSON.stringify(line)}
|
|
2856
|
+
`);
|
|
2857
|
+
} catch {
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// src/public/config-wrapper.ts
|
|
2602
2862
|
var PIWI_MODULE = "@piwitests/reporter";
|
|
2603
2863
|
var CAPTURE_DEFAULTS = {
|
|
2604
2864
|
screenshot: "only-on-failure",
|
|
@@ -2663,11 +2923,11 @@ function injectReporter(reporter, piwiOptions) {
|
|
|
2663
2923
|
}
|
|
2664
2924
|
function resolveSetupModule() {
|
|
2665
2925
|
const candidates = [
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2926
|
+
path14.join(__dirname, "global-setup-module.js"),
|
|
2927
|
+
path14.join(__dirname, "..", "global-setup-module.js"),
|
|
2928
|
+
path14.join(__dirname, "..", "global-setup-module.ts")
|
|
2669
2929
|
];
|
|
2670
|
-
return candidates.find((candidate) =>
|
|
2930
|
+
return candidates.find((candidate) => fs15.existsSync(candidate)) ?? candidates[0];
|
|
2671
2931
|
}
|
|
2672
2932
|
function wrapConfig(config, piwiOptions) {
|
|
2673
2933
|
if (piwiOptions) applyOptionsToEnv(piwiOptions);
|
|
@@ -2679,6 +2939,7 @@ function wrapConfig(config, piwiOptions) {
|
|
|
2679
2939
|
const forwarded = {};
|
|
2680
2940
|
const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
|
|
2681
2941
|
if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
|
|
2942
|
+
if (isProbeMode()) forwarded.retries = 0;
|
|
2682
2943
|
const use = applyCaptureDefaults(config.use, piwiOptions);
|
|
2683
2944
|
return {
|
|
2684
2945
|
...config,
|
|
@@ -2867,7 +3128,7 @@ function collectTestMetadata(annotations) {
|
|
|
2867
3128
|
}
|
|
2868
3129
|
|
|
2869
3130
|
// src/internal/collect/error-text.ts
|
|
2870
|
-
var
|
|
3131
|
+
var path15 = __toESM(require("path"));
|
|
2871
3132
|
|
|
2872
3133
|
// ../core/src/error-text.ts
|
|
2873
3134
|
function joinErrorMessages(errors) {
|
|
@@ -2896,13 +3157,13 @@ function buildErrorText(result) {
|
|
|
2896
3157
|
if (!text) return null;
|
|
2897
3158
|
const loc = result.error?.location;
|
|
2898
3159
|
if (!loc?.file) return text;
|
|
2899
|
-
const rel =
|
|
3160
|
+
const rel = path15.relative(process.cwd(), loc.file).split(path15.sep).join("/");
|
|
2900
3161
|
return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
|
|
2901
3162
|
}
|
|
2902
3163
|
|
|
2903
3164
|
// src/internal/support/ci-output.ts
|
|
2904
|
-
var
|
|
2905
|
-
var
|
|
3165
|
+
var fs16 = __toESM(require("fs"));
|
|
3166
|
+
var path16 = __toESM(require("path"));
|
|
2906
3167
|
var SUMMARY_MAX_FAILURES = 20;
|
|
2907
3168
|
function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
2908
3169
|
logger.info(`View run: ${output.runUrl}`);
|
|
@@ -2912,9 +3173,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
|
2912
3173
|
}
|
|
2913
3174
|
function writeOutputFile(file, output, logger) {
|
|
2914
3175
|
try {
|
|
2915
|
-
const dir =
|
|
2916
|
-
if (dir && dir !== ".")
|
|
2917
|
-
|
|
3176
|
+
const dir = path16.dirname(file);
|
|
3177
|
+
if (dir && dir !== ".") fs16.mkdirSync(dir, { recursive: true });
|
|
3178
|
+
fs16.writeFileSync(
|
|
2918
3179
|
file,
|
|
2919
3180
|
JSON.stringify(
|
|
2920
3181
|
{
|
|
@@ -2994,7 +3255,7 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
2994
3255
|
if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
|
|
2995
3256
|
if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
|
|
2996
3257
|
try {
|
|
2997
|
-
|
|
3258
|
+
fs16.writeFileSync(file, lines.join("\n") + "\n");
|
|
2998
3259
|
logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
|
|
2999
3260
|
} catch (error) {
|
|
3000
3261
|
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
@@ -3002,7 +3263,7 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
3002
3263
|
}
|
|
3003
3264
|
function appendFileLines(file, lines, logger, label) {
|
|
3004
3265
|
try {
|
|
3005
|
-
|
|
3266
|
+
fs16.appendFileSync(file, lines.join("\n") + "\n");
|
|
3006
3267
|
} catch (error) {
|
|
3007
3268
|
logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
|
|
3008
3269
|
}
|
|
@@ -3121,6 +3382,7 @@ var RunSubmitter = class {
|
|
|
3121
3382
|
didNotRunTests: run.didNotRunTests,
|
|
3122
3383
|
environment: run.options.environment,
|
|
3123
3384
|
label: run.options.label || null,
|
|
3385
|
+
keep: run.options.keep === true,
|
|
3124
3386
|
metadata: run.metadata,
|
|
3125
3387
|
instanceId: run.instanceId,
|
|
3126
3388
|
playwrightVersion: run.playwrightVersion ?? void 0,
|
|
@@ -3151,6 +3413,7 @@ var RunSubmitter = class {
|
|
|
3151
3413
|
flakyTests,
|
|
3152
3414
|
durations,
|
|
3153
3415
|
label: run.options.label || null,
|
|
3416
|
+
keep: run.options.keep === true,
|
|
3154
3417
|
metadata: run.metadata,
|
|
3155
3418
|
hasPendingUploads: this.hasReports(run),
|
|
3156
3419
|
playwrightVersion: run.playwrightVersion ?? void 0,
|
|
@@ -3238,6 +3501,298 @@ var RunSubmitter = class {
|
|
|
3238
3501
|
}
|
|
3239
3502
|
};
|
|
3240
3503
|
|
|
3504
|
+
// ../core/src/locator-chain.ts
|
|
3505
|
+
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
3506
|
+
"getByRole",
|
|
3507
|
+
"getByTestId",
|
|
3508
|
+
"getByText",
|
|
3509
|
+
"getByLabel",
|
|
3510
|
+
"getByPlaceholder",
|
|
3511
|
+
"getByAltText",
|
|
3512
|
+
"getByTitle",
|
|
3513
|
+
"locator",
|
|
3514
|
+
"frameLocator"
|
|
3515
|
+
]);
|
|
3516
|
+
var NARROWING_METHODS = /* @__PURE__ */ new Set([
|
|
3517
|
+
"filter",
|
|
3518
|
+
"first",
|
|
3519
|
+
"last",
|
|
3520
|
+
"nth",
|
|
3521
|
+
"and",
|
|
3522
|
+
"or",
|
|
3523
|
+
"visible",
|
|
3524
|
+
"contentFrame",
|
|
3525
|
+
"owner"
|
|
3526
|
+
]);
|
|
3527
|
+
var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
|
|
3528
|
+
var LocatorParseError = class extends Error {
|
|
3529
|
+
constructor(message) {
|
|
3530
|
+
super(message);
|
|
3531
|
+
this.name = "LocatorParseError";
|
|
3532
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
var SIMPLE_ESCAPES = {
|
|
3535
|
+
n: "\n",
|
|
3536
|
+
t: " ",
|
|
3537
|
+
r: "\r",
|
|
3538
|
+
b: "\b",
|
|
3539
|
+
f: "\f",
|
|
3540
|
+
v: "\v",
|
|
3541
|
+
"0": "\0"
|
|
3542
|
+
};
|
|
3543
|
+
var Parser = class {
|
|
3544
|
+
constructor(src) {
|
|
3545
|
+
this.src = src;
|
|
3546
|
+
this.i = 0;
|
|
3547
|
+
}
|
|
3548
|
+
parseTopLevel() {
|
|
3549
|
+
const chain = this.parseChain();
|
|
3550
|
+
this.skipSpace();
|
|
3551
|
+
if (this.i < this.src.length) this.fail("unexpected text");
|
|
3552
|
+
return chain;
|
|
3553
|
+
}
|
|
3554
|
+
fail(message) {
|
|
3555
|
+
throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
|
|
3556
|
+
}
|
|
3557
|
+
skipSpace() {
|
|
3558
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
3559
|
+
}
|
|
3560
|
+
peek() {
|
|
3561
|
+
return this.src[this.i];
|
|
3562
|
+
}
|
|
3563
|
+
expect(ch) {
|
|
3564
|
+
this.skipSpace();
|
|
3565
|
+
if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
|
|
3566
|
+
this.i++;
|
|
3567
|
+
}
|
|
3568
|
+
identifier() {
|
|
3569
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
|
|
3570
|
+
if (!m) this.fail("expected a name");
|
|
3571
|
+
this.i += m[0].length;
|
|
3572
|
+
return m[0];
|
|
3573
|
+
}
|
|
3574
|
+
/**
|
|
3575
|
+
* A chain of calls joined by `.`. With `lenient`, parsing stops before the
|
|
3576
|
+
* first link that doesn't parse instead of failing, so a chain followed by
|
|
3577
|
+
* other text (an error message) is read up to where it ends. `spans` receives
|
|
3578
|
+
* where each top-level call starts and ends.
|
|
3579
|
+
*/
|
|
3580
|
+
parseChain(opts = {}) {
|
|
3581
|
+
this.skipSpace();
|
|
3582
|
+
const calls = [];
|
|
3583
|
+
for (; ; ) {
|
|
3584
|
+
const before = this.i;
|
|
3585
|
+
if (calls.length > 0) this.i++;
|
|
3586
|
+
this.skipSpace();
|
|
3587
|
+
const start = this.i;
|
|
3588
|
+
try {
|
|
3589
|
+
calls.push(this.parseCall(calls.length === 0));
|
|
3590
|
+
} catch (error) {
|
|
3591
|
+
if (!opts.lenient || calls.length === 0) throw error;
|
|
3592
|
+
this.i = before;
|
|
3593
|
+
break;
|
|
3594
|
+
}
|
|
3595
|
+
opts.spans?.push({ start, end: this.i });
|
|
3596
|
+
const end = this.i;
|
|
3597
|
+
this.skipSpace();
|
|
3598
|
+
if (this.peek() !== ".") {
|
|
3599
|
+
this.i = end;
|
|
3600
|
+
break;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
return { calls };
|
|
3604
|
+
}
|
|
3605
|
+
/** Where the parser stopped. */
|
|
3606
|
+
get position() {
|
|
3607
|
+
return this.i;
|
|
3608
|
+
}
|
|
3609
|
+
parseCall(first) {
|
|
3610
|
+
this.skipSpace();
|
|
3611
|
+
const method = this.identifier();
|
|
3612
|
+
if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
|
|
3613
|
+
if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
|
|
3614
|
+
this.expect("(");
|
|
3615
|
+
const args = [];
|
|
3616
|
+
this.skipSpace();
|
|
3617
|
+
if (this.peek() === ")") {
|
|
3618
|
+
this.i++;
|
|
3619
|
+
return { method, args };
|
|
3620
|
+
}
|
|
3621
|
+
for (; ; ) {
|
|
3622
|
+
args.push(this.parseValue());
|
|
3623
|
+
this.skipSpace();
|
|
3624
|
+
if (this.peek() === ",") {
|
|
3625
|
+
this.i++;
|
|
3626
|
+
this.skipSpace();
|
|
3627
|
+
if (this.peek() === ")") {
|
|
3628
|
+
this.i++;
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
this.expect(")");
|
|
3634
|
+
break;
|
|
3635
|
+
}
|
|
3636
|
+
return { method, args };
|
|
3637
|
+
}
|
|
3638
|
+
parseValue() {
|
|
3639
|
+
this.skipSpace();
|
|
3640
|
+
const ch = this.peek();
|
|
3641
|
+
if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
|
|
3642
|
+
if (ch === "/") return this.parseRegex();
|
|
3643
|
+
if (ch === "{") return this.parseObject();
|
|
3644
|
+
if (ch !== void 0 && /[-\d]/.test(ch)) {
|
|
3645
|
+
const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
|
|
3646
|
+
if (!m) this.fail("expected a number");
|
|
3647
|
+
this.i += m[0].length;
|
|
3648
|
+
return { type: "number", value: Number(m[0]) };
|
|
3649
|
+
}
|
|
3650
|
+
const rest = this.src.slice(this.i);
|
|
3651
|
+
if (/^true\b/.test(rest)) {
|
|
3652
|
+
this.i += 4;
|
|
3653
|
+
return { type: "boolean", value: true };
|
|
3654
|
+
}
|
|
3655
|
+
if (/^false\b/.test(rest)) {
|
|
3656
|
+
this.i += 5;
|
|
3657
|
+
return { type: "boolean", value: false };
|
|
3658
|
+
}
|
|
3659
|
+
if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
|
|
3660
|
+
return this.fail("expected a value");
|
|
3661
|
+
}
|
|
3662
|
+
/** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
|
|
3663
|
+
parseString() {
|
|
3664
|
+
const quote = this.src[this.i];
|
|
3665
|
+
let out = "";
|
|
3666
|
+
this.i++;
|
|
3667
|
+
while (this.i < this.src.length) {
|
|
3668
|
+
const c = this.src[this.i];
|
|
3669
|
+
if (c === quote) {
|
|
3670
|
+
this.i++;
|
|
3671
|
+
return out;
|
|
3672
|
+
}
|
|
3673
|
+
if (c !== "\\") {
|
|
3674
|
+
out += c;
|
|
3675
|
+
this.i++;
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3678
|
+
const next = this.src[this.i + 1];
|
|
3679
|
+
if (next === void 0) break;
|
|
3680
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
3681
|
+
if (simple !== void 0) {
|
|
3682
|
+
out += simple;
|
|
3683
|
+
this.i += 2;
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
|
|
3687
|
+
if (hex) {
|
|
3688
|
+
out += String.fromCodePoint(parseInt(hex[1], 16));
|
|
3689
|
+
this.i += 1 + hex[0].length;
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
if (next === "u" || next === "x") this.fail("malformed escape");
|
|
3693
|
+
out += next;
|
|
3694
|
+
this.i += 2;
|
|
3695
|
+
}
|
|
3696
|
+
return this.fail("unterminated string");
|
|
3697
|
+
}
|
|
3698
|
+
parseRegex() {
|
|
3699
|
+
let j = this.i + 1;
|
|
3700
|
+
let inClass = false;
|
|
3701
|
+
while (j < this.src.length) {
|
|
3702
|
+
const c = this.src[j];
|
|
3703
|
+
if (c === "\\") {
|
|
3704
|
+
j += 2;
|
|
3705
|
+
continue;
|
|
3706
|
+
}
|
|
3707
|
+
if (c === "[") inClass = true;
|
|
3708
|
+
else if (c === "]") inClass = false;
|
|
3709
|
+
else if (c === "/" && !inClass) break;
|
|
3710
|
+
j++;
|
|
3711
|
+
}
|
|
3712
|
+
if (j >= this.src.length) this.fail("unterminated regex");
|
|
3713
|
+
const source = this.src.slice(this.i + 1, j);
|
|
3714
|
+
const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
|
|
3715
|
+
this.i = j + 1 + flags.length;
|
|
3716
|
+
return { type: "regex", source, flags };
|
|
3717
|
+
}
|
|
3718
|
+
parseObject() {
|
|
3719
|
+
this.expect("{");
|
|
3720
|
+
const entries = [];
|
|
3721
|
+
for (; ; ) {
|
|
3722
|
+
this.skipSpace();
|
|
3723
|
+
if (this.peek() === "}") {
|
|
3724
|
+
this.i++;
|
|
3725
|
+
break;
|
|
3726
|
+
}
|
|
3727
|
+
const ch = this.peek();
|
|
3728
|
+
const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
|
|
3729
|
+
this.expect(":");
|
|
3730
|
+
entries.push([key, this.parseValue()]);
|
|
3731
|
+
this.skipSpace();
|
|
3732
|
+
if (this.peek() === ",") {
|
|
3733
|
+
this.i++;
|
|
3734
|
+
continue;
|
|
3735
|
+
}
|
|
3736
|
+
this.expect("}");
|
|
3737
|
+
break;
|
|
3738
|
+
}
|
|
3739
|
+
return { type: "object", entries };
|
|
3740
|
+
}
|
|
3741
|
+
};
|
|
3742
|
+
function parseLocatorChain(expr) {
|
|
3743
|
+
const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
|
|
3744
|
+
if (!trimmed) throw new LocatorParseError("empty expression");
|
|
3745
|
+
return new Parser(trimmed).parseTopLevel();
|
|
3746
|
+
}
|
|
3747
|
+
function scanLocatorChain(text) {
|
|
3748
|
+
const parser = new Parser(text);
|
|
3749
|
+
const spans = [];
|
|
3750
|
+
try {
|
|
3751
|
+
const chain = parser.parseChain({ lenient: true, spans });
|
|
3752
|
+
return { chain, spans, end: parser.position };
|
|
3753
|
+
} catch {
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
function tryParseLocatorChain(expr) {
|
|
3758
|
+
try {
|
|
3759
|
+
return parseLocatorChain(expr);
|
|
3760
|
+
} catch {
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
function parseLeafLocatorCall(expr) {
|
|
3765
|
+
const chain = tryParseLocatorChain(expr);
|
|
3766
|
+
if (!chain) return null;
|
|
3767
|
+
for (let i = chain.calls.length - 1; i >= 0; i--) {
|
|
3768
|
+
if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
|
|
3769
|
+
}
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3772
|
+
function locatorCallValues(call, opts = {}) {
|
|
3773
|
+
const value = (arg) => {
|
|
3774
|
+
switch (arg.type) {
|
|
3775
|
+
case "string":
|
|
3776
|
+
case "number":
|
|
3777
|
+
case "boolean":
|
|
3778
|
+
return arg.value;
|
|
3779
|
+
case "regex":
|
|
3780
|
+
return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
|
|
3781
|
+
case "object": {
|
|
3782
|
+
const out = {};
|
|
3783
|
+
for (const [key, v] of arg.entries) {
|
|
3784
|
+
const plain = value(v);
|
|
3785
|
+
if (plain !== void 0) out[key] = plain;
|
|
3786
|
+
}
|
|
3787
|
+
return out;
|
|
3788
|
+
}
|
|
3789
|
+
case "chain":
|
|
3790
|
+
return void 0;
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
return call.args.map(value).filter((v) => v !== void 0);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3241
3796
|
// ../core/src/locator-methods.ts
|
|
3242
3797
|
var LOCATOR_BUILDER_METHODS = [
|
|
3243
3798
|
"getByRole",
|
|
@@ -3282,10 +3837,35 @@ function extractLeafSelector(text) {
|
|
|
3282
3837
|
if (!first) return null;
|
|
3283
3838
|
const nl = text.indexOf("\n", first.index);
|
|
3284
3839
|
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3840
|
+
const scanned = readChain(region);
|
|
3841
|
+
if (scanned) {
|
|
3842
|
+
for (let k = scanned.chain.calls.length - 1; k >= 0; k--) {
|
|
3843
|
+
if (LEAF_METHODS.has(scanned.chain.calls[k].method)) {
|
|
3844
|
+
return region.slice(scanned.spans[k].start, scanned.spans[k].end);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return null;
|
|
3848
|
+
}
|
|
3849
|
+
return lenientLeaf(region);
|
|
3850
|
+
}
|
|
3851
|
+
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
3852
|
+
function skipQuoted(text, i) {
|
|
3853
|
+
const quote = text[i];
|
|
3854
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
3855
|
+
if (text[j] === "\\") j++;
|
|
3856
|
+
else if (text[j] === quote) return j + 1;
|
|
3857
|
+
}
|
|
3858
|
+
return text.length;
|
|
3859
|
+
}
|
|
3860
|
+
function lenientLeaf(region) {
|
|
3285
3861
|
let depth = 0;
|
|
3286
3862
|
let leafStart = -1;
|
|
3287
3863
|
for (let i = 0; i < region.length; i++) {
|
|
3288
3864
|
const ch = region[i];
|
|
3865
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3866
|
+
i = skipQuoted(region, i) - 1;
|
|
3867
|
+
continue;
|
|
3868
|
+
}
|
|
3289
3869
|
if (ch === "(") {
|
|
3290
3870
|
depth++;
|
|
3291
3871
|
continue;
|
|
@@ -3308,7 +3888,9 @@ function extractLeafSelector(text) {
|
|
|
3308
3888
|
depth = 0;
|
|
3309
3889
|
for (let i = leafStart; i < region.length; i++) {
|
|
3310
3890
|
const ch = region[i];
|
|
3311
|
-
if (ch === "
|
|
3891
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3892
|
+
i = skipQuoted(region, i) - 1;
|
|
3893
|
+
} else if (ch === "(") {
|
|
3312
3894
|
depth++;
|
|
3313
3895
|
} else if (ch === ")") {
|
|
3314
3896
|
depth--;
|
|
@@ -3322,11 +3904,15 @@ function extractLocatorChain(text) {
|
|
|
3322
3904
|
if (!first) return null;
|
|
3323
3905
|
const nl = text.indexOf("\n", first.index);
|
|
3324
3906
|
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3907
|
+
const scanned = readChain(region);
|
|
3908
|
+
if (scanned) return region.slice(0, scanned.end);
|
|
3325
3909
|
let depth = 0;
|
|
3326
3910
|
let end = -1;
|
|
3327
3911
|
for (let i = 0; i < region.length; i++) {
|
|
3328
3912
|
const ch = region[i];
|
|
3329
|
-
if (ch === "
|
|
3913
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3914
|
+
i = skipQuoted(region, i) - 1;
|
|
3915
|
+
} else if (ch === "(") {
|
|
3330
3916
|
depth++;
|
|
3331
3917
|
} else if (ch === ")") {
|
|
3332
3918
|
if (depth > 0) depth--;
|
|
@@ -3341,6 +3927,12 @@ function extractLocatorChain(text) {
|
|
|
3341
3927
|
if (end === -1) return region.slice(0, 80);
|
|
3342
3928
|
return region.slice(0, end);
|
|
3343
3929
|
}
|
|
3930
|
+
function readChain(region) {
|
|
3931
|
+
const scanned = scanLocatorChain(region);
|
|
3932
|
+
if (!scanned) return null;
|
|
3933
|
+
const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
|
|
3934
|
+
return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
|
|
3935
|
+
}
|
|
3344
3936
|
function extractTopFrame(text) {
|
|
3345
3937
|
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
3346
3938
|
let m;
|
|
@@ -4064,7 +4656,7 @@ function testLocation(test) {
|
|
|
4064
4656
|
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
4065
4657
|
}
|
|
4066
4658
|
function testFile(test) {
|
|
4067
|
-
return
|
|
4659
|
+
return path17.relative(process.cwd(), test.location.file).split(path17.sep).join("/");
|
|
4068
4660
|
}
|
|
4069
4661
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
4070
4662
|
constructor(rawOptions = {}) {
|
|
@@ -4181,6 +4773,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4181
4773
|
);
|
|
4182
4774
|
}
|
|
4183
4775
|
this.metadata = this.metadataCollector.collect(config, suite, this.options);
|
|
4776
|
+
if (isProbeMode()) this.metadata = probeRunMetadata(this.metadata);
|
|
4184
4777
|
this.plannedTests = suite.allTests();
|
|
4185
4778
|
const pwShard = config.shard;
|
|
4186
4779
|
if (pwShard?.total && pwShard.total > 1) {
|
|
@@ -4219,18 +4812,27 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4219
4812
|
}
|
|
4220
4813
|
}
|
|
4221
4814
|
static {
|
|
4222
|
-
/**
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4815
|
+
/** Step categories streamed live while the run executes. */
|
|
4816
|
+
this.LIVE_STEP_CATEGORIES = /* @__PURE__ */ new Set(["hook", "fixture", "pw:api", "expect"]);
|
|
4817
|
+
}
|
|
4818
|
+
/**
|
|
4819
|
+
* Whether a step streams live: its category is in `LIVE_STEP_CATEGORIES`
|
|
4820
|
+
* and no ancestor is an `expect` step. The steps inside an assertion are its
|
|
4821
|
+
* polling — every `expect.poll` attempt and `toPass` retry — not a step a
|
|
4822
|
+
* human watches; the meaningful readout is the assertion around them.
|
|
4823
|
+
*/
|
|
4824
|
+
static isLiveStep(step) {
|
|
4825
|
+
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(step.category)) return false;
|
|
4826
|
+
for (let parent = step.parent; parent; parent = parent.parent) {
|
|
4827
|
+
if (parent.category === "expect") return false;
|
|
4828
|
+
}
|
|
4829
|
+
return true;
|
|
4228
4830
|
}
|
|
4229
4831
|
/** Playwright reporter hook: called when a step (including hook/fixture) begins */
|
|
4230
4832
|
onStepBegin(test, _result, step) {
|
|
4231
4833
|
if (!this.enabled || !this.streamManager) return;
|
|
4834
|
+
if (!_PiwiDashboardReporter.isLiveStep(step)) return;
|
|
4232
4835
|
const cat = step.category;
|
|
4233
|
-
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
|
|
4234
4836
|
const event = {
|
|
4235
4837
|
type: "step-begin",
|
|
4236
4838
|
title: step.title,
|
|
@@ -4246,8 +4848,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4246
4848
|
/** Playwright reporter hook: called when a step (including hook/fixture) ends */
|
|
4247
4849
|
onStepEnd(test, _result, step) {
|
|
4248
4850
|
if (!this.enabled || !this.streamManager) return;
|
|
4851
|
+
if (!_PiwiDashboardReporter.isLiveStep(step)) return;
|
|
4249
4852
|
const cat = step.category;
|
|
4250
|
-
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
|
|
4251
4853
|
const workerIndex = workerIndexOf(_result);
|
|
4252
4854
|
const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
|
|
4253
4855
|
const event = {
|
|
@@ -4537,7 +5139,7 @@ async function resolveSelection(options = {}) {
|
|
|
4537
5139
|
}
|
|
4538
5140
|
|
|
4539
5141
|
// src/internal/capture/capture-fixtures.ts
|
|
4540
|
-
var
|
|
5142
|
+
var import_node_zlib2 = require("zlib");
|
|
4541
5143
|
|
|
4542
5144
|
// ../picker-dom/src/probe.ts
|
|
4543
5145
|
function probeElementAttrs(el, arg) {
|
|
@@ -5895,7 +6497,7 @@ function mergeCandidates(base, extra) {
|
|
|
5895
6497
|
}
|
|
5896
6498
|
|
|
5897
6499
|
// src/internal/capture/locator-healing.ts
|
|
5898
|
-
var
|
|
6500
|
+
var path18 = __toESM(require("path"));
|
|
5899
6501
|
|
|
5900
6502
|
// ../core/src/locator-fingerprint.ts
|
|
5901
6503
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -6265,16 +6867,318 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
6265
6867
|
}
|
|
6266
6868
|
let rel = file;
|
|
6267
6869
|
try {
|
|
6268
|
-
rel =
|
|
6870
|
+
rel = path18.relative(process.cwd(), file);
|
|
6269
6871
|
} catch {
|
|
6270
6872
|
}
|
|
6271
|
-
rel = rel.split(
|
|
6873
|
+
rel = rel.split(path18.sep).join("/");
|
|
6272
6874
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
6273
6875
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
6274
6876
|
}
|
|
6275
6877
|
return null;
|
|
6276
6878
|
}
|
|
6277
6879
|
|
|
6880
|
+
// src/internal/capture/page-inventory.ts
|
|
6881
|
+
var PAGE_INVENTORY_MAX_ENTRIES = 500;
|
|
6882
|
+
function collectPageInventoryInPage(maxEntries) {
|
|
6883
|
+
const g = globalThis;
|
|
6884
|
+
const doc = g.document;
|
|
6885
|
+
if (!doc || !g.location) return null;
|
|
6886
|
+
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
6887
|
+
const roleOf = (el) => {
|
|
6888
|
+
const explicit = el.getAttribute("role");
|
|
6889
|
+
if (explicit) return explicit.toLowerCase();
|
|
6890
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
6891
|
+
if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
|
|
6892
|
+
if (tag === "button") return "button";
|
|
6893
|
+
if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
6894
|
+
if (tag === "textarea") return "textbox";
|
|
6895
|
+
if (tag === "input") {
|
|
6896
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
6897
|
+
const map = {
|
|
6898
|
+
checkbox: "checkbox",
|
|
6899
|
+
radio: "radio",
|
|
6900
|
+
button: "button",
|
|
6901
|
+
submit: "button",
|
|
6902
|
+
reset: "button",
|
|
6903
|
+
image: "button",
|
|
6904
|
+
range: "slider",
|
|
6905
|
+
search: "searchbox",
|
|
6906
|
+
email: "textbox",
|
|
6907
|
+
tel: "textbox",
|
|
6908
|
+
url: "textbox",
|
|
6909
|
+
number: "spinbutton"
|
|
6910
|
+
};
|
|
6911
|
+
return map[type] || "textbox";
|
|
6912
|
+
}
|
|
6913
|
+
return "generic";
|
|
6914
|
+
};
|
|
6915
|
+
const holdsValue = (el, role) => {
|
|
6916
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
6917
|
+
if (tag === "select" || tag === "textarea" || tag === "input") return true;
|
|
6918
|
+
if (el.isContentEditable === true) return true;
|
|
6919
|
+
const editable = el.getAttribute("contenteditable");
|
|
6920
|
+
if (editable === "" || editable === "true") return true;
|
|
6921
|
+
return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
|
|
6922
|
+
};
|
|
6923
|
+
const labelText = (el) => {
|
|
6924
|
+
const labels = el.labels;
|
|
6925
|
+
if (!labels || labels.length === 0) return "";
|
|
6926
|
+
let text = "";
|
|
6927
|
+
for (const label of labels) text += ` ${label.textContent || ""}`;
|
|
6928
|
+
return text;
|
|
6929
|
+
};
|
|
6930
|
+
const labelledByText = (el) => {
|
|
6931
|
+
const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
6932
|
+
if (ids.length === 0) return "";
|
|
6933
|
+
let text = "";
|
|
6934
|
+
for (const id of ids) {
|
|
6935
|
+
const ref = doc.getElementById(id);
|
|
6936
|
+
if (ref) text += ` ${ref.textContent || ""}`;
|
|
6937
|
+
}
|
|
6938
|
+
return text;
|
|
6939
|
+
};
|
|
6940
|
+
const nameOf = (el) => {
|
|
6941
|
+
const role = roleOf(el);
|
|
6942
|
+
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
6943
|
+
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
6944
|
+
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
6945
|
+
return clean(chain.find((s) => s && String(s).trim()) || "");
|
|
6946
|
+
};
|
|
6947
|
+
const cleanHref = (href) => {
|
|
6948
|
+
const cut = href.search(/[?#]/);
|
|
6949
|
+
return cut >= 0 ? href.slice(0, cut) : href;
|
|
6950
|
+
};
|
|
6951
|
+
const controls = [];
|
|
6952
|
+
const links = [];
|
|
6953
|
+
const seenC = /* @__PURE__ */ new Set();
|
|
6954
|
+
const seenL = /* @__PURE__ */ new Set();
|
|
6955
|
+
let budget = maxEntries;
|
|
6956
|
+
const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
|
|
6957
|
+
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
6958
|
+
if (budget <= 0) break;
|
|
6959
|
+
const role = roleOf(el);
|
|
6960
|
+
const name = nameOf(el);
|
|
6961
|
+
if (!name) continue;
|
|
6962
|
+
const key = `${role}\0${name}`;
|
|
6963
|
+
if (seenC.has(key)) continue;
|
|
6964
|
+
seenC.add(key);
|
|
6965
|
+
controls.push({ role, name });
|
|
6966
|
+
budget--;
|
|
6967
|
+
}
|
|
6968
|
+
for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
|
|
6969
|
+
if (budget <= 0) break;
|
|
6970
|
+
const name = nameOf(el);
|
|
6971
|
+
const href = cleanHref(el.getAttribute("href") || "");
|
|
6972
|
+
if (!name) continue;
|
|
6973
|
+
const key = `${name}\0${href}`;
|
|
6974
|
+
if (seenL.has(key)) continue;
|
|
6975
|
+
seenL.add(key);
|
|
6976
|
+
links.push({ name, href });
|
|
6977
|
+
budget--;
|
|
6978
|
+
}
|
|
6979
|
+
return { url: g.location.href, controls, links };
|
|
6980
|
+
}
|
|
6981
|
+
function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
|
|
6982
|
+
const controls = inventory.controls.slice(0, Math.max(0, max));
|
|
6983
|
+
const links = inventory.links.slice(0, Math.max(0, max - controls.length));
|
|
6984
|
+
return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
|
|
6985
|
+
}
|
|
6986
|
+
function inventoryPageKey(url) {
|
|
6987
|
+
try {
|
|
6988
|
+
const u = new URL(url);
|
|
6989
|
+
return `${u.origin}${u.pathname}`;
|
|
6990
|
+
} catch {
|
|
6991
|
+
return null;
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6995
|
+
// src/internal/probe/interception.ts
|
|
6996
|
+
var import_node_zlib = require("zlib");
|
|
6997
|
+
|
|
6998
|
+
// src/internal/probe/faults.ts
|
|
6999
|
+
var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
|
|
7000
|
+
var SERVER_PROBE_FAULTS = [
|
|
7001
|
+
"throw",
|
|
7002
|
+
"status",
|
|
7003
|
+
"delay",
|
|
7004
|
+
"dependency",
|
|
7005
|
+
"data",
|
|
7006
|
+
"extreme",
|
|
7007
|
+
"replay",
|
|
7008
|
+
"auth",
|
|
7009
|
+
"slow-first"
|
|
7010
|
+
];
|
|
7011
|
+
function isFaultAllowed(level, fault) {
|
|
7012
|
+
const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
|
|
7013
|
+
return list.includes(fault);
|
|
7014
|
+
}
|
|
7015
|
+
var SLOW_FAULT_DELAY_MS = 5e3;
|
|
7016
|
+
function dropOneField(body) {
|
|
7017
|
+
let parsed;
|
|
7018
|
+
try {
|
|
7019
|
+
parsed = JSON.parse(body);
|
|
7020
|
+
} catch {
|
|
7021
|
+
return body;
|
|
7022
|
+
}
|
|
7023
|
+
if (Array.isArray(parsed)) {
|
|
7024
|
+
let key;
|
|
7025
|
+
for (const el of parsed) {
|
|
7026
|
+
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
7027
|
+
key ??= Object.keys(el)[0];
|
|
7028
|
+
if (key) delete el[key];
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
return JSON.stringify(parsed);
|
|
7032
|
+
}
|
|
7033
|
+
if (parsed && typeof parsed === "object") {
|
|
7034
|
+
const obj = parsed;
|
|
7035
|
+
const key = Object.keys(obj)[0];
|
|
7036
|
+
if (key) delete obj[key];
|
|
7037
|
+
return JSON.stringify(obj);
|
|
7038
|
+
}
|
|
7039
|
+
return body;
|
|
7040
|
+
}
|
|
7041
|
+
function computeFault(fault, input) {
|
|
7042
|
+
const base = {
|
|
7043
|
+
status: input.status,
|
|
7044
|
+
body: input.body,
|
|
7045
|
+
contentType: input.contentType ?? null,
|
|
7046
|
+
delayMs: 0,
|
|
7047
|
+
changed: false
|
|
7048
|
+
};
|
|
7049
|
+
switch (fault) {
|
|
7050
|
+
case "status-500":
|
|
7051
|
+
return { ...base, status: 500, changed: input.status !== 500 };
|
|
7052
|
+
case "empty-body":
|
|
7053
|
+
return { ...base, body: "", changed: input.body.length > 0 };
|
|
7054
|
+
case "drop-field": {
|
|
7055
|
+
const body = dropOneField(input.body);
|
|
7056
|
+
return { ...base, body, changed: body !== input.body };
|
|
7057
|
+
}
|
|
7058
|
+
case "stale-value": {
|
|
7059
|
+
const body = input.priorBody ?? input.body;
|
|
7060
|
+
return { ...base, body, changed: body !== input.body };
|
|
7061
|
+
}
|
|
7062
|
+
case "slow":
|
|
7063
|
+
return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
|
|
7064
|
+
default:
|
|
7065
|
+
return base;
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
|
|
7069
|
+
// src/internal/probe/sign.ts
|
|
7070
|
+
var import_node_crypto = require("crypto");
|
|
7071
|
+
function signProbeMessage(secret, nonce, ts, specJson) {
|
|
7072
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
|
|
7073
|
+
}
|
|
7074
|
+
function buildProbeHeader(secret, spec, now = Date.now()) {
|
|
7075
|
+
const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
7076
|
+
const specJson = JSON.stringify(spec);
|
|
7077
|
+
const sig = signProbeMessage(secret, nonce, now, specJson);
|
|
7078
|
+
return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
// src/internal/probe/interception.ts
|
|
7082
|
+
function parseProbeTrace(traceHeader) {
|
|
7083
|
+
const none = { appliedFault: null, serverError: false };
|
|
7084
|
+
if (!traceHeader) return none;
|
|
7085
|
+
try {
|
|
7086
|
+
const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
|
|
7087
|
+
if (!Array.isArray(spans)) return none;
|
|
7088
|
+
const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
|
|
7089
|
+
if (!root) return none;
|
|
7090
|
+
const attrs = root.attrs ?? {};
|
|
7091
|
+
const appliedRaw = attrs["piwi.probe.applied"];
|
|
7092
|
+
const status = Number(attrs["http.status_code"]) || 0;
|
|
7093
|
+
return {
|
|
7094
|
+
appliedFault: appliedRaw != null ? String(appliedRaw) : null,
|
|
7095
|
+
serverError: root.status === "error" || status >= 500
|
|
7096
|
+
};
|
|
7097
|
+
} catch {
|
|
7098
|
+
return none;
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
function fulfillHeaders(responseHeaders, contentType) {
|
|
7102
|
+
const headers = { ...responseHeaders };
|
|
7103
|
+
delete headers["content-length"];
|
|
7104
|
+
delete headers["content-encoding"];
|
|
7105
|
+
if (contentType) headers["content-type"] = contentType;
|
|
7106
|
+
return headers;
|
|
7107
|
+
}
|
|
7108
|
+
function originOf(url) {
|
|
7109
|
+
try {
|
|
7110
|
+
const origin = new URL(url).origin;
|
|
7111
|
+
return origin && origin !== "null" ? origin : null;
|
|
7112
|
+
} catch {
|
|
7113
|
+
return null;
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
async function installProbeInterception(page, item) {
|
|
7117
|
+
if (!isFaultAllowed(item.level ?? "client", item.fault)) {
|
|
7118
|
+
return { applied: () => false, appliedFault: () => null, serverError: () => false };
|
|
7119
|
+
}
|
|
7120
|
+
const state = createProbeState();
|
|
7121
|
+
let applied = false;
|
|
7122
|
+
let appliedFault = null;
|
|
7123
|
+
let serverError = false;
|
|
7124
|
+
let appOrigin = null;
|
|
7125
|
+
const probeSecret = process.env[PIWI_PROBE_ENV.secret];
|
|
7126
|
+
page.on("framenavigated", (frame) => {
|
|
7127
|
+
if (frame === page.mainFrame()) {
|
|
7128
|
+
markNavigated(state);
|
|
7129
|
+
const origin = originOf(page.url());
|
|
7130
|
+
if (origin) appOrigin = origin;
|
|
7131
|
+
}
|
|
7132
|
+
});
|
|
7133
|
+
await page.route("**/*", async (route) => {
|
|
7134
|
+
const request = route.request();
|
|
7135
|
+
if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
|
|
7136
|
+
await route.fallback();
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
if (item.level === "server") {
|
|
7140
|
+
if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
|
|
7141
|
+
await route.fallback();
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
7144
|
+
const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
|
|
7145
|
+
if (item.dependency) spec.dependency = item.dependency;
|
|
7146
|
+
try {
|
|
7147
|
+
const response = await route.fetch({
|
|
7148
|
+
headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
|
|
7149
|
+
});
|
|
7150
|
+
const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
|
|
7151
|
+
appliedFault = trace.appliedFault;
|
|
7152
|
+
serverError = trace.serverError;
|
|
7153
|
+
applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
|
|
7154
|
+
await route.fulfill({ response });
|
|
7155
|
+
} catch {
|
|
7156
|
+
applied = false;
|
|
7157
|
+
await route.fallback();
|
|
7158
|
+
}
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
try {
|
|
7162
|
+
const response = await route.fetch();
|
|
7163
|
+
const body = await response.text();
|
|
7164
|
+
const out = computeFault(item.fault, {
|
|
7165
|
+
status: response.status(),
|
|
7166
|
+
body,
|
|
7167
|
+
contentType: response.headers()["content-type"] ?? null
|
|
7168
|
+
});
|
|
7169
|
+
applied = out.changed;
|
|
7170
|
+
if (applied) appliedFault = item.fault;
|
|
7171
|
+
if (out.delayMs > 0) await new Promise((resolve5) => setTimeout(resolve5, out.delayMs));
|
|
7172
|
+
const headers = fulfillHeaders(response.headers(), out.contentType);
|
|
7173
|
+
await route.fulfill({ status: out.status, body: out.body, headers });
|
|
7174
|
+
} catch {
|
|
7175
|
+
applied = false;
|
|
7176
|
+
await route.fallback();
|
|
7177
|
+
}
|
|
7178
|
+
});
|
|
7179
|
+
return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
|
|
7180
|
+
}
|
|
7181
|
+
|
|
6278
7182
|
// src/internal/capture/inspect-on-failure.ts
|
|
6279
7183
|
function isCi(ci) {
|
|
6280
7184
|
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
@@ -6311,88 +7215,11 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
6311
7215
|
}
|
|
6312
7216
|
|
|
6313
7217
|
// src/internal/capture/pick-on-failure.ts
|
|
6314
|
-
var
|
|
7218
|
+
var path19 = __toESM(require("path"));
|
|
6315
7219
|
var ANSI_RE2 = /\[[0-9;]*m/g;
|
|
6316
|
-
function endOfString(s, start) {
|
|
6317
|
-
const q = s[start];
|
|
6318
|
-
for (let i = start + 1; i < s.length; i++) {
|
|
6319
|
-
if (s[i] === "\\") {
|
|
6320
|
-
i++;
|
|
6321
|
-
continue;
|
|
6322
|
-
}
|
|
6323
|
-
if (s[i] === q) return i;
|
|
6324
|
-
}
|
|
6325
|
-
return s.length - 1;
|
|
6326
|
-
}
|
|
6327
|
-
function matchBrace(s, start) {
|
|
6328
|
-
let depth = 0;
|
|
6329
|
-
for (let i = start; i < s.length; i++) {
|
|
6330
|
-
if (s[i] === "{") depth++;
|
|
6331
|
-
else if (s[i] === "}" && --depth === 0) return i;
|
|
6332
|
-
}
|
|
6333
|
-
return s.length - 1;
|
|
6334
|
-
}
|
|
6335
|
-
function parseOptions(src) {
|
|
6336
|
-
const obj = {};
|
|
6337
|
-
const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
|
|
6338
|
-
let m;
|
|
6339
|
-
while ((m = re.exec(src)) !== null) {
|
|
6340
|
-
const key = m[1];
|
|
6341
|
-
const raw = m[2];
|
|
6342
|
-
if (raw === "true") obj[key] = true;
|
|
6343
|
-
else if (raw === "false") obj[key] = false;
|
|
6344
|
-
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
6345
|
-
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
6346
|
-
}
|
|
6347
|
-
return obj;
|
|
6348
|
-
}
|
|
6349
|
-
function parseArgs(inner) {
|
|
6350
|
-
const args = [];
|
|
6351
|
-
let i = 0;
|
|
6352
|
-
while (i < inner.length) {
|
|
6353
|
-
const c = inner[i];
|
|
6354
|
-
if (c === " " || c === ",") {
|
|
6355
|
-
i++;
|
|
6356
|
-
continue;
|
|
6357
|
-
}
|
|
6358
|
-
if (c === "'" || c === '"') {
|
|
6359
|
-
const end = endOfString(inner, i);
|
|
6360
|
-
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
6361
|
-
i = end + 1;
|
|
6362
|
-
continue;
|
|
6363
|
-
}
|
|
6364
|
-
if (c === "{") {
|
|
6365
|
-
const end = matchBrace(inner, i);
|
|
6366
|
-
args.push(parseOptions(inner.slice(i, end + 1)));
|
|
6367
|
-
i = end + 1;
|
|
6368
|
-
continue;
|
|
6369
|
-
}
|
|
6370
|
-
i++;
|
|
6371
|
-
}
|
|
6372
|
-
return args;
|
|
6373
|
-
}
|
|
6374
|
-
function leafExpression(expr) {
|
|
6375
|
-
let depth = 0;
|
|
6376
|
-
let leafStart = 0;
|
|
6377
|
-
for (let i = 0; i < expr.length - 1; i++) {
|
|
6378
|
-
const c = expr[i];
|
|
6379
|
-
if (c === "'" || c === '"') {
|
|
6380
|
-
i = endOfString(expr, i);
|
|
6381
|
-
continue;
|
|
6382
|
-
}
|
|
6383
|
-
if (c === "(") depth++;
|
|
6384
|
-
else if (c === ")") {
|
|
6385
|
-
depth--;
|
|
6386
|
-
if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
|
|
6387
|
-
}
|
|
6388
|
-
}
|
|
6389
|
-
return expr.slice(leafStart);
|
|
6390
|
-
}
|
|
6391
7220
|
function parseLeafLocatorExpression(rawExpr) {
|
|
6392
|
-
const
|
|
6393
|
-
|
|
6394
|
-
if (!m) return null;
|
|
6395
|
-
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
7221
|
+
const leaf = parseLeafLocatorCall(rawExpr);
|
|
7222
|
+
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
6396
7223
|
}
|
|
6397
7224
|
function deriveFailedLocator(testInfo) {
|
|
6398
7225
|
const info = testInfo;
|
|
@@ -6405,7 +7232,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
|
6405
7232
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
6406
7233
|
if (!parsed) continue;
|
|
6407
7234
|
const loc = err.location;
|
|
6408
|
-
const location = loc ? `${
|
|
7235
|
+
const location = loc ? `${path19.relative(process.cwd(), loc.file).split(path19.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
6409
7236
|
return { method: parsed.method, args: parsed.args, location };
|
|
6410
7237
|
}
|
|
6411
7238
|
return null;
|
|
@@ -6599,6 +7426,9 @@ function createSink() {
|
|
|
6599
7426
|
stashedPageState: null,
|
|
6600
7427
|
stashedAria: null,
|
|
6601
7428
|
stashedAriaJson: null,
|
|
7429
|
+
pageInventories: [],
|
|
7430
|
+
probeItem: null,
|
|
7431
|
+
probeInterception: null,
|
|
6602
7432
|
pickOffered: false,
|
|
6603
7433
|
userPick: null
|
|
6604
7434
|
};
|
|
@@ -6775,6 +7605,62 @@ async function readPageState(page) {
|
|
|
6775
7605
|
return null;
|
|
6776
7606
|
}
|
|
6777
7607
|
}
|
|
7608
|
+
var inventoriedPageKeys = /* @__PURE__ */ new Set();
|
|
7609
|
+
var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
7610
|
+
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
7611
|
+
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
7612
|
+
function withTimeout(promise, ms) {
|
|
7613
|
+
return new Promise((resolve5, reject) => {
|
|
7614
|
+
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
7615
|
+
promise.then(
|
|
7616
|
+
(value) => {
|
|
7617
|
+
clearTimeout(timer);
|
|
7618
|
+
resolve5(value);
|
|
7619
|
+
},
|
|
7620
|
+
(error) => {
|
|
7621
|
+
clearTimeout(timer);
|
|
7622
|
+
reject(error);
|
|
7623
|
+
}
|
|
7624
|
+
);
|
|
7625
|
+
});
|
|
7626
|
+
}
|
|
7627
|
+
async function readPageInventory(page) {
|
|
7628
|
+
try {
|
|
7629
|
+
const inventory = await withTimeout(
|
|
7630
|
+
page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
|
|
7631
|
+
PAGE_INVENTORY_EVAL_TIMEOUT_MS
|
|
7632
|
+
);
|
|
7633
|
+
return inventory ? { ...inventory, capturedAt: Date.now() } : null;
|
|
7634
|
+
} catch {
|
|
7635
|
+
return null;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
async function recordPageWindow(sink, page) {
|
|
7639
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7640
|
+
let url;
|
|
7641
|
+
try {
|
|
7642
|
+
url = page.url();
|
|
7643
|
+
} catch {
|
|
7644
|
+
return;
|
|
7645
|
+
}
|
|
7646
|
+
if (!url || url.startsWith("about:")) return;
|
|
7647
|
+
const key = inventoryPageKey(url);
|
|
7648
|
+
const capturedAt = Date.now();
|
|
7649
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7650
|
+
if (key && !inventoriedPageKeys.has(key)) {
|
|
7651
|
+
inventoriedPageKeys.add(key);
|
|
7652
|
+
const content = await readPageInventory(page);
|
|
7653
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7654
|
+
sink.pageInventories.push({
|
|
7655
|
+
url: content?.url ?? url,
|
|
7656
|
+
controls: content?.controls ?? [],
|
|
7657
|
+
links: content?.links ?? [],
|
|
7658
|
+
capturedAt
|
|
7659
|
+
});
|
|
7660
|
+
return;
|
|
7661
|
+
}
|
|
7662
|
+
sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
|
|
7663
|
+
}
|
|
6778
7664
|
async function stashPageState(sink, closing) {
|
|
6779
7665
|
const page = sink.lastActivePage;
|
|
6780
7666
|
if (!page || isPageClosed(page)) return;
|
|
@@ -6799,6 +7685,9 @@ async function stashPageState(sink, closing) {
|
|
|
6799
7685
|
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
6800
7686
|
await sampleAria();
|
|
6801
7687
|
}
|
|
7688
|
+
if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
7689
|
+
await recordPageWindow(sink, page);
|
|
7690
|
+
}
|
|
6802
7691
|
}
|
|
6803
7692
|
async function maybeOpenPicker(sink, closing) {
|
|
6804
7693
|
if (sink.pickOffered) return;
|
|
@@ -7042,6 +7931,13 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
7042
7931
|
function instrumentPage(page) {
|
|
7043
7932
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
7044
7933
|
INSTRUMENTED_PAGES.add(page);
|
|
7934
|
+
const probeSink = currentSink;
|
|
7935
|
+
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
7936
|
+
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
7937
|
+
probeSink.probeInterception = interception;
|
|
7938
|
+
}).catch(() => {
|
|
7939
|
+
});
|
|
7940
|
+
}
|
|
7045
7941
|
const ctx = pageContext(page);
|
|
7046
7942
|
if (ctx) instrumentContext(ctx);
|
|
7047
7943
|
if (typeof page.close === "function") {
|
|
@@ -7077,6 +7973,14 @@ function instrumentPage(page) {
|
|
|
7077
7973
|
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
7078
7974
|
}
|
|
7079
7975
|
}
|
|
7976
|
+
if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
7977
|
+
page.on("framenavigated", (frame) => {
|
|
7978
|
+
const sink = currentSink;
|
|
7979
|
+
if (!sink || frame !== page.mainFrame()) return;
|
|
7980
|
+
void recordPageWindow(sink, page).catch(() => {
|
|
7981
|
+
});
|
|
7982
|
+
});
|
|
7983
|
+
}
|
|
7080
7984
|
page.on("console", (msg) => {
|
|
7081
7985
|
const sink = currentSink;
|
|
7082
7986
|
if (!sink) return;
|
|
@@ -7136,14 +8040,14 @@ function instrumentPage(page) {
|
|
|
7136
8040
|
const logHeader = headers["x-piwi-logs"];
|
|
7137
8041
|
if (logHeader) {
|
|
7138
8042
|
try {
|
|
7139
|
-
entry.serverLogs = JSON.parse((0,
|
|
8043
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
7140
8044
|
} catch {
|
|
7141
8045
|
}
|
|
7142
8046
|
}
|
|
7143
8047
|
const traceHeader = headers["x-piwi-trace"];
|
|
7144
8048
|
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
7145
8049
|
try {
|
|
7146
|
-
entry.serverTraces = JSON.parse((0,
|
|
8050
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
7147
8051
|
} catch {
|
|
7148
8052
|
}
|
|
7149
8053
|
}
|
|
@@ -7328,6 +8232,16 @@ async function flushSink(sink, testInfo) {
|
|
|
7328
8232
|
});
|
|
7329
8233
|
}
|
|
7330
8234
|
}
|
|
8235
|
+
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
8236
|
+
if (pageReadable) await recordPageWindow(sink, page);
|
|
8237
|
+
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
8238
|
+
if (windows.length > 0) {
|
|
8239
|
+
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
8240
|
+
contentType: "application/json",
|
|
8241
|
+
body: Buffer.from(JSON.stringify(windows))
|
|
8242
|
+
});
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
7331
8245
|
}
|
|
7332
8246
|
var piwiFixtures = {
|
|
7333
8247
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
@@ -7353,11 +8267,36 @@ var piwiFixtures = {
|
|
|
7353
8267
|
async ({}, use, testInfo) => {
|
|
7354
8268
|
const sink = createSink();
|
|
7355
8269
|
sink.testInfo = testInfo;
|
|
8270
|
+
if (isProbeMode())
|
|
8271
|
+
sink.probeItem = probeItemForTest({
|
|
8272
|
+
title: testInfo.title,
|
|
8273
|
+
file: testInfo.file,
|
|
8274
|
+
titlePath: testInfo.titlePath
|
|
8275
|
+
});
|
|
7356
8276
|
currentSink = sink;
|
|
7357
8277
|
try {
|
|
7358
8278
|
await use();
|
|
7359
8279
|
} finally {
|
|
7360
8280
|
currentSink = null;
|
|
8281
|
+
if (sink.probeItem) {
|
|
8282
|
+
const applied = sink.probeInterception?.applied() ?? false;
|
|
8283
|
+
const level = sink.probeItem.level ?? "client";
|
|
8284
|
+
const handled = level === "server" ? classifyProbeHandled({
|
|
8285
|
+
consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
|
|
8286
|
+
dialogs: sink.dialogs.length,
|
|
8287
|
+
backendError: sink.probeInterception?.serverError() ?? false
|
|
8288
|
+
}) : "n/a";
|
|
8289
|
+
recordProbeOutcome({
|
|
8290
|
+
testCaseId: sink.probeItem.testCaseId,
|
|
8291
|
+
routeKey: sink.probeItem.routeKey,
|
|
8292
|
+
fault: sink.probeItem.fault,
|
|
8293
|
+
applied,
|
|
8294
|
+
outcome: outcomeFromStatus(testInfo.status, applied),
|
|
8295
|
+
level,
|
|
8296
|
+
dependency: sink.probeItem.dependency ?? null,
|
|
8297
|
+
handled
|
|
8298
|
+
});
|
|
8299
|
+
}
|
|
7361
8300
|
await flushSink(sink, testInfo);
|
|
7362
8301
|
}
|
|
7363
8302
|
},
|
|
@@ -7369,13 +8308,13 @@ function extendPiwiFixtures(test) {
|
|
|
7369
8308
|
}
|
|
7370
8309
|
|
|
7371
8310
|
// src/internal/ai/ai-fixtures.ts
|
|
7372
|
-
var
|
|
7373
|
-
var
|
|
8311
|
+
var fs18 = __toESM(require("fs"));
|
|
8312
|
+
var path22 = __toESM(require("path"));
|
|
7374
8313
|
var import_test = require("@playwright/test");
|
|
7375
8314
|
|
|
7376
8315
|
// src/internal/ai/artifact.ts
|
|
7377
|
-
var
|
|
7378
|
-
var
|
|
8316
|
+
var fs17 = __toESM(require("fs"));
|
|
8317
|
+
var path20 = __toESM(require("path"));
|
|
7379
8318
|
var ARTIFACT_VERSION = 1;
|
|
7380
8319
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
7381
8320
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -7468,7 +8407,7 @@ function parseEntry(text) {
|
|
|
7468
8407
|
function readEntry(file) {
|
|
7469
8408
|
let text;
|
|
7470
8409
|
try {
|
|
7471
|
-
text =
|
|
8410
|
+
text = fs17.readFileSync(file, "utf8");
|
|
7472
8411
|
} catch (error) {
|
|
7473
8412
|
if (error.code === "ENOENT") return null;
|
|
7474
8413
|
throw error;
|
|
@@ -7478,28 +8417,28 @@ function readEntry(file) {
|
|
|
7478
8417
|
function writeEntry(file, entry) {
|
|
7479
8418
|
const canonical = serializeEntry(entry);
|
|
7480
8419
|
return withEntryLock(file, () => {
|
|
7481
|
-
if (
|
|
7482
|
-
|
|
8420
|
+
if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
8421
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
7483
8422
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7484
|
-
|
|
7485
|
-
|
|
8423
|
+
fs17.writeFileSync(tmp, canonical);
|
|
8424
|
+
fs17.renameSync(tmp, file);
|
|
7486
8425
|
return { written: true };
|
|
7487
8426
|
});
|
|
7488
8427
|
}
|
|
7489
8428
|
function withEntryLock(file, fn) {
|
|
7490
8429
|
const lock = `${file}.lock`;
|
|
7491
|
-
|
|
8430
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
7492
8431
|
const deadline = Date.now() + 5e3;
|
|
7493
8432
|
for (; ; ) {
|
|
7494
8433
|
try {
|
|
7495
|
-
const fd =
|
|
7496
|
-
|
|
8434
|
+
const fd = fs17.openSync(lock, "wx");
|
|
8435
|
+
fs17.closeSync(fd);
|
|
7497
8436
|
break;
|
|
7498
8437
|
} catch (error) {
|
|
7499
8438
|
if (error.code !== "EEXIST") throw error;
|
|
7500
8439
|
if (Date.now() > deadline) {
|
|
7501
8440
|
try {
|
|
7502
|
-
|
|
8441
|
+
fs17.unlinkSync(lock);
|
|
7503
8442
|
} catch {
|
|
7504
8443
|
}
|
|
7505
8444
|
}
|
|
@@ -7509,7 +8448,7 @@ function withEntryLock(file, fn) {
|
|
|
7509
8448
|
return fn();
|
|
7510
8449
|
} finally {
|
|
7511
8450
|
try {
|
|
7512
|
-
|
|
8451
|
+
fs17.unlinkSync(lock);
|
|
7513
8452
|
} catch {
|
|
7514
8453
|
}
|
|
7515
8454
|
}
|
|
@@ -7517,7 +8456,7 @@ function withEntryLock(file, fn) {
|
|
|
7517
8456
|
|
|
7518
8457
|
// src/internal/ai/keys.ts
|
|
7519
8458
|
var crypto3 = __toESM(require("crypto"));
|
|
7520
|
-
var
|
|
8459
|
+
var path21 = __toESM(require("path"));
|
|
7521
8460
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
7522
8461
|
function normalizeTemplate(template) {
|
|
7523
8462
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -7531,13 +8470,13 @@ function slug(text, maxLength = 40) {
|
|
|
7531
8470
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
7532
8471
|
}
|
|
7533
8472
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
7534
|
-
return
|
|
8473
|
+
return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
|
|
7535
8474
|
}
|
|
7536
8475
|
function entryPath(params) {
|
|
7537
8476
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
7538
8477
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
7539
8478
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
7540
|
-
return
|
|
8479
|
+
return path21.join(entryDir(params.specFile, params.dir), name);
|
|
7541
8480
|
}
|
|
7542
8481
|
function parseLocation(location) {
|
|
7543
8482
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -7991,7 +8930,7 @@ function testIdentity(testInfo) {
|
|
|
7991
8930
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
7992
8931
|
}
|
|
7993
8932
|
function relativeToCwd(file) {
|
|
7994
|
-
return
|
|
8933
|
+
return path22.relative(process.cwd(), file).split(path22.sep).join("/");
|
|
7995
8934
|
}
|
|
7996
8935
|
function recordIntents(intents, entry) {
|
|
7997
8936
|
const add = (locator, kind) => {
|
|
@@ -8018,7 +8957,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
8018
8957
|
const readSource = () => {
|
|
8019
8958
|
if (source === null) {
|
|
8020
8959
|
try {
|
|
8021
|
-
source =
|
|
8960
|
+
source = fs18.readFileSync(testInfo.file, "utf8");
|
|
8022
8961
|
} catch {
|
|
8023
8962
|
source = "";
|
|
8024
8963
|
}
|