@piwitests/reporter 0.35.0 → 0.38.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 +281 -8
- package/dist/global-setup-module.js +168 -15
- package/dist/index.d.ts +33 -4
- package/dist/index.js +1085 -151
- 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;
|
|
@@ -1441,6 +1494,9 @@ var MetadataCollector = class {
|
|
|
1441
1494
|
testDir: p.testDir,
|
|
1442
1495
|
use: {
|
|
1443
1496
|
browserName: p.use?.browserName || p.name,
|
|
1497
|
+
// The origin of `baseURL` is what the graph treats as the run's own
|
|
1498
|
+
// surface, so only requests to it become route nodes.
|
|
1499
|
+
baseURL: p.use?.baseURL,
|
|
1444
1500
|
viewport: p.use?.viewport,
|
|
1445
1501
|
deviceScaleFactor: p.use?.deviceScaleFactor
|
|
1446
1502
|
}
|
|
@@ -1757,6 +1813,7 @@ var StreamManager = class {
|
|
|
1757
1813
|
startTime,
|
|
1758
1814
|
environment: this.options.environment || null,
|
|
1759
1815
|
label: this.options.label || null,
|
|
1816
|
+
keep: this.options.keep === true,
|
|
1760
1817
|
metadata,
|
|
1761
1818
|
instanceId,
|
|
1762
1819
|
playwrightVersion,
|
|
@@ -1779,6 +1836,7 @@ var StreamManager = class {
|
|
|
1779
1836
|
startTime,
|
|
1780
1837
|
environment: this.options.environment || null,
|
|
1781
1838
|
label: this.options.label || null,
|
|
1839
|
+
keep: this.options.keep === true,
|
|
1782
1840
|
metadata,
|
|
1783
1841
|
instanceId,
|
|
1784
1842
|
playwrightVersion,
|
|
@@ -2080,6 +2138,9 @@ function maskTokenLike(text) {
|
|
|
2080
2138
|
// ../core/src/step-analysis.ts
|
|
2081
2139
|
var MAX_STEP_PARAM_KEYS = 20;
|
|
2082
2140
|
var MAX_STEP_PARAM_VALUE_CHARS = 200;
|
|
2141
|
+
function capStepValue(value, max) {
|
|
2142
|
+
return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
|
|
2143
|
+
}
|
|
2083
2144
|
function categorizeStep(title, pwCategory, params) {
|
|
2084
2145
|
if (!title) return "other";
|
|
2085
2146
|
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
@@ -2110,11 +2171,11 @@ function normalizeStepParams(raw) {
|
|
|
2110
2171
|
out[key] = value;
|
|
2111
2172
|
count++;
|
|
2112
2173
|
} else if (typeof value === "string") {
|
|
2113
|
-
out[key] = maskTokenLike(value)
|
|
2174
|
+
out[key] = capStepValue(maskTokenLike(value), MAX_STEP_PARAM_VALUE_CHARS);
|
|
2114
2175
|
count++;
|
|
2115
2176
|
} else if (value != null) {
|
|
2116
2177
|
try {
|
|
2117
|
-
out[key] = maskTokenLike(JSON.stringify(value))
|
|
2178
|
+
out[key] = capStepValue(maskTokenLike(JSON.stringify(value)), MAX_STEP_PARAM_VALUE_CHARS);
|
|
2118
2179
|
count++;
|
|
2119
2180
|
} catch {
|
|
2120
2181
|
}
|
|
@@ -2437,8 +2498,8 @@ function isListMode(argv = process.argv) {
|
|
|
2437
2498
|
}
|
|
2438
2499
|
|
|
2439
2500
|
// src/public/global-setup.ts
|
|
2440
|
-
var
|
|
2441
|
-
var
|
|
2501
|
+
var path12 = __toESM(require("path"));
|
|
2502
|
+
var fs13 = __toESM(require("fs"));
|
|
2442
2503
|
|
|
2443
2504
|
// src/internal/support/aria-sampling.ts
|
|
2444
2505
|
var path10 = __toESM(require("path"));
|
|
@@ -2489,17 +2550,83 @@ function isDueForAriaSample(testInfo) {
|
|
|
2489
2550
|
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
2490
2551
|
}
|
|
2491
2552
|
|
|
2553
|
+
// src/internal/manifest/collect.ts
|
|
2554
|
+
var fs12 = __toESM(require("fs"));
|
|
2555
|
+
var path11 = __toESM(require("path"));
|
|
2556
|
+
var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
2557
|
+
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
2558
|
+
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
2559
|
+
function parseManifestJson(text) {
|
|
2560
|
+
let parsed;
|
|
2561
|
+
try {
|
|
2562
|
+
parsed = JSON.parse(text);
|
|
2563
|
+
} catch {
|
|
2564
|
+
return null;
|
|
2565
|
+
}
|
|
2566
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2567
|
+
const obj = parsed;
|
|
2568
|
+
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) => ({
|
|
2569
|
+
method: r.method,
|
|
2570
|
+
pattern: r.pattern,
|
|
2571
|
+
handler: typeof r.handler === "string" ? r.handler : void 0,
|
|
2572
|
+
responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
|
|
2573
|
+
})) : void 0;
|
|
2574
|
+
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;
|
|
2575
|
+
if (!routes && !pages) return null;
|
|
2576
|
+
return { routes, pages };
|
|
2577
|
+
}
|
|
2578
|
+
function hasInstrumentationHeader(headers) {
|
|
2579
|
+
const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
|
|
2580
|
+
return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
|
|
2581
|
+
}
|
|
2582
|
+
function readCommittedManifest(configDir) {
|
|
2583
|
+
const file = path11.join(configDir, COMMITTED_MANIFEST_FILE);
|
|
2584
|
+
try {
|
|
2585
|
+
if (!fs12.existsSync(file)) return null;
|
|
2586
|
+
return parseManifestJson(fs12.readFileSync(file, "utf8"));
|
|
2587
|
+
} catch {
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
function configDirFromConfig(config) {
|
|
2592
|
+
if (config?.configFile) return path11.dirname(config.configFile);
|
|
2593
|
+
if (config?.rootDir) return config.rootDir;
|
|
2594
|
+
return process.cwd();
|
|
2595
|
+
}
|
|
2596
|
+
function baseUrlFromConfig(config) {
|
|
2597
|
+
const top = config?.use?.baseURL;
|
|
2598
|
+
if (typeof top === "string" && top) return top;
|
|
2599
|
+
for (const project of config?.projects ?? []) {
|
|
2600
|
+
const url = project?.use?.baseURL;
|
|
2601
|
+
if (typeof url === "string" && url) return url;
|
|
2602
|
+
}
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
|
|
2606
|
+
async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
|
|
2607
|
+
try {
|
|
2608
|
+
const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
2609
|
+
if (!hasInstrumentationHeader(probe.headers)) return null;
|
|
2610
|
+
const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
|
|
2611
|
+
const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
|
|
2612
|
+
if (!res.ok) return null;
|
|
2613
|
+
return parseManifestJson(await res.text());
|
|
2614
|
+
} catch {
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2492
2619
|
// src/public/global-setup.ts
|
|
2493
2620
|
function createGlobalSetup(options, userSetup) {
|
|
2494
2621
|
return async function globalSetupFn(config) {
|
|
2495
|
-
const piwiReporterPath =
|
|
2622
|
+
const piwiReporterPath = path12.resolve(__dirname, "./index.js");
|
|
2496
2623
|
let inlineReporterOptions = {};
|
|
2497
2624
|
if (Array.isArray(config?.reporter)) {
|
|
2498
2625
|
for (const r of config.reporter) {
|
|
2499
2626
|
if (!Array.isArray(r) || typeof r[0] !== "string") continue;
|
|
2500
2627
|
const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
|
|
2501
2628
|
try {
|
|
2502
|
-
return
|
|
2629
|
+
return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2503
2630
|
} catch {
|
|
2504
2631
|
return false;
|
|
2505
2632
|
}
|
|
@@ -2531,7 +2658,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2531
2658
|
if (!Array.isArray(r) || typeof r[0] !== "string") return false;
|
|
2532
2659
|
if (r[0].toLowerCase().includes("piwi")) return true;
|
|
2533
2660
|
try {
|
|
2534
|
-
return
|
|
2661
|
+
return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2535
2662
|
} catch {
|
|
2536
2663
|
return false;
|
|
2537
2664
|
}
|
|
@@ -2555,6 +2682,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2555
2682
|
projectDescription: opts.projectDescription,
|
|
2556
2683
|
environment: opts.environment || null,
|
|
2557
2684
|
label: opts.label || null,
|
|
2685
|
+
keep: opts.keep === true,
|
|
2558
2686
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2559
2687
|
instanceId: computeInstanceId(opts.projectName, runLabel),
|
|
2560
2688
|
shardIndex,
|
|
@@ -2563,7 +2691,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2563
2691
|
auth
|
|
2564
2692
|
);
|
|
2565
2693
|
if (response?.runId && response?.setupToken) {
|
|
2566
|
-
|
|
2694
|
+
fs13.writeFileSync(
|
|
2567
2695
|
getSetupFilePath(opts.projectName),
|
|
2568
2696
|
JSON.stringify({
|
|
2569
2697
|
runId: response.runId,
|
|
@@ -2574,18 +2702,43 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2574
2702
|
logger.debug(`Global setup: initializing run #${response.runId}`);
|
|
2575
2703
|
}
|
|
2576
2704
|
if (opts.projectName) clearAriaSampleFile(opts.projectName);
|
|
2577
|
-
|
|
2705
|
+
const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
|
|
2706
|
+
const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
|
|
2707
|
+
const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
|
|
2708
|
+
const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
|
|
2709
|
+
let projectId;
|
|
2710
|
+
if ((wantsAria || wantsManifest) && opts.projectName) {
|
|
2578
2711
|
const menu = await httpClient.getJSON("/api/projects/menu", auth);
|
|
2579
|
-
|
|
2712
|
+
projectId = menu?.items?.find(
|
|
2580
2713
|
(p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
|
|
2581
2714
|
)?.id;
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2715
|
+
}
|
|
2716
|
+
if (wantsAria && projectId != null) {
|
|
2717
|
+
const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
|
|
2718
|
+
const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
|
|
2719
|
+
if (tests) {
|
|
2720
|
+
const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
|
|
2721
|
+
writeAriaSampleFile(opts.projectName, identities);
|
|
2722
|
+
logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
if (wantsManifest && projectId != null) {
|
|
2726
|
+
const branch = resolveScmBranch(process.env) ?? null;
|
|
2727
|
+
if (committedManifest) {
|
|
2728
|
+
await httpClient.putJSON(
|
|
2729
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
2730
|
+
{ source: "committed", branch, manifest: committedManifest },
|
|
2731
|
+
auth
|
|
2732
|
+
).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
|
|
2733
|
+
}
|
|
2734
|
+
if (manifestBaseUrl) {
|
|
2735
|
+
const declared = await fetchInstrumentationManifest(manifestBaseUrl);
|
|
2736
|
+
if (declared) {
|
|
2737
|
+
await httpClient.putJSON(
|
|
2738
|
+
`/api/projects/${projectId}/surface/manifest`,
|
|
2739
|
+
{ source: "instrumentation", branch, manifest: declared },
|
|
2740
|
+
auth
|
|
2741
|
+
).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
|
|
2589
2742
|
}
|
|
2590
2743
|
}
|
|
2591
2744
|
}
|
|
@@ -2597,8 +2750,110 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2597
2750
|
}
|
|
2598
2751
|
|
|
2599
2752
|
// src/public/config-wrapper.ts
|
|
2600
|
-
var
|
|
2601
|
-
var
|
|
2753
|
+
var path14 = __toESM(require("path"));
|
|
2754
|
+
var fs15 = __toESM(require("fs"));
|
|
2755
|
+
|
|
2756
|
+
// src/internal/probe/mode.ts
|
|
2757
|
+
var fs14 = __toESM(require("fs"));
|
|
2758
|
+
var path13 = __toESM(require("path"));
|
|
2759
|
+
|
|
2760
|
+
// src/internal/probe/plan.ts
|
|
2761
|
+
function probeRunMetadata(base = {}) {
|
|
2762
|
+
return { ...base, piwiProbe: true };
|
|
2763
|
+
}
|
|
2764
|
+
function parseRouteKey(routeKey) {
|
|
2765
|
+
const space = routeKey.indexOf(" ");
|
|
2766
|
+
if (space < 0) return { method: "", pattern: routeKey };
|
|
2767
|
+
return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
|
|
2768
|
+
}
|
|
2769
|
+
function patternToRegex(pattern) {
|
|
2770
|
+
const path23 = pattern.split("?")[0] ?? pattern;
|
|
2771
|
+
const escaped = path23.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2772
|
+
return new RegExp(`^${escaped}$`);
|
|
2773
|
+
}
|
|
2774
|
+
function suitePathEqual(a, b) {
|
|
2775
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
2776
|
+
return a.every((title, i) => title === b[i]);
|
|
2777
|
+
}
|
|
2778
|
+
function matchProbeItem(plan, test) {
|
|
2779
|
+
const byFileTitle = plan.items.filter(
|
|
2780
|
+
(item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
|
|
2781
|
+
);
|
|
2782
|
+
if (byFileTitle.length === 1) return byFileTitle[0];
|
|
2783
|
+
if (byFileTitle.length > 1) {
|
|
2784
|
+
return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
|
|
2785
|
+
}
|
|
2786
|
+
for (const item of plan.items) {
|
|
2787
|
+
if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
|
|
2788
|
+
}
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
function requestMatchesRoute(method, url, routeKey) {
|
|
2792
|
+
const { method: rm, pattern } = parseRouteKey(routeKey);
|
|
2793
|
+
if (rm && rm !== method.toUpperCase()) return false;
|
|
2794
|
+
let path23;
|
|
2795
|
+
try {
|
|
2796
|
+
path23 = new URL(url).pathname;
|
|
2797
|
+
} catch {
|
|
2798
|
+
path23 = url.split("?")[0] ?? url;
|
|
2799
|
+
}
|
|
2800
|
+
return patternToRegex(pattern).test(path23);
|
|
2801
|
+
}
|
|
2802
|
+
function createProbeState() {
|
|
2803
|
+
return { navigated: false, matches: 0 };
|
|
2804
|
+
}
|
|
2805
|
+
function markNavigated(state) {
|
|
2806
|
+
state.navigated = true;
|
|
2807
|
+
}
|
|
2808
|
+
function shouldMutate(state, item) {
|
|
2809
|
+
if (!state.navigated) return false;
|
|
2810
|
+
state.matches += 1;
|
|
2811
|
+
return state.matches === Math.max(1, item.nth ?? 1);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// src/internal/probe/mode.ts
|
|
2815
|
+
var planCache;
|
|
2816
|
+
function isProbeMode() {
|
|
2817
|
+
return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
|
|
2818
|
+
}
|
|
2819
|
+
function loadPlan() {
|
|
2820
|
+
if (planCache !== void 0) return planCache;
|
|
2821
|
+
planCache = null;
|
|
2822
|
+
const path23 = process.env[PIWI_PROBE_ENV.plan];
|
|
2823
|
+
if (path23) {
|
|
2824
|
+
try {
|
|
2825
|
+
planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
|
|
2826
|
+
} catch {
|
|
2827
|
+
planCache = null;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
return planCache;
|
|
2831
|
+
}
|
|
2832
|
+
function probeItemForTest(test) {
|
|
2833
|
+
const plan = loadPlan();
|
|
2834
|
+
if (!plan) return null;
|
|
2835
|
+
const filePath = test.file ? path13.relative(process.cwd(), test.file).split(path13.sep).join("/") : null;
|
|
2836
|
+
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
2837
|
+
return matchProbeItem(plan, { title: test.title, filePath, suitePath });
|
|
2838
|
+
}
|
|
2839
|
+
function classifyProbeHandled(signals) {
|
|
2840
|
+
return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
|
|
2841
|
+
}
|
|
2842
|
+
function outcomeFromStatus(status, applied) {
|
|
2843
|
+
if (!applied) return "inconclusive";
|
|
2844
|
+
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2845
|
+
}
|
|
2846
|
+
function recordProbeOutcome(line) {
|
|
2847
|
+
const path23 = process.env[PIWI_PROBE_ENV.results];
|
|
2848
|
+
if (!path23) return;
|
|
2849
|
+
try {
|
|
2850
|
+
fs14.appendFileSync(path23, `${JSON.stringify(line)}
|
|
2851
|
+
`);
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// src/public/config-wrapper.ts
|
|
2602
2857
|
var PIWI_MODULE = "@piwitests/reporter";
|
|
2603
2858
|
var CAPTURE_DEFAULTS = {
|
|
2604
2859
|
screenshot: "only-on-failure",
|
|
@@ -2663,11 +2918,11 @@ function injectReporter(reporter, piwiOptions) {
|
|
|
2663
2918
|
}
|
|
2664
2919
|
function resolveSetupModule() {
|
|
2665
2920
|
const candidates = [
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2921
|
+
path14.join(__dirname, "global-setup-module.js"),
|
|
2922
|
+
path14.join(__dirname, "..", "global-setup-module.js"),
|
|
2923
|
+
path14.join(__dirname, "..", "global-setup-module.ts")
|
|
2669
2924
|
];
|
|
2670
|
-
return candidates.find((candidate) =>
|
|
2925
|
+
return candidates.find((candidate) => fs15.existsSync(candidate)) ?? candidates[0];
|
|
2671
2926
|
}
|
|
2672
2927
|
function wrapConfig(config, piwiOptions) {
|
|
2673
2928
|
if (piwiOptions) applyOptionsToEnv(piwiOptions);
|
|
@@ -2679,6 +2934,7 @@ function wrapConfig(config, piwiOptions) {
|
|
|
2679
2934
|
const forwarded = {};
|
|
2680
2935
|
const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
|
|
2681
2936
|
if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
|
|
2937
|
+
if (isProbeMode()) forwarded.retries = 0;
|
|
2682
2938
|
const use = applyCaptureDefaults(config.use, piwiOptions);
|
|
2683
2939
|
return {
|
|
2684
2940
|
...config,
|
|
@@ -2867,7 +3123,7 @@ function collectTestMetadata(annotations) {
|
|
|
2867
3123
|
}
|
|
2868
3124
|
|
|
2869
3125
|
// src/internal/collect/error-text.ts
|
|
2870
|
-
var
|
|
3126
|
+
var path15 = __toESM(require("path"));
|
|
2871
3127
|
|
|
2872
3128
|
// ../core/src/error-text.ts
|
|
2873
3129
|
function joinErrorMessages(errors) {
|
|
@@ -2896,13 +3152,13 @@ function buildErrorText(result) {
|
|
|
2896
3152
|
if (!text) return null;
|
|
2897
3153
|
const loc = result.error?.location;
|
|
2898
3154
|
if (!loc?.file) return text;
|
|
2899
|
-
const rel =
|
|
3155
|
+
const rel = path15.relative(process.cwd(), loc.file).split(path15.sep).join("/");
|
|
2900
3156
|
return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
|
|
2901
3157
|
}
|
|
2902
3158
|
|
|
2903
3159
|
// src/internal/support/ci-output.ts
|
|
2904
|
-
var
|
|
2905
|
-
var
|
|
3160
|
+
var fs16 = __toESM(require("fs"));
|
|
3161
|
+
var path16 = __toESM(require("path"));
|
|
2906
3162
|
var SUMMARY_MAX_FAILURES = 20;
|
|
2907
3163
|
function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
2908
3164
|
logger.info(`View run: ${output.runUrl}`);
|
|
@@ -2912,9 +3168,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
|
2912
3168
|
}
|
|
2913
3169
|
function writeOutputFile(file, output, logger) {
|
|
2914
3170
|
try {
|
|
2915
|
-
const dir =
|
|
2916
|
-
if (dir && dir !== ".")
|
|
2917
|
-
|
|
3171
|
+
const dir = path16.dirname(file);
|
|
3172
|
+
if (dir && dir !== ".") fs16.mkdirSync(dir, { recursive: true });
|
|
3173
|
+
fs16.writeFileSync(
|
|
2918
3174
|
file,
|
|
2919
3175
|
JSON.stringify(
|
|
2920
3176
|
{
|
|
@@ -2994,7 +3250,7 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
2994
3250
|
if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
|
|
2995
3251
|
if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
|
|
2996
3252
|
try {
|
|
2997
|
-
|
|
3253
|
+
fs16.writeFileSync(file, lines.join("\n") + "\n");
|
|
2998
3254
|
logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
|
|
2999
3255
|
} catch (error) {
|
|
3000
3256
|
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
@@ -3002,7 +3258,7 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
3002
3258
|
}
|
|
3003
3259
|
function appendFileLines(file, lines, logger, label) {
|
|
3004
3260
|
try {
|
|
3005
|
-
|
|
3261
|
+
fs16.appendFileSync(file, lines.join("\n") + "\n");
|
|
3006
3262
|
} catch (error) {
|
|
3007
3263
|
logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
|
|
3008
3264
|
}
|
|
@@ -3121,6 +3377,7 @@ var RunSubmitter = class {
|
|
|
3121
3377
|
didNotRunTests: run.didNotRunTests,
|
|
3122
3378
|
environment: run.options.environment,
|
|
3123
3379
|
label: run.options.label || null,
|
|
3380
|
+
keep: run.options.keep === true,
|
|
3124
3381
|
metadata: run.metadata,
|
|
3125
3382
|
instanceId: run.instanceId,
|
|
3126
3383
|
playwrightVersion: run.playwrightVersion ?? void 0,
|
|
@@ -3151,6 +3408,7 @@ var RunSubmitter = class {
|
|
|
3151
3408
|
flakyTests,
|
|
3152
3409
|
durations,
|
|
3153
3410
|
label: run.options.label || null,
|
|
3411
|
+
keep: run.options.keep === true,
|
|
3154
3412
|
metadata: run.metadata,
|
|
3155
3413
|
hasPendingUploads: this.hasReports(run),
|
|
3156
3414
|
playwrightVersion: run.playwrightVersion ?? void 0,
|
|
@@ -3238,6 +3496,298 @@ var RunSubmitter = class {
|
|
|
3238
3496
|
}
|
|
3239
3497
|
};
|
|
3240
3498
|
|
|
3499
|
+
// ../core/src/locator-chain.ts
|
|
3500
|
+
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
3501
|
+
"getByRole",
|
|
3502
|
+
"getByTestId",
|
|
3503
|
+
"getByText",
|
|
3504
|
+
"getByLabel",
|
|
3505
|
+
"getByPlaceholder",
|
|
3506
|
+
"getByAltText",
|
|
3507
|
+
"getByTitle",
|
|
3508
|
+
"locator",
|
|
3509
|
+
"frameLocator"
|
|
3510
|
+
]);
|
|
3511
|
+
var NARROWING_METHODS = /* @__PURE__ */ new Set([
|
|
3512
|
+
"filter",
|
|
3513
|
+
"first",
|
|
3514
|
+
"last",
|
|
3515
|
+
"nth",
|
|
3516
|
+
"and",
|
|
3517
|
+
"or",
|
|
3518
|
+
"visible",
|
|
3519
|
+
"contentFrame",
|
|
3520
|
+
"owner"
|
|
3521
|
+
]);
|
|
3522
|
+
var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
|
|
3523
|
+
var LocatorParseError = class extends Error {
|
|
3524
|
+
constructor(message) {
|
|
3525
|
+
super(message);
|
|
3526
|
+
this.name = "LocatorParseError";
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
var SIMPLE_ESCAPES = {
|
|
3530
|
+
n: "\n",
|
|
3531
|
+
t: " ",
|
|
3532
|
+
r: "\r",
|
|
3533
|
+
b: "\b",
|
|
3534
|
+
f: "\f",
|
|
3535
|
+
v: "\v",
|
|
3536
|
+
"0": "\0"
|
|
3537
|
+
};
|
|
3538
|
+
var Parser = class {
|
|
3539
|
+
constructor(src) {
|
|
3540
|
+
this.src = src;
|
|
3541
|
+
this.i = 0;
|
|
3542
|
+
}
|
|
3543
|
+
parseTopLevel() {
|
|
3544
|
+
const chain = this.parseChain();
|
|
3545
|
+
this.skipSpace();
|
|
3546
|
+
if (this.i < this.src.length) this.fail("unexpected text");
|
|
3547
|
+
return chain;
|
|
3548
|
+
}
|
|
3549
|
+
fail(message) {
|
|
3550
|
+
throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
|
|
3551
|
+
}
|
|
3552
|
+
skipSpace() {
|
|
3553
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
3554
|
+
}
|
|
3555
|
+
peek() {
|
|
3556
|
+
return this.src[this.i];
|
|
3557
|
+
}
|
|
3558
|
+
expect(ch) {
|
|
3559
|
+
this.skipSpace();
|
|
3560
|
+
if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
|
|
3561
|
+
this.i++;
|
|
3562
|
+
}
|
|
3563
|
+
identifier() {
|
|
3564
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
|
|
3565
|
+
if (!m) this.fail("expected a name");
|
|
3566
|
+
this.i += m[0].length;
|
|
3567
|
+
return m[0];
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* A chain of calls joined by `.`. With `lenient`, parsing stops before the
|
|
3571
|
+
* first link that doesn't parse instead of failing, so a chain followed by
|
|
3572
|
+
* other text (an error message) is read up to where it ends. `spans` receives
|
|
3573
|
+
* where each top-level call starts and ends.
|
|
3574
|
+
*/
|
|
3575
|
+
parseChain(opts = {}) {
|
|
3576
|
+
this.skipSpace();
|
|
3577
|
+
const calls = [];
|
|
3578
|
+
for (; ; ) {
|
|
3579
|
+
const before = this.i;
|
|
3580
|
+
if (calls.length > 0) this.i++;
|
|
3581
|
+
this.skipSpace();
|
|
3582
|
+
const start = this.i;
|
|
3583
|
+
try {
|
|
3584
|
+
calls.push(this.parseCall(calls.length === 0));
|
|
3585
|
+
} catch (error) {
|
|
3586
|
+
if (!opts.lenient || calls.length === 0) throw error;
|
|
3587
|
+
this.i = before;
|
|
3588
|
+
break;
|
|
3589
|
+
}
|
|
3590
|
+
opts.spans?.push({ start, end: this.i });
|
|
3591
|
+
const end = this.i;
|
|
3592
|
+
this.skipSpace();
|
|
3593
|
+
if (this.peek() !== ".") {
|
|
3594
|
+
this.i = end;
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return { calls };
|
|
3599
|
+
}
|
|
3600
|
+
/** Where the parser stopped. */
|
|
3601
|
+
get position() {
|
|
3602
|
+
return this.i;
|
|
3603
|
+
}
|
|
3604
|
+
parseCall(first) {
|
|
3605
|
+
this.skipSpace();
|
|
3606
|
+
const method = this.identifier();
|
|
3607
|
+
if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
|
|
3608
|
+
if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
|
|
3609
|
+
this.expect("(");
|
|
3610
|
+
const args = [];
|
|
3611
|
+
this.skipSpace();
|
|
3612
|
+
if (this.peek() === ")") {
|
|
3613
|
+
this.i++;
|
|
3614
|
+
return { method, args };
|
|
3615
|
+
}
|
|
3616
|
+
for (; ; ) {
|
|
3617
|
+
args.push(this.parseValue());
|
|
3618
|
+
this.skipSpace();
|
|
3619
|
+
if (this.peek() === ",") {
|
|
3620
|
+
this.i++;
|
|
3621
|
+
this.skipSpace();
|
|
3622
|
+
if (this.peek() === ")") {
|
|
3623
|
+
this.i++;
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
this.expect(")");
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
return { method, args };
|
|
3632
|
+
}
|
|
3633
|
+
parseValue() {
|
|
3634
|
+
this.skipSpace();
|
|
3635
|
+
const ch = this.peek();
|
|
3636
|
+
if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
|
|
3637
|
+
if (ch === "/") return this.parseRegex();
|
|
3638
|
+
if (ch === "{") return this.parseObject();
|
|
3639
|
+
if (ch !== void 0 && /[-\d]/.test(ch)) {
|
|
3640
|
+
const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
|
|
3641
|
+
if (!m) this.fail("expected a number");
|
|
3642
|
+
this.i += m[0].length;
|
|
3643
|
+
return { type: "number", value: Number(m[0]) };
|
|
3644
|
+
}
|
|
3645
|
+
const rest = this.src.slice(this.i);
|
|
3646
|
+
if (/^true\b/.test(rest)) {
|
|
3647
|
+
this.i += 4;
|
|
3648
|
+
return { type: "boolean", value: true };
|
|
3649
|
+
}
|
|
3650
|
+
if (/^false\b/.test(rest)) {
|
|
3651
|
+
this.i += 5;
|
|
3652
|
+
return { type: "boolean", value: false };
|
|
3653
|
+
}
|
|
3654
|
+
if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
|
|
3655
|
+
return this.fail("expected a value");
|
|
3656
|
+
}
|
|
3657
|
+
/** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
|
|
3658
|
+
parseString() {
|
|
3659
|
+
const quote = this.src[this.i];
|
|
3660
|
+
let out = "";
|
|
3661
|
+
this.i++;
|
|
3662
|
+
while (this.i < this.src.length) {
|
|
3663
|
+
const c = this.src[this.i];
|
|
3664
|
+
if (c === quote) {
|
|
3665
|
+
this.i++;
|
|
3666
|
+
return out;
|
|
3667
|
+
}
|
|
3668
|
+
if (c !== "\\") {
|
|
3669
|
+
out += c;
|
|
3670
|
+
this.i++;
|
|
3671
|
+
continue;
|
|
3672
|
+
}
|
|
3673
|
+
const next = this.src[this.i + 1];
|
|
3674
|
+
if (next === void 0) break;
|
|
3675
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
3676
|
+
if (simple !== void 0) {
|
|
3677
|
+
out += simple;
|
|
3678
|
+
this.i += 2;
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
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;
|
|
3682
|
+
if (hex) {
|
|
3683
|
+
out += String.fromCodePoint(parseInt(hex[1], 16));
|
|
3684
|
+
this.i += 1 + hex[0].length;
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
if (next === "u" || next === "x") this.fail("malformed escape");
|
|
3688
|
+
out += next;
|
|
3689
|
+
this.i += 2;
|
|
3690
|
+
}
|
|
3691
|
+
return this.fail("unterminated string");
|
|
3692
|
+
}
|
|
3693
|
+
parseRegex() {
|
|
3694
|
+
let j = this.i + 1;
|
|
3695
|
+
let inClass = false;
|
|
3696
|
+
while (j < this.src.length) {
|
|
3697
|
+
const c = this.src[j];
|
|
3698
|
+
if (c === "\\") {
|
|
3699
|
+
j += 2;
|
|
3700
|
+
continue;
|
|
3701
|
+
}
|
|
3702
|
+
if (c === "[") inClass = true;
|
|
3703
|
+
else if (c === "]") inClass = false;
|
|
3704
|
+
else if (c === "/" && !inClass) break;
|
|
3705
|
+
j++;
|
|
3706
|
+
}
|
|
3707
|
+
if (j >= this.src.length) this.fail("unterminated regex");
|
|
3708
|
+
const source = this.src.slice(this.i + 1, j);
|
|
3709
|
+
const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
|
|
3710
|
+
this.i = j + 1 + flags.length;
|
|
3711
|
+
return { type: "regex", source, flags };
|
|
3712
|
+
}
|
|
3713
|
+
parseObject() {
|
|
3714
|
+
this.expect("{");
|
|
3715
|
+
const entries = [];
|
|
3716
|
+
for (; ; ) {
|
|
3717
|
+
this.skipSpace();
|
|
3718
|
+
if (this.peek() === "}") {
|
|
3719
|
+
this.i++;
|
|
3720
|
+
break;
|
|
3721
|
+
}
|
|
3722
|
+
const ch = this.peek();
|
|
3723
|
+
const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
|
|
3724
|
+
this.expect(":");
|
|
3725
|
+
entries.push([key, this.parseValue()]);
|
|
3726
|
+
this.skipSpace();
|
|
3727
|
+
if (this.peek() === ",") {
|
|
3728
|
+
this.i++;
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
this.expect("}");
|
|
3732
|
+
break;
|
|
3733
|
+
}
|
|
3734
|
+
return { type: "object", entries };
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
function parseLocatorChain(expr) {
|
|
3738
|
+
const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
|
|
3739
|
+
if (!trimmed) throw new LocatorParseError("empty expression");
|
|
3740
|
+
return new Parser(trimmed).parseTopLevel();
|
|
3741
|
+
}
|
|
3742
|
+
function scanLocatorChain(text) {
|
|
3743
|
+
const parser = new Parser(text);
|
|
3744
|
+
const spans = [];
|
|
3745
|
+
try {
|
|
3746
|
+
const chain = parser.parseChain({ lenient: true, spans });
|
|
3747
|
+
return { chain, spans, end: parser.position };
|
|
3748
|
+
} catch {
|
|
3749
|
+
return null;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
function tryParseLocatorChain(expr) {
|
|
3753
|
+
try {
|
|
3754
|
+
return parseLocatorChain(expr);
|
|
3755
|
+
} catch {
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function parseLeafLocatorCall(expr) {
|
|
3760
|
+
const chain = tryParseLocatorChain(expr);
|
|
3761
|
+
if (!chain) return null;
|
|
3762
|
+
for (let i = chain.calls.length - 1; i >= 0; i--) {
|
|
3763
|
+
if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
|
|
3764
|
+
}
|
|
3765
|
+
return null;
|
|
3766
|
+
}
|
|
3767
|
+
function locatorCallValues(call, opts = {}) {
|
|
3768
|
+
const value = (arg) => {
|
|
3769
|
+
switch (arg.type) {
|
|
3770
|
+
case "string":
|
|
3771
|
+
case "number":
|
|
3772
|
+
case "boolean":
|
|
3773
|
+
return arg.value;
|
|
3774
|
+
case "regex":
|
|
3775
|
+
return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
|
|
3776
|
+
case "object": {
|
|
3777
|
+
const out = {};
|
|
3778
|
+
for (const [key, v] of arg.entries) {
|
|
3779
|
+
const plain = value(v);
|
|
3780
|
+
if (plain !== void 0) out[key] = plain;
|
|
3781
|
+
}
|
|
3782
|
+
return out;
|
|
3783
|
+
}
|
|
3784
|
+
case "chain":
|
|
3785
|
+
return void 0;
|
|
3786
|
+
}
|
|
3787
|
+
};
|
|
3788
|
+
return call.args.map(value).filter((v) => v !== void 0);
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3241
3791
|
// ../core/src/locator-methods.ts
|
|
3242
3792
|
var LOCATOR_BUILDER_METHODS = [
|
|
3243
3793
|
"getByRole",
|
|
@@ -3282,10 +3832,35 @@ function extractLeafSelector(text) {
|
|
|
3282
3832
|
if (!first) return null;
|
|
3283
3833
|
const nl = text.indexOf("\n", first.index);
|
|
3284
3834
|
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3835
|
+
const scanned = readChain(region);
|
|
3836
|
+
if (scanned) {
|
|
3837
|
+
for (let k = scanned.chain.calls.length - 1; k >= 0; k--) {
|
|
3838
|
+
if (LEAF_METHODS.has(scanned.chain.calls[k].method)) {
|
|
3839
|
+
return region.slice(scanned.spans[k].start, scanned.spans[k].end);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return null;
|
|
3843
|
+
}
|
|
3844
|
+
return lenientLeaf(region);
|
|
3845
|
+
}
|
|
3846
|
+
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
3847
|
+
function skipQuoted(text, i) {
|
|
3848
|
+
const quote = text[i];
|
|
3849
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
3850
|
+
if (text[j] === "\\") j++;
|
|
3851
|
+
else if (text[j] === quote) return j + 1;
|
|
3852
|
+
}
|
|
3853
|
+
return text.length;
|
|
3854
|
+
}
|
|
3855
|
+
function lenientLeaf(region) {
|
|
3285
3856
|
let depth = 0;
|
|
3286
3857
|
let leafStart = -1;
|
|
3287
3858
|
for (let i = 0; i < region.length; i++) {
|
|
3288
3859
|
const ch = region[i];
|
|
3860
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3861
|
+
i = skipQuoted(region, i) - 1;
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3289
3864
|
if (ch === "(") {
|
|
3290
3865
|
depth++;
|
|
3291
3866
|
continue;
|
|
@@ -3308,7 +3883,9 @@ function extractLeafSelector(text) {
|
|
|
3308
3883
|
depth = 0;
|
|
3309
3884
|
for (let i = leafStart; i < region.length; i++) {
|
|
3310
3885
|
const ch = region[i];
|
|
3311
|
-
if (ch === "
|
|
3886
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3887
|
+
i = skipQuoted(region, i) - 1;
|
|
3888
|
+
} else if (ch === "(") {
|
|
3312
3889
|
depth++;
|
|
3313
3890
|
} else if (ch === ")") {
|
|
3314
3891
|
depth--;
|
|
@@ -3322,11 +3899,15 @@ function extractLocatorChain(text) {
|
|
|
3322
3899
|
if (!first) return null;
|
|
3323
3900
|
const nl = text.indexOf("\n", first.index);
|
|
3324
3901
|
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3902
|
+
const scanned = readChain(region);
|
|
3903
|
+
if (scanned) return region.slice(0, scanned.end);
|
|
3325
3904
|
let depth = 0;
|
|
3326
3905
|
let end = -1;
|
|
3327
3906
|
for (let i = 0; i < region.length; i++) {
|
|
3328
3907
|
const ch = region[i];
|
|
3329
|
-
if (ch === "
|
|
3908
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3909
|
+
i = skipQuoted(region, i) - 1;
|
|
3910
|
+
} else if (ch === "(") {
|
|
3330
3911
|
depth++;
|
|
3331
3912
|
} else if (ch === ")") {
|
|
3332
3913
|
if (depth > 0) depth--;
|
|
@@ -3341,6 +3922,12 @@ function extractLocatorChain(text) {
|
|
|
3341
3922
|
if (end === -1) return region.slice(0, 80);
|
|
3342
3923
|
return region.slice(0, end);
|
|
3343
3924
|
}
|
|
3925
|
+
function readChain(region) {
|
|
3926
|
+
const scanned = scanLocatorChain(region);
|
|
3927
|
+
if (!scanned) return null;
|
|
3928
|
+
const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
|
|
3929
|
+
return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
|
|
3930
|
+
}
|
|
3344
3931
|
function extractTopFrame(text) {
|
|
3345
3932
|
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
3346
3933
|
let m;
|
|
@@ -4064,7 +4651,7 @@ function testLocation(test) {
|
|
|
4064
4651
|
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
4065
4652
|
}
|
|
4066
4653
|
function testFile(test) {
|
|
4067
|
-
return
|
|
4654
|
+
return path17.relative(process.cwd(), test.location.file).split(path17.sep).join("/");
|
|
4068
4655
|
}
|
|
4069
4656
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
4070
4657
|
constructor(rawOptions = {}) {
|
|
@@ -4181,6 +4768,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4181
4768
|
);
|
|
4182
4769
|
}
|
|
4183
4770
|
this.metadata = this.metadataCollector.collect(config, suite, this.options);
|
|
4771
|
+
if (isProbeMode()) this.metadata = probeRunMetadata(this.metadata);
|
|
4184
4772
|
this.plannedTests = suite.allTests();
|
|
4185
4773
|
const pwShard = config.shard;
|
|
4186
4774
|
if (pwShard?.total && pwShard.total > 1) {
|
|
@@ -4219,18 +4807,27 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4219
4807
|
}
|
|
4220
4808
|
}
|
|
4221
4809
|
static {
|
|
4222
|
-
/**
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4810
|
+
/** Step categories streamed live while the run executes. */
|
|
4811
|
+
this.LIVE_STEP_CATEGORIES = /* @__PURE__ */ new Set(["hook", "fixture", "pw:api", "expect"]);
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* Whether a step streams live: its category is in `LIVE_STEP_CATEGORIES`
|
|
4815
|
+
* and no ancestor is an `expect` step. The steps inside an assertion are its
|
|
4816
|
+
* polling — every `expect.poll` attempt and `toPass` retry — not a step a
|
|
4817
|
+
* human watches; the meaningful readout is the assertion around them.
|
|
4818
|
+
*/
|
|
4819
|
+
static isLiveStep(step) {
|
|
4820
|
+
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(step.category)) return false;
|
|
4821
|
+
for (let parent = step.parent; parent; parent = parent.parent) {
|
|
4822
|
+
if (parent.category === "expect") return false;
|
|
4823
|
+
}
|
|
4824
|
+
return true;
|
|
4228
4825
|
}
|
|
4229
4826
|
/** Playwright reporter hook: called when a step (including hook/fixture) begins */
|
|
4230
4827
|
onStepBegin(test, _result, step) {
|
|
4231
4828
|
if (!this.enabled || !this.streamManager) return;
|
|
4829
|
+
if (!_PiwiDashboardReporter.isLiveStep(step)) return;
|
|
4232
4830
|
const cat = step.category;
|
|
4233
|
-
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
|
|
4234
4831
|
const event = {
|
|
4235
4832
|
type: "step-begin",
|
|
4236
4833
|
title: step.title,
|
|
@@ -4246,8 +4843,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4246
4843
|
/** Playwright reporter hook: called when a step (including hook/fixture) ends */
|
|
4247
4844
|
onStepEnd(test, _result, step) {
|
|
4248
4845
|
if (!this.enabled || !this.streamManager) return;
|
|
4846
|
+
if (!_PiwiDashboardReporter.isLiveStep(step)) return;
|
|
4249
4847
|
const cat = step.category;
|
|
4250
|
-
if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
|
|
4251
4848
|
const workerIndex = workerIndexOf(_result);
|
|
4252
4849
|
const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
|
|
4253
4850
|
const event = {
|
|
@@ -4537,7 +5134,7 @@ async function resolveSelection(options = {}) {
|
|
|
4537
5134
|
}
|
|
4538
5135
|
|
|
4539
5136
|
// src/internal/capture/capture-fixtures.ts
|
|
4540
|
-
var
|
|
5137
|
+
var import_node_zlib2 = require("zlib");
|
|
4541
5138
|
|
|
4542
5139
|
// ../picker-dom/src/probe.ts
|
|
4543
5140
|
function probeElementAttrs(el, arg) {
|
|
@@ -5895,7 +6492,7 @@ function mergeCandidates(base, extra) {
|
|
|
5895
6492
|
}
|
|
5896
6493
|
|
|
5897
6494
|
// src/internal/capture/locator-healing.ts
|
|
5898
|
-
var
|
|
6495
|
+
var path18 = __toESM(require("path"));
|
|
5899
6496
|
|
|
5900
6497
|
// ../core/src/locator-fingerprint.ts
|
|
5901
6498
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -6265,16 +6862,318 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
6265
6862
|
}
|
|
6266
6863
|
let rel = file;
|
|
6267
6864
|
try {
|
|
6268
|
-
rel =
|
|
6865
|
+
rel = path18.relative(process.cwd(), file);
|
|
6269
6866
|
} catch {
|
|
6270
6867
|
}
|
|
6271
|
-
rel = rel.split(
|
|
6868
|
+
rel = rel.split(path18.sep).join("/");
|
|
6272
6869
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
6273
6870
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
6274
6871
|
}
|
|
6275
6872
|
return null;
|
|
6276
6873
|
}
|
|
6277
6874
|
|
|
6875
|
+
// src/internal/capture/page-inventory.ts
|
|
6876
|
+
var PAGE_INVENTORY_MAX_ENTRIES = 500;
|
|
6877
|
+
function collectPageInventoryInPage(maxEntries) {
|
|
6878
|
+
const g = globalThis;
|
|
6879
|
+
const doc = g.document;
|
|
6880
|
+
if (!doc || !g.location) return null;
|
|
6881
|
+
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
6882
|
+
const roleOf = (el) => {
|
|
6883
|
+
const explicit = el.getAttribute("role");
|
|
6884
|
+
if (explicit) return explicit.toLowerCase();
|
|
6885
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
6886
|
+
if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
|
|
6887
|
+
if (tag === "button") return "button";
|
|
6888
|
+
if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
6889
|
+
if (tag === "textarea") return "textbox";
|
|
6890
|
+
if (tag === "input") {
|
|
6891
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
6892
|
+
const map = {
|
|
6893
|
+
checkbox: "checkbox",
|
|
6894
|
+
radio: "radio",
|
|
6895
|
+
button: "button",
|
|
6896
|
+
submit: "button",
|
|
6897
|
+
reset: "button",
|
|
6898
|
+
image: "button",
|
|
6899
|
+
range: "slider",
|
|
6900
|
+
search: "searchbox",
|
|
6901
|
+
email: "textbox",
|
|
6902
|
+
tel: "textbox",
|
|
6903
|
+
url: "textbox",
|
|
6904
|
+
number: "spinbutton"
|
|
6905
|
+
};
|
|
6906
|
+
return map[type] || "textbox";
|
|
6907
|
+
}
|
|
6908
|
+
return "generic";
|
|
6909
|
+
};
|
|
6910
|
+
const holdsValue = (el, role) => {
|
|
6911
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
6912
|
+
if (tag === "select" || tag === "textarea" || tag === "input") return true;
|
|
6913
|
+
if (el.isContentEditable === true) return true;
|
|
6914
|
+
const editable = el.getAttribute("contenteditable");
|
|
6915
|
+
if (editable === "" || editable === "true") return true;
|
|
6916
|
+
return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
|
|
6917
|
+
};
|
|
6918
|
+
const labelText = (el) => {
|
|
6919
|
+
const labels = el.labels;
|
|
6920
|
+
if (!labels || labels.length === 0) return "";
|
|
6921
|
+
let text = "";
|
|
6922
|
+
for (const label of labels) text += ` ${label.textContent || ""}`;
|
|
6923
|
+
return text;
|
|
6924
|
+
};
|
|
6925
|
+
const labelledByText = (el) => {
|
|
6926
|
+
const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
6927
|
+
if (ids.length === 0) return "";
|
|
6928
|
+
let text = "";
|
|
6929
|
+
for (const id of ids) {
|
|
6930
|
+
const ref = doc.getElementById(id);
|
|
6931
|
+
if (ref) text += ` ${ref.textContent || ""}`;
|
|
6932
|
+
}
|
|
6933
|
+
return text;
|
|
6934
|
+
};
|
|
6935
|
+
const nameOf = (el) => {
|
|
6936
|
+
const role = roleOf(el);
|
|
6937
|
+
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
6938
|
+
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
6939
|
+
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
6940
|
+
return clean(chain.find((s) => s && String(s).trim()) || "");
|
|
6941
|
+
};
|
|
6942
|
+
const cleanHref = (href) => {
|
|
6943
|
+
const cut = href.search(/[?#]/);
|
|
6944
|
+
return cut >= 0 ? href.slice(0, cut) : href;
|
|
6945
|
+
};
|
|
6946
|
+
const controls = [];
|
|
6947
|
+
const links = [];
|
|
6948
|
+
const seenC = /* @__PURE__ */ new Set();
|
|
6949
|
+
const seenL = /* @__PURE__ */ new Set();
|
|
6950
|
+
let budget = maxEntries;
|
|
6951
|
+
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"]';
|
|
6952
|
+
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
6953
|
+
if (budget <= 0) break;
|
|
6954
|
+
const role = roleOf(el);
|
|
6955
|
+
const name = nameOf(el);
|
|
6956
|
+
if (!name) continue;
|
|
6957
|
+
const key = `${role}\0${name}`;
|
|
6958
|
+
if (seenC.has(key)) continue;
|
|
6959
|
+
seenC.add(key);
|
|
6960
|
+
controls.push({ role, name });
|
|
6961
|
+
budget--;
|
|
6962
|
+
}
|
|
6963
|
+
for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
|
|
6964
|
+
if (budget <= 0) break;
|
|
6965
|
+
const name = nameOf(el);
|
|
6966
|
+
const href = cleanHref(el.getAttribute("href") || "");
|
|
6967
|
+
if (!name) continue;
|
|
6968
|
+
const key = `${name}\0${href}`;
|
|
6969
|
+
if (seenL.has(key)) continue;
|
|
6970
|
+
seenL.add(key);
|
|
6971
|
+
links.push({ name, href });
|
|
6972
|
+
budget--;
|
|
6973
|
+
}
|
|
6974
|
+
return { url: g.location.href, controls, links };
|
|
6975
|
+
}
|
|
6976
|
+
function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
|
|
6977
|
+
const controls = inventory.controls.slice(0, Math.max(0, max));
|
|
6978
|
+
const links = inventory.links.slice(0, Math.max(0, max - controls.length));
|
|
6979
|
+
return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
|
|
6980
|
+
}
|
|
6981
|
+
function inventoryPageKey(url) {
|
|
6982
|
+
try {
|
|
6983
|
+
const u = new URL(url);
|
|
6984
|
+
return `${u.origin}${u.pathname}`;
|
|
6985
|
+
} catch {
|
|
6986
|
+
return null;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6990
|
+
// src/internal/probe/interception.ts
|
|
6991
|
+
var import_node_zlib = require("zlib");
|
|
6992
|
+
|
|
6993
|
+
// src/internal/probe/faults.ts
|
|
6994
|
+
var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
|
|
6995
|
+
var SERVER_PROBE_FAULTS = [
|
|
6996
|
+
"throw",
|
|
6997
|
+
"status",
|
|
6998
|
+
"delay",
|
|
6999
|
+
"dependency",
|
|
7000
|
+
"data",
|
|
7001
|
+
"extreme",
|
|
7002
|
+
"replay",
|
|
7003
|
+
"auth",
|
|
7004
|
+
"slow-first"
|
|
7005
|
+
];
|
|
7006
|
+
function isFaultAllowed(level, fault) {
|
|
7007
|
+
const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
|
|
7008
|
+
return list.includes(fault);
|
|
7009
|
+
}
|
|
7010
|
+
var SLOW_FAULT_DELAY_MS = 5e3;
|
|
7011
|
+
function dropOneField(body) {
|
|
7012
|
+
let parsed;
|
|
7013
|
+
try {
|
|
7014
|
+
parsed = JSON.parse(body);
|
|
7015
|
+
} catch {
|
|
7016
|
+
return body;
|
|
7017
|
+
}
|
|
7018
|
+
if (Array.isArray(parsed)) {
|
|
7019
|
+
let key;
|
|
7020
|
+
for (const el of parsed) {
|
|
7021
|
+
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
7022
|
+
key ??= Object.keys(el)[0];
|
|
7023
|
+
if (key) delete el[key];
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
return JSON.stringify(parsed);
|
|
7027
|
+
}
|
|
7028
|
+
if (parsed && typeof parsed === "object") {
|
|
7029
|
+
const obj = parsed;
|
|
7030
|
+
const key = Object.keys(obj)[0];
|
|
7031
|
+
if (key) delete obj[key];
|
|
7032
|
+
return JSON.stringify(obj);
|
|
7033
|
+
}
|
|
7034
|
+
return body;
|
|
7035
|
+
}
|
|
7036
|
+
function computeFault(fault, input) {
|
|
7037
|
+
const base = {
|
|
7038
|
+
status: input.status,
|
|
7039
|
+
body: input.body,
|
|
7040
|
+
contentType: input.contentType ?? null,
|
|
7041
|
+
delayMs: 0,
|
|
7042
|
+
changed: false
|
|
7043
|
+
};
|
|
7044
|
+
switch (fault) {
|
|
7045
|
+
case "status-500":
|
|
7046
|
+
return { ...base, status: 500, changed: input.status !== 500 };
|
|
7047
|
+
case "empty-body":
|
|
7048
|
+
return { ...base, body: "", changed: input.body.length > 0 };
|
|
7049
|
+
case "drop-field": {
|
|
7050
|
+
const body = dropOneField(input.body);
|
|
7051
|
+
return { ...base, body, changed: body !== input.body };
|
|
7052
|
+
}
|
|
7053
|
+
case "stale-value": {
|
|
7054
|
+
const body = input.priorBody ?? input.body;
|
|
7055
|
+
return { ...base, body, changed: body !== input.body };
|
|
7056
|
+
}
|
|
7057
|
+
case "slow":
|
|
7058
|
+
return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
|
|
7059
|
+
default:
|
|
7060
|
+
return base;
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
|
|
7064
|
+
// src/internal/probe/sign.ts
|
|
7065
|
+
var import_node_crypto = require("crypto");
|
|
7066
|
+
function signProbeMessage(secret, nonce, ts, specJson) {
|
|
7067
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
|
|
7068
|
+
}
|
|
7069
|
+
function buildProbeHeader(secret, spec, now = Date.now()) {
|
|
7070
|
+
const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
7071
|
+
const specJson = JSON.stringify(spec);
|
|
7072
|
+
const sig = signProbeMessage(secret, nonce, now, specJson);
|
|
7073
|
+
return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
|
|
7074
|
+
}
|
|
7075
|
+
|
|
7076
|
+
// src/internal/probe/interception.ts
|
|
7077
|
+
function parseProbeTrace(traceHeader) {
|
|
7078
|
+
const none = { appliedFault: null, serverError: false };
|
|
7079
|
+
if (!traceHeader) return none;
|
|
7080
|
+
try {
|
|
7081
|
+
const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
|
|
7082
|
+
if (!Array.isArray(spans)) return none;
|
|
7083
|
+
const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
|
|
7084
|
+
if (!root) return none;
|
|
7085
|
+
const attrs = root.attrs ?? {};
|
|
7086
|
+
const appliedRaw = attrs["piwi.probe.applied"];
|
|
7087
|
+
const status = Number(attrs["http.status_code"]) || 0;
|
|
7088
|
+
return {
|
|
7089
|
+
appliedFault: appliedRaw != null ? String(appliedRaw) : null,
|
|
7090
|
+
serverError: root.status === "error" || status >= 500
|
|
7091
|
+
};
|
|
7092
|
+
} catch {
|
|
7093
|
+
return none;
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
function fulfillHeaders(responseHeaders, contentType) {
|
|
7097
|
+
const headers = { ...responseHeaders };
|
|
7098
|
+
delete headers["content-length"];
|
|
7099
|
+
delete headers["content-encoding"];
|
|
7100
|
+
if (contentType) headers["content-type"] = contentType;
|
|
7101
|
+
return headers;
|
|
7102
|
+
}
|
|
7103
|
+
function originOf(url) {
|
|
7104
|
+
try {
|
|
7105
|
+
const origin = new URL(url).origin;
|
|
7106
|
+
return origin && origin !== "null" ? origin : null;
|
|
7107
|
+
} catch {
|
|
7108
|
+
return null;
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
7111
|
+
async function installProbeInterception(page, item) {
|
|
7112
|
+
if (!isFaultAllowed(item.level ?? "client", item.fault)) {
|
|
7113
|
+
return { applied: () => false, appliedFault: () => null, serverError: () => false };
|
|
7114
|
+
}
|
|
7115
|
+
const state = createProbeState();
|
|
7116
|
+
let applied = false;
|
|
7117
|
+
let appliedFault = null;
|
|
7118
|
+
let serverError = false;
|
|
7119
|
+
let appOrigin = null;
|
|
7120
|
+
const probeSecret = process.env[PIWI_PROBE_ENV.secret];
|
|
7121
|
+
page.on("framenavigated", (frame) => {
|
|
7122
|
+
if (frame === page.mainFrame()) {
|
|
7123
|
+
markNavigated(state);
|
|
7124
|
+
const origin = originOf(page.url());
|
|
7125
|
+
if (origin) appOrigin = origin;
|
|
7126
|
+
}
|
|
7127
|
+
});
|
|
7128
|
+
await page.route("**/*", async (route) => {
|
|
7129
|
+
const request = route.request();
|
|
7130
|
+
if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
|
|
7131
|
+
await route.fallback();
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
if (item.level === "server") {
|
|
7135
|
+
if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
|
|
7136
|
+
await route.fallback();
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
|
|
7140
|
+
if (item.dependency) spec.dependency = item.dependency;
|
|
7141
|
+
try {
|
|
7142
|
+
const response = await route.fetch({
|
|
7143
|
+
headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
|
|
7144
|
+
});
|
|
7145
|
+
const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
|
|
7146
|
+
appliedFault = trace.appliedFault;
|
|
7147
|
+
serverError = trace.serverError;
|
|
7148
|
+
applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
|
|
7149
|
+
await route.fulfill({ response });
|
|
7150
|
+
} catch {
|
|
7151
|
+
applied = false;
|
|
7152
|
+
await route.fallback();
|
|
7153
|
+
}
|
|
7154
|
+
return;
|
|
7155
|
+
}
|
|
7156
|
+
try {
|
|
7157
|
+
const response = await route.fetch();
|
|
7158
|
+
const body = await response.text();
|
|
7159
|
+
const out = computeFault(item.fault, {
|
|
7160
|
+
status: response.status(),
|
|
7161
|
+
body,
|
|
7162
|
+
contentType: response.headers()["content-type"] ?? null
|
|
7163
|
+
});
|
|
7164
|
+
applied = out.changed;
|
|
7165
|
+
if (applied) appliedFault = item.fault;
|
|
7166
|
+
if (out.delayMs > 0) await new Promise((resolve5) => setTimeout(resolve5, out.delayMs));
|
|
7167
|
+
const headers = fulfillHeaders(response.headers(), out.contentType);
|
|
7168
|
+
await route.fulfill({ status: out.status, body: out.body, headers });
|
|
7169
|
+
} catch {
|
|
7170
|
+
applied = false;
|
|
7171
|
+
await route.fallback();
|
|
7172
|
+
}
|
|
7173
|
+
});
|
|
7174
|
+
return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
|
|
7175
|
+
}
|
|
7176
|
+
|
|
6278
7177
|
// src/internal/capture/inspect-on-failure.ts
|
|
6279
7178
|
function isCi(ci) {
|
|
6280
7179
|
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
@@ -6311,88 +7210,11 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
6311
7210
|
}
|
|
6312
7211
|
|
|
6313
7212
|
// src/internal/capture/pick-on-failure.ts
|
|
6314
|
-
var
|
|
7213
|
+
var path19 = __toESM(require("path"));
|
|
6315
7214
|
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
7215
|
function parseLeafLocatorExpression(rawExpr) {
|
|
6392
|
-
const
|
|
6393
|
-
|
|
6394
|
-
if (!m) return null;
|
|
6395
|
-
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
7216
|
+
const leaf = parseLeafLocatorCall(rawExpr);
|
|
7217
|
+
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
6396
7218
|
}
|
|
6397
7219
|
function deriveFailedLocator(testInfo) {
|
|
6398
7220
|
const info = testInfo;
|
|
@@ -6405,7 +7227,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
|
6405
7227
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
6406
7228
|
if (!parsed) continue;
|
|
6407
7229
|
const loc = err.location;
|
|
6408
|
-
const location = loc ? `${
|
|
7230
|
+
const location = loc ? `${path19.relative(process.cwd(), loc.file).split(path19.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
6409
7231
|
return { method: parsed.method, args: parsed.args, location };
|
|
6410
7232
|
}
|
|
6411
7233
|
return null;
|
|
@@ -6599,6 +7421,9 @@ function createSink() {
|
|
|
6599
7421
|
stashedPageState: null,
|
|
6600
7422
|
stashedAria: null,
|
|
6601
7423
|
stashedAriaJson: null,
|
|
7424
|
+
pageInventories: [],
|
|
7425
|
+
probeItem: null,
|
|
7426
|
+
probeInterception: null,
|
|
6602
7427
|
pickOffered: false,
|
|
6603
7428
|
userPick: null
|
|
6604
7429
|
};
|
|
@@ -6775,6 +7600,62 @@ async function readPageState(page) {
|
|
|
6775
7600
|
return null;
|
|
6776
7601
|
}
|
|
6777
7602
|
}
|
|
7603
|
+
var inventoriedPageKeys = /* @__PURE__ */ new Set();
|
|
7604
|
+
var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
7605
|
+
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
7606
|
+
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
7607
|
+
function withTimeout(promise, ms) {
|
|
7608
|
+
return new Promise((resolve5, reject) => {
|
|
7609
|
+
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
7610
|
+
promise.then(
|
|
7611
|
+
(value) => {
|
|
7612
|
+
clearTimeout(timer);
|
|
7613
|
+
resolve5(value);
|
|
7614
|
+
},
|
|
7615
|
+
(error) => {
|
|
7616
|
+
clearTimeout(timer);
|
|
7617
|
+
reject(error);
|
|
7618
|
+
}
|
|
7619
|
+
);
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
7622
|
+
async function readPageInventory(page) {
|
|
7623
|
+
try {
|
|
7624
|
+
const inventory = await withTimeout(
|
|
7625
|
+
page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
|
|
7626
|
+
PAGE_INVENTORY_EVAL_TIMEOUT_MS
|
|
7627
|
+
);
|
|
7628
|
+
return inventory ? { ...inventory, capturedAt: Date.now() } : null;
|
|
7629
|
+
} catch {
|
|
7630
|
+
return null;
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
async function recordPageWindow(sink, page) {
|
|
7634
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7635
|
+
let url;
|
|
7636
|
+
try {
|
|
7637
|
+
url = page.url();
|
|
7638
|
+
} catch {
|
|
7639
|
+
return;
|
|
7640
|
+
}
|
|
7641
|
+
if (!url || url.startsWith("about:")) return;
|
|
7642
|
+
const key = inventoryPageKey(url);
|
|
7643
|
+
const capturedAt = Date.now();
|
|
7644
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7645
|
+
if (key && !inventoriedPageKeys.has(key)) {
|
|
7646
|
+
inventoriedPageKeys.add(key);
|
|
7647
|
+
const content = await readPageInventory(page);
|
|
7648
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7649
|
+
sink.pageInventories.push({
|
|
7650
|
+
url: content?.url ?? url,
|
|
7651
|
+
controls: content?.controls ?? [],
|
|
7652
|
+
links: content?.links ?? [],
|
|
7653
|
+
capturedAt
|
|
7654
|
+
});
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
7657
|
+
sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
|
|
7658
|
+
}
|
|
6778
7659
|
async function stashPageState(sink, closing) {
|
|
6779
7660
|
const page = sink.lastActivePage;
|
|
6780
7661
|
if (!page || isPageClosed(page)) return;
|
|
@@ -6799,6 +7680,9 @@ async function stashPageState(sink, closing) {
|
|
|
6799
7680
|
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
6800
7681
|
await sampleAria();
|
|
6801
7682
|
}
|
|
7683
|
+
if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
7684
|
+
await recordPageWindow(sink, page);
|
|
7685
|
+
}
|
|
6802
7686
|
}
|
|
6803
7687
|
async function maybeOpenPicker(sink, closing) {
|
|
6804
7688
|
if (sink.pickOffered) return;
|
|
@@ -7042,6 +7926,13 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
7042
7926
|
function instrumentPage(page) {
|
|
7043
7927
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
7044
7928
|
INSTRUMENTED_PAGES.add(page);
|
|
7929
|
+
const probeSink = currentSink;
|
|
7930
|
+
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
7931
|
+
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
7932
|
+
probeSink.probeInterception = interception;
|
|
7933
|
+
}).catch(() => {
|
|
7934
|
+
});
|
|
7935
|
+
}
|
|
7045
7936
|
const ctx = pageContext(page);
|
|
7046
7937
|
if (ctx) instrumentContext(ctx);
|
|
7047
7938
|
if (typeof page.close === "function") {
|
|
@@ -7077,6 +7968,14 @@ function instrumentPage(page) {
|
|
|
7077
7968
|
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
7078
7969
|
}
|
|
7079
7970
|
}
|
|
7971
|
+
if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
7972
|
+
page.on("framenavigated", (frame) => {
|
|
7973
|
+
const sink = currentSink;
|
|
7974
|
+
if (!sink || frame !== page.mainFrame()) return;
|
|
7975
|
+
void recordPageWindow(sink, page).catch(() => {
|
|
7976
|
+
});
|
|
7977
|
+
});
|
|
7978
|
+
}
|
|
7080
7979
|
page.on("console", (msg) => {
|
|
7081
7980
|
const sink = currentSink;
|
|
7082
7981
|
if (!sink) return;
|
|
@@ -7136,14 +8035,14 @@ function instrumentPage(page) {
|
|
|
7136
8035
|
const logHeader = headers["x-piwi-logs"];
|
|
7137
8036
|
if (logHeader) {
|
|
7138
8037
|
try {
|
|
7139
|
-
entry.serverLogs = JSON.parse((0,
|
|
8038
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
7140
8039
|
} catch {
|
|
7141
8040
|
}
|
|
7142
8041
|
}
|
|
7143
8042
|
const traceHeader = headers["x-piwi-trace"];
|
|
7144
8043
|
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
7145
8044
|
try {
|
|
7146
|
-
entry.serverTraces = JSON.parse((0,
|
|
8045
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
7147
8046
|
} catch {
|
|
7148
8047
|
}
|
|
7149
8048
|
}
|
|
@@ -7328,6 +8227,16 @@ async function flushSink(sink, testInfo) {
|
|
|
7328
8227
|
});
|
|
7329
8228
|
}
|
|
7330
8229
|
}
|
|
8230
|
+
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
8231
|
+
if (pageReadable) await recordPageWindow(sink, page);
|
|
8232
|
+
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
8233
|
+
if (windows.length > 0) {
|
|
8234
|
+
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
8235
|
+
contentType: "application/json",
|
|
8236
|
+
body: Buffer.from(JSON.stringify(windows))
|
|
8237
|
+
});
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
7331
8240
|
}
|
|
7332
8241
|
var piwiFixtures = {
|
|
7333
8242
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
@@ -7353,11 +8262,36 @@ var piwiFixtures = {
|
|
|
7353
8262
|
async ({}, use, testInfo) => {
|
|
7354
8263
|
const sink = createSink();
|
|
7355
8264
|
sink.testInfo = testInfo;
|
|
8265
|
+
if (isProbeMode())
|
|
8266
|
+
sink.probeItem = probeItemForTest({
|
|
8267
|
+
title: testInfo.title,
|
|
8268
|
+
file: testInfo.file,
|
|
8269
|
+
titlePath: testInfo.titlePath
|
|
8270
|
+
});
|
|
7356
8271
|
currentSink = sink;
|
|
7357
8272
|
try {
|
|
7358
8273
|
await use();
|
|
7359
8274
|
} finally {
|
|
7360
8275
|
currentSink = null;
|
|
8276
|
+
if (sink.probeItem) {
|
|
8277
|
+
const applied = sink.probeInterception?.applied() ?? false;
|
|
8278
|
+
const level = sink.probeItem.level ?? "client";
|
|
8279
|
+
const handled = level === "server" ? classifyProbeHandled({
|
|
8280
|
+
consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
|
|
8281
|
+
dialogs: sink.dialogs.length,
|
|
8282
|
+
backendError: sink.probeInterception?.serverError() ?? false
|
|
8283
|
+
}) : "n/a";
|
|
8284
|
+
recordProbeOutcome({
|
|
8285
|
+
testCaseId: sink.probeItem.testCaseId,
|
|
8286
|
+
routeKey: sink.probeItem.routeKey,
|
|
8287
|
+
fault: sink.probeItem.fault,
|
|
8288
|
+
applied,
|
|
8289
|
+
outcome: outcomeFromStatus(testInfo.status, applied),
|
|
8290
|
+
level,
|
|
8291
|
+
dependency: sink.probeItem.dependency ?? null,
|
|
8292
|
+
handled
|
|
8293
|
+
});
|
|
8294
|
+
}
|
|
7361
8295
|
await flushSink(sink, testInfo);
|
|
7362
8296
|
}
|
|
7363
8297
|
},
|
|
@@ -7369,13 +8303,13 @@ function extendPiwiFixtures(test) {
|
|
|
7369
8303
|
}
|
|
7370
8304
|
|
|
7371
8305
|
// src/internal/ai/ai-fixtures.ts
|
|
7372
|
-
var
|
|
7373
|
-
var
|
|
8306
|
+
var fs18 = __toESM(require("fs"));
|
|
8307
|
+
var path22 = __toESM(require("path"));
|
|
7374
8308
|
var import_test = require("@playwright/test");
|
|
7375
8309
|
|
|
7376
8310
|
// src/internal/ai/artifact.ts
|
|
7377
|
-
var
|
|
7378
|
-
var
|
|
8311
|
+
var fs17 = __toESM(require("fs"));
|
|
8312
|
+
var path20 = __toESM(require("path"));
|
|
7379
8313
|
var ARTIFACT_VERSION = 1;
|
|
7380
8314
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
7381
8315
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -7468,7 +8402,7 @@ function parseEntry(text) {
|
|
|
7468
8402
|
function readEntry(file) {
|
|
7469
8403
|
let text;
|
|
7470
8404
|
try {
|
|
7471
|
-
text =
|
|
8405
|
+
text = fs17.readFileSync(file, "utf8");
|
|
7472
8406
|
} catch (error) {
|
|
7473
8407
|
if (error.code === "ENOENT") return null;
|
|
7474
8408
|
throw error;
|
|
@@ -7478,28 +8412,28 @@ function readEntry(file) {
|
|
|
7478
8412
|
function writeEntry(file, entry) {
|
|
7479
8413
|
const canonical = serializeEntry(entry);
|
|
7480
8414
|
return withEntryLock(file, () => {
|
|
7481
|
-
if (
|
|
7482
|
-
|
|
8415
|
+
if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
8416
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
7483
8417
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7484
|
-
|
|
7485
|
-
|
|
8418
|
+
fs17.writeFileSync(tmp, canonical);
|
|
8419
|
+
fs17.renameSync(tmp, file);
|
|
7486
8420
|
return { written: true };
|
|
7487
8421
|
});
|
|
7488
8422
|
}
|
|
7489
8423
|
function withEntryLock(file, fn) {
|
|
7490
8424
|
const lock = `${file}.lock`;
|
|
7491
|
-
|
|
8425
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
7492
8426
|
const deadline = Date.now() + 5e3;
|
|
7493
8427
|
for (; ; ) {
|
|
7494
8428
|
try {
|
|
7495
|
-
const fd =
|
|
7496
|
-
|
|
8429
|
+
const fd = fs17.openSync(lock, "wx");
|
|
8430
|
+
fs17.closeSync(fd);
|
|
7497
8431
|
break;
|
|
7498
8432
|
} catch (error) {
|
|
7499
8433
|
if (error.code !== "EEXIST") throw error;
|
|
7500
8434
|
if (Date.now() > deadline) {
|
|
7501
8435
|
try {
|
|
7502
|
-
|
|
8436
|
+
fs17.unlinkSync(lock);
|
|
7503
8437
|
} catch {
|
|
7504
8438
|
}
|
|
7505
8439
|
}
|
|
@@ -7509,7 +8443,7 @@ function withEntryLock(file, fn) {
|
|
|
7509
8443
|
return fn();
|
|
7510
8444
|
} finally {
|
|
7511
8445
|
try {
|
|
7512
|
-
|
|
8446
|
+
fs17.unlinkSync(lock);
|
|
7513
8447
|
} catch {
|
|
7514
8448
|
}
|
|
7515
8449
|
}
|
|
@@ -7517,7 +8451,7 @@ function withEntryLock(file, fn) {
|
|
|
7517
8451
|
|
|
7518
8452
|
// src/internal/ai/keys.ts
|
|
7519
8453
|
var crypto3 = __toESM(require("crypto"));
|
|
7520
|
-
var
|
|
8454
|
+
var path21 = __toESM(require("path"));
|
|
7521
8455
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
7522
8456
|
function normalizeTemplate(template) {
|
|
7523
8457
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -7531,13 +8465,13 @@ function slug(text, maxLength = 40) {
|
|
|
7531
8465
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
7532
8466
|
}
|
|
7533
8467
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
7534
|
-
return
|
|
8468
|
+
return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
|
|
7535
8469
|
}
|
|
7536
8470
|
function entryPath(params) {
|
|
7537
8471
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
7538
8472
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
7539
8473
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
7540
|
-
return
|
|
8474
|
+
return path21.join(entryDir(params.specFile, params.dir), name);
|
|
7541
8475
|
}
|
|
7542
8476
|
function parseLocation(location) {
|
|
7543
8477
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -7991,7 +8925,7 @@ function testIdentity(testInfo) {
|
|
|
7991
8925
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
7992
8926
|
}
|
|
7993
8927
|
function relativeToCwd(file) {
|
|
7994
|
-
return
|
|
8928
|
+
return path22.relative(process.cwd(), file).split(path22.sep).join("/");
|
|
7995
8929
|
}
|
|
7996
8930
|
function recordIntents(intents, entry) {
|
|
7997
8931
|
const add = (locator, kind) => {
|
|
@@ -8018,7 +8952,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
8018
8952
|
const readSource = () => {
|
|
8019
8953
|
if (source === null) {
|
|
8020
8954
|
try {
|
|
8021
|
-
source =
|
|
8955
|
+
source = fs18.readFileSync(testInfo.file, "utf8");
|
|
8022
8956
|
} catch {
|
|
8023
8957
|
source = "";
|
|
8024
8958
|
}
|