@piwitests/reporter 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/index.js +142 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +476 -83
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +1 -0
- package/dist/internal/capture/capture-fixtures.d.ts +47 -1
- package/dist/internal/capture/capture-fixtures.js +309 -73
- package/dist/internal/capture/locator-healing.js +7 -1
- package/dist/internal/capture/pick-on-failure.js +7 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -568,6 +568,7 @@ function toWireTestCase(tc) {
|
|
|
568
568
|
locks: rest.locks ?? null,
|
|
569
569
|
testMeta: rest.testMeta ?? null,
|
|
570
570
|
locatorSnapshots: rest.locatorSnapshots || null,
|
|
571
|
+
locatorPages: rest.locatorPages || null,
|
|
571
572
|
didNotRunReason: rest.didNotRunReason ?? null,
|
|
572
573
|
blockedBy: rest.blockedBy ?? null
|
|
573
574
|
};
|
|
@@ -996,6 +997,7 @@ var ATTACHMENT_NAMES = {
|
|
|
996
997
|
dialogs: "piwi-dialogs",
|
|
997
998
|
network: "piwi-network",
|
|
998
999
|
pageInventory: "piwi-page-inventory",
|
|
1000
|
+
locatorPages: "piwi-locator-pages",
|
|
999
1001
|
webVitals: "piwi-web-vitals",
|
|
1000
1002
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
1001
1003
|
pageState: "piwi-page-state",
|
|
@@ -1382,7 +1384,7 @@ var MetadataCollector = class {
|
|
|
1382
1384
|
/**
|
|
1383
1385
|
* Walk the test's parent `describe` suites to extract the suite path (titles)
|
|
1384
1386
|
* and per-level config (parallel mode + annotations). Reaches into Playwright
|
|
1385
|
-
* suite internals (`_parallelMode`, `
|
|
1387
|
+
* suite internals (`_parallelMode`, `_staticAnnotations`) — kept here next to
|
|
1386
1388
|
* `getBrowserConfig` so all such access is guarded behind one class.
|
|
1387
1389
|
*/
|
|
1388
1390
|
getSuiteInfo(test) {
|
|
@@ -1399,7 +1401,10 @@ var MetadataCollector = class {
|
|
|
1399
1401
|
suitePath.push(s.title);
|
|
1400
1402
|
const rawMode = s._parallelMode;
|
|
1401
1403
|
const mode = rawMode === "parallel" ? "parallel" : rawMode === "serial" ? "serial" : "default";
|
|
1402
|
-
const
|
|
1404
|
+
const staticAnnotations = s._staticAnnotations ?? [];
|
|
1405
|
+
const annotations = staticAnnotations.map(
|
|
1406
|
+
(a) => a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description }
|
|
1407
|
+
);
|
|
1403
1408
|
suiteConfig.push({ mode, annotations });
|
|
1404
1409
|
}
|
|
1405
1410
|
return { suitePath, suiteConfig };
|
|
@@ -1497,6 +1502,8 @@ var MetadataCollector = class {
|
|
|
1497
1502
|
// The origin of `baseURL` is what the graph treats as the run's own
|
|
1498
1503
|
// surface, so only requests to it become route nodes.
|
|
1499
1504
|
baseURL: p.use?.baseURL,
|
|
1505
|
+
// The attribute `getByTestId` reads, so the locator index can resolve those locators on a live page.
|
|
1506
|
+
testIdAttribute: p.use?.testIdAttribute,
|
|
1500
1507
|
viewport: p.use?.viewport,
|
|
1501
1508
|
deviceScaleFactor: p.use?.deviceScaleFactor
|
|
1502
1509
|
}
|
|
@@ -2141,11 +2148,14 @@ var MAX_STEP_PARAM_VALUE_CHARS = 200;
|
|
|
2141
2148
|
function capStepValue(value, max) {
|
|
2142
2149
|
return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
|
|
2143
2150
|
}
|
|
2151
|
+
var HTTP_METHOD_TITLE_RE = /^(?:get|post|put|patch|delete|head|options|fetch)$/i;
|
|
2144
2152
|
function categorizeStep(title, pwCategory, params) {
|
|
2145
2153
|
if (!title) return "other";
|
|
2146
|
-
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
2154
|
+
if (pwCategory === "hook" || pwCategory === "fixture" || pwCategory === "test.step") return pwCategory;
|
|
2147
2155
|
if (pwCategory === "expect") return "assertion";
|
|
2156
|
+
if (pwCategory === "test.attach") return "attach";
|
|
2148
2157
|
const lower = title.toLowerCase();
|
|
2158
|
+
if (HTTP_METHOD_TITLE_RE.test(title.trim())) return "api";
|
|
2149
2159
|
if (lower.startsWith("wait for") || lower.startsWith("locator.waitfor") || lower.startsWith("page.waitfor") || lower.startsWith("frame.waitfor"))
|
|
2150
2160
|
return "wait";
|
|
2151
2161
|
if (typeof params?.url === "string" && pwCategory !== "expect") return "navigation";
|
|
@@ -2183,13 +2193,14 @@ function normalizeStepParams(raw) {
|
|
|
2183
2193
|
}
|
|
2184
2194
|
return count > 0 ? out : void 0;
|
|
2185
2195
|
}
|
|
2186
|
-
function flattenSteps(steps) {
|
|
2196
|
+
function flattenSteps(steps, depth = 0) {
|
|
2187
2197
|
const result = [];
|
|
2188
2198
|
for (const step of steps) {
|
|
2189
2199
|
const flat = {
|
|
2190
2200
|
title: step.title,
|
|
2191
2201
|
duration: step.duration,
|
|
2192
|
-
category: categorizeStep(step.title, step.category, step.params)
|
|
2202
|
+
category: categorizeStep(step.title, step.category, step.params),
|
|
2203
|
+
depth
|
|
2193
2204
|
};
|
|
2194
2205
|
if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
|
|
2195
2206
|
const params = normalizeStepParams(step.params);
|
|
@@ -2201,7 +2212,7 @@ function flattenSteps(steps) {
|
|
|
2201
2212
|
if (step.location) flat.location = `${step.location.file}:${step.location.line}:${step.location.column}`;
|
|
2202
2213
|
if (step.startTime) flat.startTime = step.startTime instanceof Date ? step.startTime.getTime() : step.startTime;
|
|
2203
2214
|
result.push(flat);
|
|
2204
|
-
if (step.steps?.length > 0) result.push(...flattenSteps(step.steps));
|
|
2215
|
+
if (step.steps?.length > 0) result.push(...flattenSteps(step.steps, depth + 1));
|
|
2205
2216
|
}
|
|
2206
2217
|
return result;
|
|
2207
2218
|
}
|
|
@@ -2313,7 +2324,8 @@ function findOwnPackageJson(fromDir) {
|
|
|
2313
2324
|
const pkgPath = path8.resolve(fromDir, offset, "package.json");
|
|
2314
2325
|
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
2315
2326
|
const { name, version } = pkg;
|
|
2316
|
-
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
2327
|
+
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
2328
|
+
return { version, root: path8.dirname(pkgPath) };
|
|
2317
2329
|
} catch {
|
|
2318
2330
|
}
|
|
2319
2331
|
}
|
|
@@ -2767,8 +2779,8 @@ function parseRouteKey(routeKey) {
|
|
|
2767
2779
|
return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
|
|
2768
2780
|
}
|
|
2769
2781
|
function patternToRegex(pattern) {
|
|
2770
|
-
const
|
|
2771
|
-
const escaped =
|
|
2782
|
+
const path24 = pattern.split("?")[0] ?? pattern;
|
|
2783
|
+
const escaped = path24.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2772
2784
|
return new RegExp(`^${escaped}$`);
|
|
2773
2785
|
}
|
|
2774
2786
|
function suitePathEqual(a, b) {
|
|
@@ -2791,13 +2803,13 @@ function matchProbeItem(plan, test) {
|
|
|
2791
2803
|
function requestMatchesRoute(method, url, routeKey) {
|
|
2792
2804
|
const { method: rm, pattern } = parseRouteKey(routeKey);
|
|
2793
2805
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
2794
|
-
let
|
|
2806
|
+
let path24;
|
|
2795
2807
|
try {
|
|
2796
|
-
|
|
2808
|
+
path24 = new URL(url).pathname;
|
|
2797
2809
|
} catch {
|
|
2798
|
-
|
|
2810
|
+
path24 = url.split("?")[0] ?? url;
|
|
2799
2811
|
}
|
|
2800
|
-
return patternToRegex(pattern).test(
|
|
2812
|
+
return patternToRegex(pattern).test(path24);
|
|
2801
2813
|
}
|
|
2802
2814
|
function createProbeState() {
|
|
2803
2815
|
return { navigated: false, matches: 0 };
|
|
@@ -2819,10 +2831,10 @@ function isProbeMode() {
|
|
|
2819
2831
|
function loadPlan() {
|
|
2820
2832
|
if (planCache !== void 0) return planCache;
|
|
2821
2833
|
planCache = null;
|
|
2822
|
-
const
|
|
2823
|
-
if (
|
|
2834
|
+
const path24 = process.env[PIWI_PROBE_ENV.plan];
|
|
2835
|
+
if (path24) {
|
|
2824
2836
|
try {
|
|
2825
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
2837
|
+
planCache = JSON.parse(fs14.readFileSync(path24, "utf8"));
|
|
2826
2838
|
} catch {
|
|
2827
2839
|
planCache = null;
|
|
2828
2840
|
}
|
|
@@ -2844,10 +2856,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
2844
2856
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2845
2857
|
}
|
|
2846
2858
|
function recordProbeOutcome(line) {
|
|
2847
|
-
const
|
|
2848
|
-
if (!
|
|
2859
|
+
const path24 = process.env[PIWI_PROBE_ENV.results];
|
|
2860
|
+
if (!path24) return;
|
|
2849
2861
|
try {
|
|
2850
|
-
fs14.appendFileSync(
|
|
2862
|
+
fs14.appendFileSync(path24, `${JSON.stringify(line)}
|
|
2851
2863
|
`);
|
|
2852
2864
|
} catch {
|
|
2853
2865
|
}
|
|
@@ -4084,12 +4096,20 @@ function readUrl(text, callLog, received) {
|
|
|
4084
4096
|
}
|
|
4085
4097
|
return null;
|
|
4086
4098
|
}
|
|
4099
|
+
var MATCHER_LINE_RE = /^(?:expect[.(]|Timed out\b|Expected\b|Received\b|Locator\b)/;
|
|
4100
|
+
function readCustomMessage(messageHead) {
|
|
4101
|
+
const lines = messageHead.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
4102
|
+
if (lines.length < 2) return null;
|
|
4103
|
+
const first = lines[0].replace(/^Error:\s*/, "");
|
|
4104
|
+
if (!first || MATCHER_LINE_RE.test(first)) return null;
|
|
4105
|
+
return /^(?:expect[.(]|Timed out\b)/.test(lines[1]) ? first : null;
|
|
4106
|
+
}
|
|
4087
4107
|
function parsePlaywrightError(raw, context) {
|
|
4088
4108
|
const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
|
|
4089
4109
|
const text = withoutStackFrames(clean);
|
|
4090
4110
|
const messageHead = extractMessageHead(clean);
|
|
4091
|
-
const
|
|
4092
|
-
const errorName = ERROR_NAME_RE.exec(
|
|
4111
|
+
const firstLine2 = messageHead.split("\n")[0] ?? "";
|
|
4112
|
+
const errorName = ERROR_NAME_RE.exec(firstLine2)?.[1] ?? null;
|
|
4093
4113
|
const strict = STRICT_RE.exec(text);
|
|
4094
4114
|
const call = CALL_RE.exec(text);
|
|
4095
4115
|
const callLog = readCallLog(text);
|
|
@@ -4159,12 +4179,134 @@ function parsePlaywrightError(raw, context) {
|
|
|
4159
4179
|
lastCallLogLine: callLog.lastLine,
|
|
4160
4180
|
lastStateLine: callLog.stateLine,
|
|
4161
4181
|
messageHead,
|
|
4182
|
+
customMessage: isAssertion ? readCustomMessage(messageHead) : null,
|
|
4162
4183
|
topFrame: frame ? `${frame.file}:${frame.line}` : null,
|
|
4163
4184
|
isNavigationFailure,
|
|
4164
4185
|
isLocatorResolutionFailure
|
|
4165
4186
|
};
|
|
4166
4187
|
}
|
|
4167
4188
|
|
|
4189
|
+
// ../core/src/step-tree.ts
|
|
4190
|
+
var SPAN_TOLERANCE_MS = 1;
|
|
4191
|
+
var SETUP_CONTAINER_RE = /^before hooks$/i;
|
|
4192
|
+
var TEARDOWN_CONTAINER_RE = /^(?:after hooks|worker cleanup|worker teardown)$/i;
|
|
4193
|
+
var NAMED_HOOK_RE = /^(before|after)(each|all) hook$/i;
|
|
4194
|
+
var FIXTURE_TITLE_RE = /^fixture\s+["“](.+)["”]$/i;
|
|
4195
|
+
var CAPTURE_ATTACH_RE = /^attach\s+["“]piwi-/i;
|
|
4196
|
+
var CAPTURE_FIXTURE_RE = /^fixture\s+["“]piwiCapture["”]$/i;
|
|
4197
|
+
var CAPTURE_PROBE_TITLE_RE = /^(?:evaluate|aria snapshot|locator\.(?:evaluate|ariasnapshot)|page\.evaluate)\b/i;
|
|
4198
|
+
var REPORTER_BUNDLE_RE = /[\\/](?:@piwitests[\\/]reporter|packages[\\/]reporter)[\\/]dist[\\/]/;
|
|
4199
|
+
function str(value) {
|
|
4200
|
+
return typeof value === "string" ? value : "";
|
|
4201
|
+
}
|
|
4202
|
+
function num(value) {
|
|
4203
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4204
|
+
}
|
|
4205
|
+
function errorMessage2(step) {
|
|
4206
|
+
const error = step.error;
|
|
4207
|
+
if (typeof error === "string") return error;
|
|
4208
|
+
return str(error?.message);
|
|
4209
|
+
}
|
|
4210
|
+
function isFailedStep(step) {
|
|
4211
|
+
return step.failed === true || errorMessage2(step).trim().length > 0;
|
|
4212
|
+
}
|
|
4213
|
+
function isCaptureStep(step) {
|
|
4214
|
+
const title = str(step.title).trim();
|
|
4215
|
+
if (CAPTURE_ATTACH_RE.test(title) || CAPTURE_FIXTURE_RE.test(title)) return true;
|
|
4216
|
+
return CAPTURE_PROBE_TITLE_RE.test(title) && REPORTER_BUNDLE_RE.test(str(step.location));
|
|
4217
|
+
}
|
|
4218
|
+
function isPhaseContainer(step) {
|
|
4219
|
+
const title = str(step.title).trim();
|
|
4220
|
+
return SETUP_CONTAINER_RE.test(title) || TEARDOWN_CONTAINER_RE.test(title);
|
|
4221
|
+
}
|
|
4222
|
+
function stepParents(steps) {
|
|
4223
|
+
const parents = steps.map(() => -1);
|
|
4224
|
+
if (steps.length === 0) return parents;
|
|
4225
|
+
if (steps.every((s) => num(s.depth) !== null)) {
|
|
4226
|
+
const stack = [];
|
|
4227
|
+
steps.forEach((step, i) => {
|
|
4228
|
+
const depth = num(step.depth);
|
|
4229
|
+
while (stack.length > 0 && num(steps[stack[stack.length - 1]].depth) >= depth) stack.pop();
|
|
4230
|
+
parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1;
|
|
4231
|
+
stack.push(i);
|
|
4232
|
+
});
|
|
4233
|
+
return parents;
|
|
4234
|
+
}
|
|
4235
|
+
if (steps.every((s) => num(s.startTime) !== null && num(s.duration) !== null)) {
|
|
4236
|
+
const start = (i) => num(steps[i].startTime);
|
|
4237
|
+
const end = (i) => start(i) + Math.max(0, num(steps[i].duration));
|
|
4238
|
+
const contains = (a, b) => end(a) > start(a) && start(b) >= start(a) - SPAN_TOLERANCE_MS && start(b) < end(a) && end(b) <= end(a) + SPAN_TOLERANCE_MS;
|
|
4239
|
+
const stack = [];
|
|
4240
|
+
steps.forEach((_, i) => {
|
|
4241
|
+
while (stack.length > 0 && !contains(stack[stack.length - 1], i)) stack.pop();
|
|
4242
|
+
parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1;
|
|
4243
|
+
stack.push(i);
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
return parents;
|
|
4247
|
+
}
|
|
4248
|
+
function isDescendant(descendant, ancestor, parents) {
|
|
4249
|
+
for (let p = parents[descendant] ?? -1; p !== -1; p = parents[p] ?? -1) {
|
|
4250
|
+
if (p === ancestor) return true;
|
|
4251
|
+
}
|
|
4252
|
+
return false;
|
|
4253
|
+
}
|
|
4254
|
+
function topAncestor(index, parents) {
|
|
4255
|
+
let top = index;
|
|
4256
|
+
while ((parents[top] ?? -1) !== -1) top = parents[top];
|
|
4257
|
+
return top;
|
|
4258
|
+
}
|
|
4259
|
+
function stepPhases(steps, parents = stepParents(steps)) {
|
|
4260
|
+
return steps.map((_, i) => {
|
|
4261
|
+
const top = str(steps[topAncestor(i, parents)].title).trim();
|
|
4262
|
+
if (SETUP_CONTAINER_RE.test(top)) return "setup";
|
|
4263
|
+
if (TEARDOWN_CONTAINER_RE.test(top)) return "teardown";
|
|
4264
|
+
return "body";
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
function failingStepIndex(steps, parents = stepParents(steps)) {
|
|
4268
|
+
const candidates = [];
|
|
4269
|
+
steps.forEach((step, i) => {
|
|
4270
|
+
if (isFailedStep(step) && !isCaptureStep(step)) candidates.push(i);
|
|
4271
|
+
});
|
|
4272
|
+
if (candidates.length === 0) return null;
|
|
4273
|
+
let current = candidates[0];
|
|
4274
|
+
for (; ; ) {
|
|
4275
|
+
const inner = candidates.find((c) => c > current && isDescendant(c, current, parents));
|
|
4276
|
+
if (inner === void 0) return current;
|
|
4277
|
+
current = inner;
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
function stepHookName(step) {
|
|
4281
|
+
const title = str(step.title).trim();
|
|
4282
|
+
const category = str(step.category);
|
|
4283
|
+
const named = NAMED_HOOK_RE.exec(title);
|
|
4284
|
+
if (named) return `${named[1].toLowerCase()}${named[2][0].toUpperCase()}${named[2].slice(1).toLowerCase()}`;
|
|
4285
|
+
const fixture = FIXTURE_TITLE_RE.exec(title);
|
|
4286
|
+
if (fixture) return `fixture "${fixture[1]}"`;
|
|
4287
|
+
if (category === "fixture") return title ? `fixture "${title.replace(/^fixture:\s*/i, "")}"` : null;
|
|
4288
|
+
if (category === "hook" && title && !isPhaseContainer(step)) return `hook "${title}"`;
|
|
4289
|
+
return null;
|
|
4290
|
+
}
|
|
4291
|
+
function firstLine(message) {
|
|
4292
|
+
return stripAnsi(str(message)).split("\n")[0].trim();
|
|
4293
|
+
}
|
|
4294
|
+
function failureHookContext(steps, error, parents = stepParents(steps)) {
|
|
4295
|
+
const index = failingStepIndex(steps, parents);
|
|
4296
|
+
if (index === null) return null;
|
|
4297
|
+
const phase = stepPhases(steps, parents)[index];
|
|
4298
|
+
if (phase === "body") return null;
|
|
4299
|
+
if (error != null) {
|
|
4300
|
+
const stepError = firstLine(errorMessage2(steps[index]));
|
|
4301
|
+
if (stepError && firstLine(error) !== stepError) return null;
|
|
4302
|
+
}
|
|
4303
|
+
for (let i = index; i !== -1; i = parents[i] ?? -1) {
|
|
4304
|
+
const name = stepHookName(steps[i]);
|
|
4305
|
+
if (name) return { phase, hook: name };
|
|
4306
|
+
}
|
|
4307
|
+
return { phase, hook: phase };
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4168
4310
|
// ../core/src/describe-failure.ts
|
|
4169
4311
|
var HEADLINE_MAX_CHARS = 120;
|
|
4170
4312
|
var VALUE_MAX_CHARS = 40;
|
|
@@ -4511,7 +4653,27 @@ function buildCrash(parsed, opts) {
|
|
|
4511
4653
|
}
|
|
4512
4654
|
return line.text(what);
|
|
4513
4655
|
}
|
|
4656
|
+
var CUSTOM_MESSAGE_MAX_CHARS = 60;
|
|
4657
|
+
var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
|
|
4658
|
+
function withLeads(line, parsed, ctx) {
|
|
4659
|
+
const hook = ctx?.failedIn && !parsed.timeoutPhase ? ctx.failedIn.hook : null;
|
|
4660
|
+
const message = parsed.customMessage ? truncateValue(parsed.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
|
|
4661
|
+
if (!hook && !message) return line;
|
|
4662
|
+
const led = new Line();
|
|
4663
|
+
if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
|
|
4664
|
+
if (message) led.text(`"${message}" failed: `);
|
|
4665
|
+
line.parts.forEach((part, i) => {
|
|
4666
|
+
const text = i === 0 && part.kind === "text" && SENTENCE_CASE_WORD_RE.test(part.text) ? part.text[0].toLowerCase() + part.text.slice(1) : part.text;
|
|
4667
|
+
if (part.kind === "text") led.text(text);
|
|
4668
|
+
else if (part.kind === "locator") led.locator(text);
|
|
4669
|
+
else led.value(text);
|
|
4670
|
+
});
|
|
4671
|
+
return led;
|
|
4672
|
+
}
|
|
4514
4673
|
function build(parsed, opts, ctx) {
|
|
4674
|
+
return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
|
|
4675
|
+
}
|
|
4676
|
+
function buildLine(parsed, opts, ctx) {
|
|
4515
4677
|
switch (parsed.kind) {
|
|
4516
4678
|
case "action-timeout":
|
|
4517
4679
|
return buildActionTimeout(parsed, opts);
|
|
@@ -4597,15 +4759,36 @@ function describeFailureText(raw, ctx) {
|
|
|
4597
4759
|
if (!raw || !raw.trim()) return null;
|
|
4598
4760
|
return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
|
|
4599
4761
|
}
|
|
4600
|
-
function
|
|
4762
|
+
function headlineStep(steps) {
|
|
4601
4763
|
if (!steps || steps.length === 0) return null;
|
|
4602
|
-
const
|
|
4603
|
-
|
|
4764
|
+
const parents = stepParents(steps);
|
|
4765
|
+
const failed = failingStepIndex(steps, parents);
|
|
4766
|
+
if (failed !== null) return steps[failed];
|
|
4767
|
+
const phases = stepPhases(steps, parents);
|
|
4768
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
4769
|
+
const step = steps[i];
|
|
4770
|
+
if (phases[i] === "body" && !isCaptureStep(step) && !isPhaseContainer(step)) return step;
|
|
4771
|
+
}
|
|
4772
|
+
return steps[steps.length - 1];
|
|
4773
|
+
}
|
|
4774
|
+
function lastStepTitle(steps) {
|
|
4775
|
+
const title = headlineStep(steps)?.title;
|
|
4776
|
+
return typeof title === "string" ? title : null;
|
|
4777
|
+
}
|
|
4778
|
+
function failingStepParams(steps) {
|
|
4779
|
+
return headlineStep(steps)?.params ?? null;
|
|
4780
|
+
}
|
|
4781
|
+
function stepHeadlineContext(steps, error) {
|
|
4782
|
+
return {
|
|
4783
|
+
lastStepTitle: lastStepTitle(steps),
|
|
4784
|
+
stepParams: failingStepParams(steps),
|
|
4785
|
+
failedIn: steps && steps.length > 0 ? failureHookContext(steps, error ?? null) : null
|
|
4786
|
+
};
|
|
4604
4787
|
}
|
|
4605
4788
|
|
|
4606
4789
|
// src/internal/support/failure-links.ts
|
|
4607
4790
|
function failureHeadline(error, steps) {
|
|
4608
|
-
return describeFailureText(error,
|
|
4791
|
+
return describeFailureText(error, stepHeadlineContext(steps, error))?.headline ?? null;
|
|
4609
4792
|
}
|
|
4610
4793
|
function caseLocateUrl(serverUrl, runId, test) {
|
|
4611
4794
|
const params = [
|
|
@@ -4941,6 +5124,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4941
5124
|
} catch {
|
|
4942
5125
|
}
|
|
4943
5126
|
}
|
|
5127
|
+
const pagesAttachment = this.options.captureLocators !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.locatorPages) : void 0;
|
|
5128
|
+
if (pagesAttachment?.body) {
|
|
5129
|
+
try {
|
|
5130
|
+
testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
|
|
5131
|
+
} catch {
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
4944
5134
|
}
|
|
4945
5135
|
switch (status) {
|
|
4946
5136
|
case "passed":
|
|
@@ -5138,7 +5328,30 @@ var import_node_zlib2 = require("zlib");
|
|
|
5138
5328
|
|
|
5139
5329
|
// ../picker-dom/src/probe.ts
|
|
5140
5330
|
function probeElementAttrs(el, arg) {
|
|
5141
|
-
const { keep, tagRoles, inputRoles, roleSources, includeStructural
|
|
5331
|
+
const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
|
|
5332
|
+
const FIELD_TAGS = ["input", "select", "textarea"];
|
|
5333
|
+
const textWithout = (root, skip) => {
|
|
5334
|
+
if (root === skip) return "";
|
|
5335
|
+
if (root.nodeType === 3) return root.nodeValue || "";
|
|
5336
|
+
let text = "";
|
|
5337
|
+
const kids = root.childNodes || [];
|
|
5338
|
+
for (let i = 0; i < kids.length; i++) text += textWithout(kids[i], skip);
|
|
5339
|
+
return text;
|
|
5340
|
+
};
|
|
5341
|
+
const labelTextOf = (n) => {
|
|
5342
|
+
try {
|
|
5343
|
+
const ids = (n.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
5344
|
+
let text = "";
|
|
5345
|
+
for (const id of ids) {
|
|
5346
|
+
const ref = n.ownerDocument.getElementById(id);
|
|
5347
|
+
if (ref) text += ` ${textWithout(ref, n)}`;
|
|
5348
|
+
}
|
|
5349
|
+
if (!text.trim() && n.labels && n.labels.length > 0) text = textWithout(n.labels[0], n);
|
|
5350
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 120) || null;
|
|
5351
|
+
} catch {
|
|
5352
|
+
return null;
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5142
5355
|
const attrMap = {};
|
|
5143
5356
|
for (const key of keep) {
|
|
5144
5357
|
const v = el.getAttribute(key) ?? el[key];
|
|
@@ -5218,10 +5431,18 @@ function probeElementAttrs(el, arg) {
|
|
|
5218
5431
|
const targetRole = roleOf(el);
|
|
5219
5432
|
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
5220
5433
|
const nameOf = (n) => {
|
|
5434
|
+
const labelledBy = !!n.getAttribute("aria-labelledby");
|
|
5435
|
+
const isField = FIELD_TAGS.indexOf((n.tagName || "").toLowerCase()) !== -1;
|
|
5436
|
+
const lt = labelledBy || isField ? labelTextOf(n) : null;
|
|
5437
|
+
if (lt && labelledBy) return lt;
|
|
5221
5438
|
const al = n.getAttribute("aria-label");
|
|
5222
5439
|
if (al) return al;
|
|
5223
|
-
|
|
5224
|
-
|
|
5440
|
+
if (isField) {
|
|
5441
|
+
if (lt) return lt;
|
|
5442
|
+
} else {
|
|
5443
|
+
const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
5444
|
+
if (txt) return txt;
|
|
5445
|
+
}
|
|
5225
5446
|
return n.getAttribute("title") || n.getAttribute("placeholder") || null;
|
|
5226
5447
|
};
|
|
5227
5448
|
const targetName = nameOf(el);
|
|
@@ -5413,16 +5634,7 @@ function probeElementAttrs(el, arg) {
|
|
|
5413
5634
|
}
|
|
5414
5635
|
}
|
|
5415
5636
|
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
5416
|
-
|
|
5417
|
-
if (includeLabelText) {
|
|
5418
|
-
labelText = null;
|
|
5419
|
-
try {
|
|
5420
|
-
const label = hasLabel ? el.labels[0] : null;
|
|
5421
|
-
labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
|
|
5422
|
-
} catch {
|
|
5423
|
-
labelText = null;
|
|
5424
|
-
}
|
|
5425
|
-
}
|
|
5637
|
+
const labelText = labelTextOf(el);
|
|
5426
5638
|
return {
|
|
5427
5639
|
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
5428
5640
|
attributes: attrMap,
|
|
@@ -5434,7 +5646,7 @@ function probeElementAttrs(el, arg) {
|
|
|
5434
5646
|
y: Math.round(r.y + r.height / 2)
|
|
5435
5647
|
},
|
|
5436
5648
|
hasLabel,
|
|
5437
|
-
|
|
5649
|
+
labelText,
|
|
5438
5650
|
selectorCounts,
|
|
5439
5651
|
...includeStructural ? { rolePosition, ancestors } : {}
|
|
5440
5652
|
};
|
|
@@ -6405,9 +6617,15 @@ function generateAlternatives(attrs) {
|
|
|
6405
6617
|
}
|
|
6406
6618
|
function approximateAccessibleName(attrs) {
|
|
6407
6619
|
const a = attrs.attributes;
|
|
6620
|
+
const labelText = attrs.labelText || null;
|
|
6621
|
+
if (a["aria-labelledby"] && labelText) return labelText;
|
|
6408
6622
|
const ariaLabel = a["aria-label"];
|
|
6409
6623
|
if (ariaLabel) return ariaLabel;
|
|
6410
|
-
if (attrs.
|
|
6624
|
+
if (["input", "select", "textarea"].includes(attrs.tagName)) {
|
|
6625
|
+
if (labelText) return labelText;
|
|
6626
|
+
} else if (attrs.textContent) {
|
|
6627
|
+
return attrs.textContent;
|
|
6628
|
+
}
|
|
6411
6629
|
const title = a["title"];
|
|
6412
6630
|
if (title) return title;
|
|
6413
6631
|
const placeholder = a["placeholder"];
|
|
@@ -6987,6 +7205,94 @@ function inventoryPageKey(url) {
|
|
|
6987
7205
|
}
|
|
6988
7206
|
}
|
|
6989
7207
|
|
|
7208
|
+
// ../core/src/page-key.ts
|
|
7209
|
+
function normalizeRoute(url) {
|
|
7210
|
+
try {
|
|
7211
|
+
const parsed = new URL(url);
|
|
7212
|
+
let pathname = parsed.pathname;
|
|
7213
|
+
pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
|
|
7214
|
+
pathname = pathname.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=\/|$)/gi, "/:uuid");
|
|
7215
|
+
pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
|
|
7216
|
+
pathname = pathname.replace(/\/[A-Za-z0-9_-]{40,}(?=\/|$)/g, "/:token");
|
|
7217
|
+
pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
|
|
7218
|
+
pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
7219
|
+
let queryString = "";
|
|
7220
|
+
if (parsed.search) {
|
|
7221
|
+
const params = new URLSearchParams(parsed.search);
|
|
7222
|
+
const redacted = new URLSearchParams();
|
|
7223
|
+
for (const [key] of params) {
|
|
7224
|
+
redacted.set(key, "<redacted>");
|
|
7225
|
+
}
|
|
7226
|
+
queryString = "?" + redacted.toString();
|
|
7227
|
+
}
|
|
7228
|
+
return `${pathname}${queryString}`;
|
|
7229
|
+
} catch {
|
|
7230
|
+
return url;
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
var PATH_ANCHOR_HOST = "piwi.invalid";
|
|
7234
|
+
function pageKey(url) {
|
|
7235
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(url);
|
|
7236
|
+
if (hasScheme && !/^https?:\/\//i.test(url)) return null;
|
|
7237
|
+
const absolute = /^https?:\/\//i.test(url) ? url : `http://${PATH_ANCHOR_HOST}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
7238
|
+
const normalized = normalizeRoute(absolute);
|
|
7239
|
+
const q = normalized.indexOf("?");
|
|
7240
|
+
return q < 0 ? normalized : normalized.slice(0, q);
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
// src/internal/capture/locator-pages.ts
|
|
7244
|
+
var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
|
|
7245
|
+
var MAX_LOCATOR_CHARS = 1e3;
|
|
7246
|
+
var NON_INTERACTIONS = /* @__PURE__ */ new Set(["waitFor"]);
|
|
7247
|
+
var interactions = /* @__PURE__ */ new WeakMap();
|
|
7248
|
+
function noteNavigation(page) {
|
|
7249
|
+
interactions.set(page, 0);
|
|
7250
|
+
}
|
|
7251
|
+
function isOnArrival(page) {
|
|
7252
|
+
return (interactions.get(page) ?? 0) === 0;
|
|
7253
|
+
}
|
|
7254
|
+
function noteLocatorCall(page, method, assertion = false) {
|
|
7255
|
+
const arrival = isOnArrival(page);
|
|
7256
|
+
if (!assertion && !NON_INTERACTIONS.has(method)) interactions.set(page, (interactions.get(page) ?? 0) + 1);
|
|
7257
|
+
return arrival;
|
|
7258
|
+
}
|
|
7259
|
+
function pageOf(url) {
|
|
7260
|
+
const key = pageKey(url);
|
|
7261
|
+
if (key === null) return null;
|
|
7262
|
+
try {
|
|
7263
|
+
const origin = new URL(url).origin;
|
|
7264
|
+
return origin && origin !== "null" ? { origin, page: key } : null;
|
|
7265
|
+
} catch {
|
|
7266
|
+
return null;
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
var LocatorPageLog = class {
|
|
7270
|
+
constructor() {
|
|
7271
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
7272
|
+
}
|
|
7273
|
+
/** Record one call. Silently skipped without a call site, off an http(s) page, or past the limits. */
|
|
7274
|
+
record(call) {
|
|
7275
|
+
if (!call.location || !call.locator || call.locator.length > MAX_LOCATOR_CHARS) return;
|
|
7276
|
+
const where = pageOf(call.url);
|
|
7277
|
+
if (!where) return;
|
|
7278
|
+
const key = `${call.location}\0${call.locator}\0${where.origin}\0${where.page}`;
|
|
7279
|
+
const existing = this.entries.get(key);
|
|
7280
|
+
if (existing) {
|
|
7281
|
+
existing.arrival ||= call.arrival;
|
|
7282
|
+
return;
|
|
7283
|
+
}
|
|
7284
|
+
if (this.entries.size >= MAX_LOCATOR_PAGE_ENTRIES) return;
|
|
7285
|
+
this.entries.set(key, { location: call.location, locator: call.locator, ...where, arrival: call.arrival });
|
|
7286
|
+
}
|
|
7287
|
+
get size() {
|
|
7288
|
+
return this.entries.size;
|
|
7289
|
+
}
|
|
7290
|
+
/** The entries in the order the calls first happened. */
|
|
7291
|
+
list() {
|
|
7292
|
+
return [...this.entries.values()];
|
|
7293
|
+
}
|
|
7294
|
+
};
|
|
7295
|
+
|
|
6990
7296
|
// src/internal/probe/interception.ts
|
|
6991
7297
|
var import_node_zlib = require("zlib");
|
|
6992
7298
|
|
|
@@ -7375,6 +7681,59 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
7375
7681
|
return true;
|
|
7376
7682
|
}
|
|
7377
7683
|
|
|
7684
|
+
// src/internal/capture/quiet-capture.ts
|
|
7685
|
+
var import_node_module = require("module");
|
|
7686
|
+
var path20 = __toESM(require("path"));
|
|
7687
|
+
var BOXED_PREFIX_EXPORTS = [
|
|
7688
|
+
[
|
|
7689
|
+
"playwright-core/lib/coreBundle",
|
|
7690
|
+
(mod) => mod?.utils?.setBoxedStackPrefixes
|
|
7691
|
+
],
|
|
7692
|
+
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
7693
|
+
];
|
|
7694
|
+
var boxing;
|
|
7695
|
+
function resolveBoxing() {
|
|
7696
|
+
try {
|
|
7697
|
+
const root = findOwnPackageJson(__dirname)?.root;
|
|
7698
|
+
if (!root) return null;
|
|
7699
|
+
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
7700
|
+
const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
7701
|
+
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
7702
|
+
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
7703
|
+
let set;
|
|
7704
|
+
try {
|
|
7705
|
+
set = pick(requireFromPlaywright(moduleId));
|
|
7706
|
+
} catch {
|
|
7707
|
+
continue;
|
|
7708
|
+
}
|
|
7709
|
+
if (typeof set !== "function") continue;
|
|
7710
|
+
return {
|
|
7711
|
+
set,
|
|
7712
|
+
prefixes: [path20.dirname(playwrightPackageJson), path20.join(root, "dist") + path20.sep]
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
} catch {
|
|
7716
|
+
}
|
|
7717
|
+
return null;
|
|
7718
|
+
}
|
|
7719
|
+
function boxCaptureFrames(extraPrefixes = []) {
|
|
7720
|
+
const first = boxing === void 0;
|
|
7721
|
+
if (first) boxing = resolveBoxing();
|
|
7722
|
+
if (!boxing) return;
|
|
7723
|
+
const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
|
|
7724
|
+
if (!first && added.length === 0) return;
|
|
7725
|
+
boxing.prefixes.push(...added);
|
|
7726
|
+
try {
|
|
7727
|
+
boxing.set([...boxing.prefixes]);
|
|
7728
|
+
} catch {
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
function internalCall(owner, run) {
|
|
7732
|
+
const wrap = owner?._wrapApiCall;
|
|
7733
|
+
if (typeof wrap !== "function") return run();
|
|
7734
|
+
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7378
7737
|
// src/internal/capture/capture-fixtures.ts
|
|
7379
7738
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
7380
7739
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -7425,7 +7784,8 @@ function createSink() {
|
|
|
7425
7784
|
probeItem: null,
|
|
7426
7785
|
probeInterception: null,
|
|
7427
7786
|
pickOffered: false,
|
|
7428
|
-
userPick: null
|
|
7787
|
+
userPick: null,
|
|
7788
|
+
locatorPages: new LocatorPageLog()
|
|
7429
7789
|
};
|
|
7430
7790
|
}
|
|
7431
7791
|
var currentSink = null;
|
|
@@ -7644,7 +8004,7 @@ async function recordPageWindow(sink, page) {
|
|
|
7644
8004
|
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7645
8005
|
if (key && !inventoriedPageKeys.has(key)) {
|
|
7646
8006
|
inventoriedPageKeys.add(key);
|
|
7647
|
-
const content = await readPageInventory(page);
|
|
8007
|
+
const content = await internalCall(page, () => readPageInventory(page));
|
|
7648
8008
|
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7649
8009
|
sink.pageInventories.push({
|
|
7650
8010
|
url: content?.url ?? url,
|
|
@@ -7661,6 +8021,9 @@ async function stashPageState(sink, closing) {
|
|
|
7661
8021
|
if (!page || isPageClosed(page)) return;
|
|
7662
8022
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
7663
8023
|
if (!belongsToClosing) return;
|
|
8024
|
+
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
8025
|
+
}
|
|
8026
|
+
async function readPageBeforeClose(sink, page) {
|
|
7664
8027
|
const vitals = await readWebVitals(page);
|
|
7665
8028
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
7666
8029
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
@@ -7702,7 +8065,10 @@ async function maybeOpenPicker(sink, closing) {
|
|
|
7702
8065
|
console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
|
|
7703
8066
|
return;
|
|
7704
8067
|
}
|
|
7705
|
-
const pick = await
|
|
8068
|
+
const pick = await internalCall(
|
|
8069
|
+
page,
|
|
8070
|
+
() => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
|
|
8071
|
+
);
|
|
7706
8072
|
if (!pick) return;
|
|
7707
8073
|
sink.userPick = pick;
|
|
7708
8074
|
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
@@ -7721,12 +8087,8 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
7721
8087
|
// special-cased logic in the probe, so add them explicitly).
|
|
7722
8088
|
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
7723
8089
|
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
7724
|
-
// anchors step and generateAnchoredAlternatives both need them).
|
|
7725
|
-
|
|
7726
|
-
// same pass — so asking for it here is free and settles the accessible name
|
|
7727
|
-
// of a labeled field without a second round trip (`exactAccessibleName`).
|
|
7728
|
-
includeStructural: true,
|
|
7729
|
-
includeLabelText: true
|
|
8090
|
+
// anchors step and generateAnchoredAlternatives both need them).
|
|
8091
|
+
includeStructural: true
|
|
7730
8092
|
};
|
|
7731
8093
|
var PROBE_GLOBAL = "__piwiProbeElement";
|
|
7732
8094
|
var PROBE_INIT_SCRIPT = `(() => {
|
|
@@ -7737,15 +8099,18 @@ var PROBE_INIT_SCRIPT = `(() => {
|
|
|
7737
8099
|
var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
|
|
7738
8100
|
function probeElement(page, target) {
|
|
7739
8101
|
const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
7740
|
-
if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource
|
|
7741
|
-
return
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
8102
|
+
if (!page || PROBE_UNSEEDED_PAGES.has(page)) return internalCall(page, shipSource);
|
|
8103
|
+
return internalCall(
|
|
8104
|
+
page,
|
|
8105
|
+
() => target.evaluate((el, name) => {
|
|
8106
|
+
const seeded = globalThis[name];
|
|
8107
|
+
return typeof seeded === "function" ? seeded(el) : null;
|
|
8108
|
+
}, PROBE_GLOBAL).then((attrs) => {
|
|
8109
|
+
if (attrs) return attrs;
|
|
8110
|
+
PROBE_UNSEEDED_PAGES.add(page);
|
|
8111
|
+
return shipSource();
|
|
8112
|
+
})
|
|
8113
|
+
);
|
|
7749
8114
|
}
|
|
7750
8115
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
7751
8116
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
@@ -7796,7 +8161,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
7796
8161
|
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
7797
8162
|
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
7798
8163
|
const exactName = exactAccessibleName(attrs, role);
|
|
7799
|
-
const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
|
|
8164
|
+
const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, 500)) : null;
|
|
7800
8165
|
const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
|
|
7801
8166
|
sink.capturedLocators[seq] = {
|
|
7802
8167
|
location: callerLocation,
|
|
@@ -7824,15 +8189,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
7824
8189
|
})();
|
|
7825
8190
|
sink.capturePromises.push(resolveAttrs);
|
|
7826
8191
|
}
|
|
8192
|
+
function callMethod(target, prop, args) {
|
|
8193
|
+
return target[prop](...args);
|
|
8194
|
+
}
|
|
8195
|
+
function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
8196
|
+
try {
|
|
8197
|
+
sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
|
|
8198
|
+
} catch {
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
7827
8201
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
7828
8202
|
return new Proxy(locator, {
|
|
7829
8203
|
get(target, prop) {
|
|
7830
8204
|
const original = Reflect.get(target, prop);
|
|
7831
8205
|
if (typeof original !== "function") return original;
|
|
7832
|
-
const fn = original;
|
|
7833
8206
|
if (CHAIN_METHOD_SET.has(prop)) {
|
|
7834
8207
|
return (...args) => {
|
|
7835
|
-
const next =
|
|
8208
|
+
const next = callMethod(target, prop, args);
|
|
7836
8209
|
if (LOCATOR_CREATING_CHAINS.has(prop)) {
|
|
7837
8210
|
return wrapLocator(page, next, String(prop), args);
|
|
7838
8211
|
}
|
|
@@ -7844,11 +8217,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7844
8217
|
const sink = currentSink;
|
|
7845
8218
|
const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
|
|
7846
8219
|
const isNot = Boolean(callArgs[1]?.isNot);
|
|
8220
|
+
const callerLocation = sink ? captureCallerLocation() : null;
|
|
8221
|
+
if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
|
|
7847
8222
|
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
7848
|
-
return
|
|
8223
|
+
return callMethod(target, prop, callArgs);
|
|
7849
8224
|
}
|
|
7850
8225
|
sink.lastActivePage = page;
|
|
7851
|
-
const callerLocation = captureCallerLocation();
|
|
7852
8226
|
const used = {
|
|
7853
8227
|
method: originMethod,
|
|
7854
8228
|
args: originArgs,
|
|
@@ -7860,7 +8234,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7860
8234
|
seq = sink.capturedLocators.length;
|
|
7861
8235
|
sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
|
|
7862
8236
|
}
|
|
7863
|
-
const result = await
|
|
8237
|
+
const result = await callMethod(target, prop, callArgs);
|
|
7864
8238
|
const matches = result?.matches;
|
|
7865
8239
|
if (matches === true && !alreadyProbed) {
|
|
7866
8240
|
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
@@ -7874,9 +8248,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7874
8248
|
if (!ACTION_METHOD_SET.has(prop)) return original;
|
|
7875
8249
|
return async (...callArgs) => {
|
|
7876
8250
|
const sink = currentSink;
|
|
7877
|
-
if (!sink) return
|
|
8251
|
+
if (!sink) return callMethod(target, prop, callArgs);
|
|
7878
8252
|
sink.lastActivePage = page;
|
|
7879
8253
|
const callerLocation = captureCallerLocation();
|
|
8254
|
+
recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
|
|
7880
8255
|
const used = {
|
|
7881
8256
|
method: originMethod,
|
|
7882
8257
|
args: originArgs,
|
|
@@ -7895,7 +8270,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7895
8270
|
}
|
|
7896
8271
|
let result;
|
|
7897
8272
|
try {
|
|
7898
|
-
result = await
|
|
8273
|
+
result = await callMethod(target, prop, callArgs);
|
|
7899
8274
|
} catch (error) {
|
|
7900
8275
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
7901
8276
|
throw error;
|
|
@@ -7914,11 +8289,10 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
7914
8289
|
get(target, prop) {
|
|
7915
8290
|
const original = Reflect.get(target, prop);
|
|
7916
8291
|
if (typeof original !== "function") return original;
|
|
7917
|
-
const fn = original;
|
|
7918
8292
|
if (!LOCATOR_METHOD_SET.has(prop)) return original;
|
|
7919
8293
|
return (...args) => {
|
|
7920
8294
|
if (currentSink) currentSink.lastActivePage = page;
|
|
7921
|
-
return wrapLocator(page,
|
|
8295
|
+
return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
|
|
7922
8296
|
};
|
|
7923
8297
|
}
|
|
7924
8298
|
});
|
|
@@ -7965,7 +8339,13 @@ function instrumentPage(page) {
|
|
|
7965
8339
|
};
|
|
7966
8340
|
}
|
|
7967
8341
|
if (typeof page.on === "function") {
|
|
7968
|
-
page.on("framenavigated", () =>
|
|
8342
|
+
page.on("framenavigated", (frame) => {
|
|
8343
|
+
PROBE_UNSEEDED_PAGES.delete(page);
|
|
8344
|
+
try {
|
|
8345
|
+
if (frame === page.mainFrame()) noteNavigation(page);
|
|
8346
|
+
} catch {
|
|
8347
|
+
}
|
|
8348
|
+
});
|
|
7969
8349
|
}
|
|
7970
8350
|
}
|
|
7971
8351
|
if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
@@ -8104,6 +8484,12 @@ function patchBrowser(browser) {
|
|
|
8104
8484
|
};
|
|
8105
8485
|
}
|
|
8106
8486
|
}
|
|
8487
|
+
function readRootAria(page) {
|
|
8488
|
+
return internalCall(page, () => ariaSnapshotBestEffort(page.locator(":root")));
|
|
8489
|
+
}
|
|
8490
|
+
function readRootAriaJson(page) {
|
|
8491
|
+
return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
|
|
8492
|
+
}
|
|
8107
8493
|
async function flushSink(sink, testInfo) {
|
|
8108
8494
|
await Promise.allSettled(sink.pendingHandlers);
|
|
8109
8495
|
let drainDeadline;
|
|
@@ -8126,15 +8512,21 @@ async function flushSink(sink, testInfo) {
|
|
|
8126
8512
|
body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
|
|
8127
8513
|
});
|
|
8128
8514
|
}
|
|
8515
|
+
if (sink.locatorPages.size > 0) {
|
|
8516
|
+
await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
|
|
8517
|
+
contentType: "application/json",
|
|
8518
|
+
body: Buffer.from(JSON.stringify(sink.locatorPages.list()))
|
|
8519
|
+
});
|
|
8520
|
+
}
|
|
8129
8521
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
8130
8522
|
try {
|
|
8131
|
-
const snapshot = (pageReadable ? await
|
|
8523
|
+
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8132
8524
|
if (snapshot) {
|
|
8133
8525
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8134
8526
|
contentType: "text/plain",
|
|
8135
8527
|
body: snapshot
|
|
8136
8528
|
});
|
|
8137
|
-
const snapshotJson = (pageReadable ? await
|
|
8529
|
+
const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8138
8530
|
if (snapshotJson) {
|
|
8139
8531
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8140
8532
|
contentType: "application/json",
|
|
@@ -8159,13 +8551,13 @@ async function flushSink(sink, testInfo) {
|
|
|
8159
8551
|
}
|
|
8160
8552
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
8161
8553
|
try {
|
|
8162
|
-
const snapshot = (pageReadable ? await
|
|
8554
|
+
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8163
8555
|
if (snapshot) {
|
|
8164
8556
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8165
8557
|
contentType: "text/plain",
|
|
8166
8558
|
body: snapshot
|
|
8167
8559
|
});
|
|
8168
|
-
const snapshotJson = (pageReadable ? await
|
|
8560
|
+
const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8169
8561
|
if (snapshotJson) {
|
|
8170
8562
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8171
8563
|
contentType: "application/json",
|
|
@@ -8211,7 +8603,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8211
8603
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
8212
8604
|
});
|
|
8213
8605
|
}
|
|
8214
|
-
const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
|
|
8606
|
+
const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
8215
8607
|
if (webVitals) {
|
|
8216
8608
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
8217
8609
|
contentType: "application/json",
|
|
@@ -8219,7 +8611,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8219
8611
|
});
|
|
8220
8612
|
}
|
|
8221
8613
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
8222
|
-
const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
|
|
8614
|
+
const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
8223
8615
|
if (pageState) {
|
|
8224
8616
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
8225
8617
|
contentType: "application/json",
|
|
@@ -8260,6 +8652,7 @@ var piwiFixtures = {
|
|
|
8260
8652
|
// requested, so suites that never destructure `page` are still captured.
|
|
8261
8653
|
piwiCapture: [
|
|
8262
8654
|
async ({}, use, testInfo) => {
|
|
8655
|
+
boxCaptureFrames();
|
|
8263
8656
|
const sink = createSink();
|
|
8264
8657
|
sink.testInfo = testInfo;
|
|
8265
8658
|
if (isProbeMode())
|
|
@@ -8304,12 +8697,12 @@ function extendPiwiFixtures(test) {
|
|
|
8304
8697
|
|
|
8305
8698
|
// src/internal/ai/ai-fixtures.ts
|
|
8306
8699
|
var fs18 = __toESM(require("fs"));
|
|
8307
|
-
var
|
|
8700
|
+
var path23 = __toESM(require("path"));
|
|
8308
8701
|
var import_test = require("@playwright/test");
|
|
8309
8702
|
|
|
8310
8703
|
// src/internal/ai/artifact.ts
|
|
8311
8704
|
var fs17 = __toESM(require("fs"));
|
|
8312
|
-
var
|
|
8705
|
+
var path21 = __toESM(require("path"));
|
|
8313
8706
|
var ARTIFACT_VERSION = 1;
|
|
8314
8707
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
8315
8708
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -8413,7 +8806,7 @@ function writeEntry(file, entry) {
|
|
|
8413
8806
|
const canonical = serializeEntry(entry);
|
|
8414
8807
|
return withEntryLock(file, () => {
|
|
8415
8808
|
if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
8416
|
-
fs17.mkdirSync(
|
|
8809
|
+
fs17.mkdirSync(path21.dirname(file), { recursive: true });
|
|
8417
8810
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8418
8811
|
fs17.writeFileSync(tmp, canonical);
|
|
8419
8812
|
fs17.renameSync(tmp, file);
|
|
@@ -8422,7 +8815,7 @@ function writeEntry(file, entry) {
|
|
|
8422
8815
|
}
|
|
8423
8816
|
function withEntryLock(file, fn) {
|
|
8424
8817
|
const lock = `${file}.lock`;
|
|
8425
|
-
fs17.mkdirSync(
|
|
8818
|
+
fs17.mkdirSync(path21.dirname(file), { recursive: true });
|
|
8426
8819
|
const deadline = Date.now() + 5e3;
|
|
8427
8820
|
for (; ; ) {
|
|
8428
8821
|
try {
|
|
@@ -8451,7 +8844,7 @@ function withEntryLock(file, fn) {
|
|
|
8451
8844
|
|
|
8452
8845
|
// src/internal/ai/keys.ts
|
|
8453
8846
|
var crypto3 = __toESM(require("crypto"));
|
|
8454
|
-
var
|
|
8847
|
+
var path22 = __toESM(require("path"));
|
|
8455
8848
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
8456
8849
|
function normalizeTemplate(template) {
|
|
8457
8850
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -8465,13 +8858,13 @@ function slug(text, maxLength = 40) {
|
|
|
8465
8858
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
8466
8859
|
}
|
|
8467
8860
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
8468
|
-
return
|
|
8861
|
+
return path22.join(path22.dirname(specFile), dir, path22.basename(specFile));
|
|
8469
8862
|
}
|
|
8470
8863
|
function entryPath(params) {
|
|
8471
8864
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
8472
8865
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
8473
8866
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
8474
|
-
return
|
|
8867
|
+
return path22.join(entryDir(params.specFile, params.dir), name);
|
|
8475
8868
|
}
|
|
8476
8869
|
function parseLocation(location) {
|
|
8477
8870
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -8925,7 +9318,7 @@ function testIdentity(testInfo) {
|
|
|
8925
9318
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
8926
9319
|
}
|
|
8927
9320
|
function relativeToCwd(file) {
|
|
8928
|
-
return
|
|
9321
|
+
return path23.relative(process.cwd(), file).split(path23.sep).join("/");
|
|
8929
9322
|
}
|
|
8930
9323
|
function recordIntents(intents, entry) {
|
|
8931
9324
|
const add = (locator, kind) => {
|