@piwitests/reporter 0.39.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/index.d.ts +3 -2
- package/dist/index.js +469 -81
- 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",
|
|
@@ -2146,11 +2148,14 @@ var MAX_STEP_PARAM_VALUE_CHARS = 200;
|
|
|
2146
2148
|
function capStepValue(value, max) {
|
|
2147
2149
|
return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
|
|
2148
2150
|
}
|
|
2151
|
+
var HTTP_METHOD_TITLE_RE = /^(?:get|post|put|patch|delete|head|options|fetch)$/i;
|
|
2149
2152
|
function categorizeStep(title, pwCategory, params) {
|
|
2150
2153
|
if (!title) return "other";
|
|
2151
|
-
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
2154
|
+
if (pwCategory === "hook" || pwCategory === "fixture" || pwCategory === "test.step") return pwCategory;
|
|
2152
2155
|
if (pwCategory === "expect") return "assertion";
|
|
2156
|
+
if (pwCategory === "test.attach") return "attach";
|
|
2153
2157
|
const lower = title.toLowerCase();
|
|
2158
|
+
if (HTTP_METHOD_TITLE_RE.test(title.trim())) return "api";
|
|
2154
2159
|
if (lower.startsWith("wait for") || lower.startsWith("locator.waitfor") || lower.startsWith("page.waitfor") || lower.startsWith("frame.waitfor"))
|
|
2155
2160
|
return "wait";
|
|
2156
2161
|
if (typeof params?.url === "string" && pwCategory !== "expect") return "navigation";
|
|
@@ -2188,13 +2193,14 @@ function normalizeStepParams(raw) {
|
|
|
2188
2193
|
}
|
|
2189
2194
|
return count > 0 ? out : void 0;
|
|
2190
2195
|
}
|
|
2191
|
-
function flattenSteps(steps) {
|
|
2196
|
+
function flattenSteps(steps, depth = 0) {
|
|
2192
2197
|
const result = [];
|
|
2193
2198
|
for (const step of steps) {
|
|
2194
2199
|
const flat = {
|
|
2195
2200
|
title: step.title,
|
|
2196
2201
|
duration: step.duration,
|
|
2197
|
-
category: categorizeStep(step.title, step.category, step.params)
|
|
2202
|
+
category: categorizeStep(step.title, step.category, step.params),
|
|
2203
|
+
depth
|
|
2198
2204
|
};
|
|
2199
2205
|
if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
|
|
2200
2206
|
const params = normalizeStepParams(step.params);
|
|
@@ -2206,7 +2212,7 @@ function flattenSteps(steps) {
|
|
|
2206
2212
|
if (step.location) flat.location = `${step.location.file}:${step.location.line}:${step.location.column}`;
|
|
2207
2213
|
if (step.startTime) flat.startTime = step.startTime instanceof Date ? step.startTime.getTime() : step.startTime;
|
|
2208
2214
|
result.push(flat);
|
|
2209
|
-
if (step.steps?.length > 0) result.push(...flattenSteps(step.steps));
|
|
2215
|
+
if (step.steps?.length > 0) result.push(...flattenSteps(step.steps, depth + 1));
|
|
2210
2216
|
}
|
|
2211
2217
|
return result;
|
|
2212
2218
|
}
|
|
@@ -2318,7 +2324,8 @@ function findOwnPackageJson(fromDir) {
|
|
|
2318
2324
|
const pkgPath = path8.resolve(fromDir, offset, "package.json");
|
|
2319
2325
|
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
2320
2326
|
const { name, version } = pkg;
|
|
2321
|
-
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
2327
|
+
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
2328
|
+
return { version, root: path8.dirname(pkgPath) };
|
|
2322
2329
|
} catch {
|
|
2323
2330
|
}
|
|
2324
2331
|
}
|
|
@@ -2772,8 +2779,8 @@ function parseRouteKey(routeKey) {
|
|
|
2772
2779
|
return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
|
|
2773
2780
|
}
|
|
2774
2781
|
function patternToRegex(pattern) {
|
|
2775
|
-
const
|
|
2776
|
-
const escaped =
|
|
2782
|
+
const path24 = pattern.split("?")[0] ?? pattern;
|
|
2783
|
+
const escaped = path24.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2777
2784
|
return new RegExp(`^${escaped}$`);
|
|
2778
2785
|
}
|
|
2779
2786
|
function suitePathEqual(a, b) {
|
|
@@ -2796,13 +2803,13 @@ function matchProbeItem(plan, test) {
|
|
|
2796
2803
|
function requestMatchesRoute(method, url, routeKey) {
|
|
2797
2804
|
const { method: rm, pattern } = parseRouteKey(routeKey);
|
|
2798
2805
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
2799
|
-
let
|
|
2806
|
+
let path24;
|
|
2800
2807
|
try {
|
|
2801
|
-
|
|
2808
|
+
path24 = new URL(url).pathname;
|
|
2802
2809
|
} catch {
|
|
2803
|
-
|
|
2810
|
+
path24 = url.split("?")[0] ?? url;
|
|
2804
2811
|
}
|
|
2805
|
-
return patternToRegex(pattern).test(
|
|
2812
|
+
return patternToRegex(pattern).test(path24);
|
|
2806
2813
|
}
|
|
2807
2814
|
function createProbeState() {
|
|
2808
2815
|
return { navigated: false, matches: 0 };
|
|
@@ -2824,10 +2831,10 @@ function isProbeMode() {
|
|
|
2824
2831
|
function loadPlan() {
|
|
2825
2832
|
if (planCache !== void 0) return planCache;
|
|
2826
2833
|
planCache = null;
|
|
2827
|
-
const
|
|
2828
|
-
if (
|
|
2834
|
+
const path24 = process.env[PIWI_PROBE_ENV.plan];
|
|
2835
|
+
if (path24) {
|
|
2829
2836
|
try {
|
|
2830
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
2837
|
+
planCache = JSON.parse(fs14.readFileSync(path24, "utf8"));
|
|
2831
2838
|
} catch {
|
|
2832
2839
|
planCache = null;
|
|
2833
2840
|
}
|
|
@@ -2849,10 +2856,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
2849
2856
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2850
2857
|
}
|
|
2851
2858
|
function recordProbeOutcome(line) {
|
|
2852
|
-
const
|
|
2853
|
-
if (!
|
|
2859
|
+
const path24 = process.env[PIWI_PROBE_ENV.results];
|
|
2860
|
+
if (!path24) return;
|
|
2854
2861
|
try {
|
|
2855
|
-
fs14.appendFileSync(
|
|
2862
|
+
fs14.appendFileSync(path24, `${JSON.stringify(line)}
|
|
2856
2863
|
`);
|
|
2857
2864
|
} catch {
|
|
2858
2865
|
}
|
|
@@ -4089,12 +4096,20 @@ function readUrl(text, callLog, received) {
|
|
|
4089
4096
|
}
|
|
4090
4097
|
return null;
|
|
4091
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
|
+
}
|
|
4092
4107
|
function parsePlaywrightError(raw, context) {
|
|
4093
4108
|
const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
|
|
4094
4109
|
const text = withoutStackFrames(clean);
|
|
4095
4110
|
const messageHead = extractMessageHead(clean);
|
|
4096
|
-
const
|
|
4097
|
-
const errorName = ERROR_NAME_RE.exec(
|
|
4111
|
+
const firstLine2 = messageHead.split("\n")[0] ?? "";
|
|
4112
|
+
const errorName = ERROR_NAME_RE.exec(firstLine2)?.[1] ?? null;
|
|
4098
4113
|
const strict = STRICT_RE.exec(text);
|
|
4099
4114
|
const call = CALL_RE.exec(text);
|
|
4100
4115
|
const callLog = readCallLog(text);
|
|
@@ -4164,12 +4179,134 @@ function parsePlaywrightError(raw, context) {
|
|
|
4164
4179
|
lastCallLogLine: callLog.lastLine,
|
|
4165
4180
|
lastStateLine: callLog.stateLine,
|
|
4166
4181
|
messageHead,
|
|
4182
|
+
customMessage: isAssertion ? readCustomMessage(messageHead) : null,
|
|
4167
4183
|
topFrame: frame ? `${frame.file}:${frame.line}` : null,
|
|
4168
4184
|
isNavigationFailure,
|
|
4169
4185
|
isLocatorResolutionFailure
|
|
4170
4186
|
};
|
|
4171
4187
|
}
|
|
4172
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
|
+
|
|
4173
4310
|
// ../core/src/describe-failure.ts
|
|
4174
4311
|
var HEADLINE_MAX_CHARS = 120;
|
|
4175
4312
|
var VALUE_MAX_CHARS = 40;
|
|
@@ -4516,7 +4653,27 @@ function buildCrash(parsed, opts) {
|
|
|
4516
4653
|
}
|
|
4517
4654
|
return line.text(what);
|
|
4518
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
|
+
}
|
|
4519
4673
|
function build(parsed, opts, ctx) {
|
|
4674
|
+
return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
|
|
4675
|
+
}
|
|
4676
|
+
function buildLine(parsed, opts, ctx) {
|
|
4520
4677
|
switch (parsed.kind) {
|
|
4521
4678
|
case "action-timeout":
|
|
4522
4679
|
return buildActionTimeout(parsed, opts);
|
|
@@ -4602,15 +4759,36 @@ function describeFailureText(raw, ctx) {
|
|
|
4602
4759
|
if (!raw || !raw.trim()) return null;
|
|
4603
4760
|
return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
|
|
4604
4761
|
}
|
|
4605
|
-
function
|
|
4762
|
+
function headlineStep(steps) {
|
|
4606
4763
|
if (!steps || steps.length === 0) return null;
|
|
4607
|
-
const
|
|
4608
|
-
|
|
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
|
+
};
|
|
4609
4787
|
}
|
|
4610
4788
|
|
|
4611
4789
|
// src/internal/support/failure-links.ts
|
|
4612
4790
|
function failureHeadline(error, steps) {
|
|
4613
|
-
return describeFailureText(error,
|
|
4791
|
+
return describeFailureText(error, stepHeadlineContext(steps, error))?.headline ?? null;
|
|
4614
4792
|
}
|
|
4615
4793
|
function caseLocateUrl(serverUrl, runId, test) {
|
|
4616
4794
|
const params = [
|
|
@@ -4946,6 +5124,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
4946
5124
|
} catch {
|
|
4947
5125
|
}
|
|
4948
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
|
+
}
|
|
4949
5134
|
}
|
|
4950
5135
|
switch (status) {
|
|
4951
5136
|
case "passed":
|
|
@@ -5143,7 +5328,30 @@ var import_node_zlib2 = require("zlib");
|
|
|
5143
5328
|
|
|
5144
5329
|
// ../picker-dom/src/probe.ts
|
|
5145
5330
|
function probeElementAttrs(el, arg) {
|
|
5146
|
-
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
|
+
};
|
|
5147
5355
|
const attrMap = {};
|
|
5148
5356
|
for (const key of keep) {
|
|
5149
5357
|
const v = el.getAttribute(key) ?? el[key];
|
|
@@ -5223,10 +5431,18 @@ function probeElementAttrs(el, arg) {
|
|
|
5223
5431
|
const targetRole = roleOf(el);
|
|
5224
5432
|
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
5225
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;
|
|
5226
5438
|
const al = n.getAttribute("aria-label");
|
|
5227
5439
|
if (al) return al;
|
|
5228
|
-
|
|
5229
|
-
|
|
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
|
+
}
|
|
5230
5446
|
return n.getAttribute("title") || n.getAttribute("placeholder") || null;
|
|
5231
5447
|
};
|
|
5232
5448
|
const targetName = nameOf(el);
|
|
@@ -5418,16 +5634,7 @@ function probeElementAttrs(el, arg) {
|
|
|
5418
5634
|
}
|
|
5419
5635
|
}
|
|
5420
5636
|
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
5421
|
-
|
|
5422
|
-
if (includeLabelText) {
|
|
5423
|
-
labelText = null;
|
|
5424
|
-
try {
|
|
5425
|
-
const label = hasLabel ? el.labels[0] : null;
|
|
5426
|
-
labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
|
|
5427
|
-
} catch {
|
|
5428
|
-
labelText = null;
|
|
5429
|
-
}
|
|
5430
|
-
}
|
|
5637
|
+
const labelText = labelTextOf(el);
|
|
5431
5638
|
return {
|
|
5432
5639
|
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
5433
5640
|
attributes: attrMap,
|
|
@@ -5439,7 +5646,7 @@ function probeElementAttrs(el, arg) {
|
|
|
5439
5646
|
y: Math.round(r.y + r.height / 2)
|
|
5440
5647
|
},
|
|
5441
5648
|
hasLabel,
|
|
5442
|
-
|
|
5649
|
+
labelText,
|
|
5443
5650
|
selectorCounts,
|
|
5444
5651
|
...includeStructural ? { rolePosition, ancestors } : {}
|
|
5445
5652
|
};
|
|
@@ -6410,9 +6617,15 @@ function generateAlternatives(attrs) {
|
|
|
6410
6617
|
}
|
|
6411
6618
|
function approximateAccessibleName(attrs) {
|
|
6412
6619
|
const a = attrs.attributes;
|
|
6620
|
+
const labelText = attrs.labelText || null;
|
|
6621
|
+
if (a["aria-labelledby"] && labelText) return labelText;
|
|
6413
6622
|
const ariaLabel = a["aria-label"];
|
|
6414
6623
|
if (ariaLabel) return ariaLabel;
|
|
6415
|
-
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
|
+
}
|
|
6416
6629
|
const title = a["title"];
|
|
6417
6630
|
if (title) return title;
|
|
6418
6631
|
const placeholder = a["placeholder"];
|
|
@@ -6992,6 +7205,94 @@ function inventoryPageKey(url) {
|
|
|
6992
7205
|
}
|
|
6993
7206
|
}
|
|
6994
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
|
+
|
|
6995
7296
|
// src/internal/probe/interception.ts
|
|
6996
7297
|
var import_node_zlib = require("zlib");
|
|
6997
7298
|
|
|
@@ -7380,6 +7681,59 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
7380
7681
|
return true;
|
|
7381
7682
|
}
|
|
7382
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
|
+
|
|
7383
7737
|
// src/internal/capture/capture-fixtures.ts
|
|
7384
7738
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
7385
7739
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -7430,7 +7784,8 @@ function createSink() {
|
|
|
7430
7784
|
probeItem: null,
|
|
7431
7785
|
probeInterception: null,
|
|
7432
7786
|
pickOffered: false,
|
|
7433
|
-
userPick: null
|
|
7787
|
+
userPick: null,
|
|
7788
|
+
locatorPages: new LocatorPageLog()
|
|
7434
7789
|
};
|
|
7435
7790
|
}
|
|
7436
7791
|
var currentSink = null;
|
|
@@ -7649,7 +8004,7 @@ async function recordPageWindow(sink, page) {
|
|
|
7649
8004
|
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7650
8005
|
if (key && !inventoriedPageKeys.has(key)) {
|
|
7651
8006
|
inventoriedPageKeys.add(key);
|
|
7652
|
-
const content = await readPageInventory(page);
|
|
8007
|
+
const content = await internalCall(page, () => readPageInventory(page));
|
|
7653
8008
|
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
7654
8009
|
sink.pageInventories.push({
|
|
7655
8010
|
url: content?.url ?? url,
|
|
@@ -7666,6 +8021,9 @@ async function stashPageState(sink, closing) {
|
|
|
7666
8021
|
if (!page || isPageClosed(page)) return;
|
|
7667
8022
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
7668
8023
|
if (!belongsToClosing) return;
|
|
8024
|
+
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
8025
|
+
}
|
|
8026
|
+
async function readPageBeforeClose(sink, page) {
|
|
7669
8027
|
const vitals = await readWebVitals(page);
|
|
7670
8028
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
7671
8029
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
@@ -7707,7 +8065,10 @@ async function maybeOpenPicker(sink, closing) {
|
|
|
7707
8065
|
console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
|
|
7708
8066
|
return;
|
|
7709
8067
|
}
|
|
7710
|
-
const pick = await
|
|
8068
|
+
const pick = await internalCall(
|
|
8069
|
+
page,
|
|
8070
|
+
() => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
|
|
8071
|
+
);
|
|
7711
8072
|
if (!pick) return;
|
|
7712
8073
|
sink.userPick = pick;
|
|
7713
8074
|
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
@@ -7726,12 +8087,8 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
7726
8087
|
// special-cased logic in the probe, so add them explicitly).
|
|
7727
8088
|
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
7728
8089
|
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
7729
|
-
// anchors step and generateAnchoredAlternatives both need them).
|
|
7730
|
-
|
|
7731
|
-
// same pass — so asking for it here is free and settles the accessible name
|
|
7732
|
-
// of a labeled field without a second round trip (`exactAccessibleName`).
|
|
7733
|
-
includeStructural: true,
|
|
7734
|
-
includeLabelText: true
|
|
8090
|
+
// anchors step and generateAnchoredAlternatives both need them).
|
|
8091
|
+
includeStructural: true
|
|
7735
8092
|
};
|
|
7736
8093
|
var PROBE_GLOBAL = "__piwiProbeElement";
|
|
7737
8094
|
var PROBE_INIT_SCRIPT = `(() => {
|
|
@@ -7742,15 +8099,18 @@ var PROBE_INIT_SCRIPT = `(() => {
|
|
|
7742
8099
|
var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
|
|
7743
8100
|
function probeElement(page, target) {
|
|
7744
8101
|
const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
7745
|
-
if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource
|
|
7746
|
-
return
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
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
|
+
);
|
|
7754
8114
|
}
|
|
7755
8115
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
7756
8116
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
@@ -7801,7 +8161,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
7801
8161
|
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
7802
8162
|
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
7803
8163
|
const exactName = exactAccessibleName(attrs, role);
|
|
7804
|
-
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;
|
|
7805
8165
|
const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
|
|
7806
8166
|
sink.capturedLocators[seq] = {
|
|
7807
8167
|
location: callerLocation,
|
|
@@ -7829,15 +8189,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
7829
8189
|
})();
|
|
7830
8190
|
sink.capturePromises.push(resolveAttrs);
|
|
7831
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
|
+
}
|
|
7832
8201
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
7833
8202
|
return new Proxy(locator, {
|
|
7834
8203
|
get(target, prop) {
|
|
7835
8204
|
const original = Reflect.get(target, prop);
|
|
7836
8205
|
if (typeof original !== "function") return original;
|
|
7837
|
-
const fn = original;
|
|
7838
8206
|
if (CHAIN_METHOD_SET.has(prop)) {
|
|
7839
8207
|
return (...args) => {
|
|
7840
|
-
const next =
|
|
8208
|
+
const next = callMethod(target, prop, args);
|
|
7841
8209
|
if (LOCATOR_CREATING_CHAINS.has(prop)) {
|
|
7842
8210
|
return wrapLocator(page, next, String(prop), args);
|
|
7843
8211
|
}
|
|
@@ -7849,11 +8217,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7849
8217
|
const sink = currentSink;
|
|
7850
8218
|
const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
|
|
7851
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));
|
|
7852
8222
|
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
7853
|
-
return
|
|
8223
|
+
return callMethod(target, prop, callArgs);
|
|
7854
8224
|
}
|
|
7855
8225
|
sink.lastActivePage = page;
|
|
7856
|
-
const callerLocation = captureCallerLocation();
|
|
7857
8226
|
const used = {
|
|
7858
8227
|
method: originMethod,
|
|
7859
8228
|
args: originArgs,
|
|
@@ -7865,7 +8234,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7865
8234
|
seq = sink.capturedLocators.length;
|
|
7866
8235
|
sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
|
|
7867
8236
|
}
|
|
7868
|
-
const result = await
|
|
8237
|
+
const result = await callMethod(target, prop, callArgs);
|
|
7869
8238
|
const matches = result?.matches;
|
|
7870
8239
|
if (matches === true && !alreadyProbed) {
|
|
7871
8240
|
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
@@ -7879,9 +8248,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7879
8248
|
if (!ACTION_METHOD_SET.has(prop)) return original;
|
|
7880
8249
|
return async (...callArgs) => {
|
|
7881
8250
|
const sink = currentSink;
|
|
7882
|
-
if (!sink) return
|
|
8251
|
+
if (!sink) return callMethod(target, prop, callArgs);
|
|
7883
8252
|
sink.lastActivePage = page;
|
|
7884
8253
|
const callerLocation = captureCallerLocation();
|
|
8254
|
+
recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
|
|
7885
8255
|
const used = {
|
|
7886
8256
|
method: originMethod,
|
|
7887
8257
|
args: originArgs,
|
|
@@ -7900,7 +8270,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
7900
8270
|
}
|
|
7901
8271
|
let result;
|
|
7902
8272
|
try {
|
|
7903
|
-
result = await
|
|
8273
|
+
result = await callMethod(target, prop, callArgs);
|
|
7904
8274
|
} catch (error) {
|
|
7905
8275
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
7906
8276
|
throw error;
|
|
@@ -7919,11 +8289,10 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
7919
8289
|
get(target, prop) {
|
|
7920
8290
|
const original = Reflect.get(target, prop);
|
|
7921
8291
|
if (typeof original !== "function") return original;
|
|
7922
|
-
const fn = original;
|
|
7923
8292
|
if (!LOCATOR_METHOD_SET.has(prop)) return original;
|
|
7924
8293
|
return (...args) => {
|
|
7925
8294
|
if (currentSink) currentSink.lastActivePage = page;
|
|
7926
|
-
return wrapLocator(page,
|
|
8295
|
+
return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
|
|
7927
8296
|
};
|
|
7928
8297
|
}
|
|
7929
8298
|
});
|
|
@@ -7970,7 +8339,13 @@ function instrumentPage(page) {
|
|
|
7970
8339
|
};
|
|
7971
8340
|
}
|
|
7972
8341
|
if (typeof page.on === "function") {
|
|
7973
|
-
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
|
+
});
|
|
7974
8349
|
}
|
|
7975
8350
|
}
|
|
7976
8351
|
if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
@@ -8109,6 +8484,12 @@ function patchBrowser(browser) {
|
|
|
8109
8484
|
};
|
|
8110
8485
|
}
|
|
8111
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
|
+
}
|
|
8112
8493
|
async function flushSink(sink, testInfo) {
|
|
8113
8494
|
await Promise.allSettled(sink.pendingHandlers);
|
|
8114
8495
|
let drainDeadline;
|
|
@@ -8131,15 +8512,21 @@ async function flushSink(sink, testInfo) {
|
|
|
8131
8512
|
body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
|
|
8132
8513
|
});
|
|
8133
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
|
+
}
|
|
8134
8521
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
8135
8522
|
try {
|
|
8136
|
-
const snapshot = (pageReadable ? await
|
|
8523
|
+
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8137
8524
|
if (snapshot) {
|
|
8138
8525
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8139
8526
|
contentType: "text/plain",
|
|
8140
8527
|
body: snapshot
|
|
8141
8528
|
});
|
|
8142
|
-
const snapshotJson = (pageReadable ? await
|
|
8529
|
+
const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8143
8530
|
if (snapshotJson) {
|
|
8144
8531
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8145
8532
|
contentType: "application/json",
|
|
@@ -8164,13 +8551,13 @@ async function flushSink(sink, testInfo) {
|
|
|
8164
8551
|
}
|
|
8165
8552
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
8166
8553
|
try {
|
|
8167
|
-
const snapshot = (pageReadable ? await
|
|
8554
|
+
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8168
8555
|
if (snapshot) {
|
|
8169
8556
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8170
8557
|
contentType: "text/plain",
|
|
8171
8558
|
body: snapshot
|
|
8172
8559
|
});
|
|
8173
|
-
const snapshotJson = (pageReadable ? await
|
|
8560
|
+
const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8174
8561
|
if (snapshotJson) {
|
|
8175
8562
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8176
8563
|
contentType: "application/json",
|
|
@@ -8216,7 +8603,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8216
8603
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
8217
8604
|
});
|
|
8218
8605
|
}
|
|
8219
|
-
const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
|
|
8606
|
+
const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
8220
8607
|
if (webVitals) {
|
|
8221
8608
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
8222
8609
|
contentType: "application/json",
|
|
@@ -8224,7 +8611,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8224
8611
|
});
|
|
8225
8612
|
}
|
|
8226
8613
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
8227
|
-
const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
|
|
8614
|
+
const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
8228
8615
|
if (pageState) {
|
|
8229
8616
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
8230
8617
|
contentType: "application/json",
|
|
@@ -8265,6 +8652,7 @@ var piwiFixtures = {
|
|
|
8265
8652
|
// requested, so suites that never destructure `page` are still captured.
|
|
8266
8653
|
piwiCapture: [
|
|
8267
8654
|
async ({}, use, testInfo) => {
|
|
8655
|
+
boxCaptureFrames();
|
|
8268
8656
|
const sink = createSink();
|
|
8269
8657
|
sink.testInfo = testInfo;
|
|
8270
8658
|
if (isProbeMode())
|
|
@@ -8309,12 +8697,12 @@ function extendPiwiFixtures(test) {
|
|
|
8309
8697
|
|
|
8310
8698
|
// src/internal/ai/ai-fixtures.ts
|
|
8311
8699
|
var fs18 = __toESM(require("fs"));
|
|
8312
|
-
var
|
|
8700
|
+
var path23 = __toESM(require("path"));
|
|
8313
8701
|
var import_test = require("@playwright/test");
|
|
8314
8702
|
|
|
8315
8703
|
// src/internal/ai/artifact.ts
|
|
8316
8704
|
var fs17 = __toESM(require("fs"));
|
|
8317
|
-
var
|
|
8705
|
+
var path21 = __toESM(require("path"));
|
|
8318
8706
|
var ARTIFACT_VERSION = 1;
|
|
8319
8707
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
8320
8708
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -8418,7 +8806,7 @@ function writeEntry(file, entry) {
|
|
|
8418
8806
|
const canonical = serializeEntry(entry);
|
|
8419
8807
|
return withEntryLock(file, () => {
|
|
8420
8808
|
if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
8421
|
-
fs17.mkdirSync(
|
|
8809
|
+
fs17.mkdirSync(path21.dirname(file), { recursive: true });
|
|
8422
8810
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8423
8811
|
fs17.writeFileSync(tmp, canonical);
|
|
8424
8812
|
fs17.renameSync(tmp, file);
|
|
@@ -8427,7 +8815,7 @@ function writeEntry(file, entry) {
|
|
|
8427
8815
|
}
|
|
8428
8816
|
function withEntryLock(file, fn) {
|
|
8429
8817
|
const lock = `${file}.lock`;
|
|
8430
|
-
fs17.mkdirSync(
|
|
8818
|
+
fs17.mkdirSync(path21.dirname(file), { recursive: true });
|
|
8431
8819
|
const deadline = Date.now() + 5e3;
|
|
8432
8820
|
for (; ; ) {
|
|
8433
8821
|
try {
|
|
@@ -8456,7 +8844,7 @@ function withEntryLock(file, fn) {
|
|
|
8456
8844
|
|
|
8457
8845
|
// src/internal/ai/keys.ts
|
|
8458
8846
|
var crypto3 = __toESM(require("crypto"));
|
|
8459
|
-
var
|
|
8847
|
+
var path22 = __toESM(require("path"));
|
|
8460
8848
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
8461
8849
|
function normalizeTemplate(template) {
|
|
8462
8850
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -8470,13 +8858,13 @@ function slug(text, maxLength = 40) {
|
|
|
8470
8858
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
8471
8859
|
}
|
|
8472
8860
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
8473
|
-
return
|
|
8861
|
+
return path22.join(path22.dirname(specFile), dir, path22.basename(specFile));
|
|
8474
8862
|
}
|
|
8475
8863
|
function entryPath(params) {
|
|
8476
8864
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
8477
8865
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
8478
8866
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
8479
|
-
return
|
|
8867
|
+
return path22.join(entryDir(params.specFile, params.dir), name);
|
|
8480
8868
|
}
|
|
8481
8869
|
function parseLocation(location) {
|
|
8482
8870
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -8930,7 +9318,7 @@ function testIdentity(testInfo) {
|
|
|
8930
9318
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
8931
9319
|
}
|
|
8932
9320
|
function relativeToCwd(file) {
|
|
8933
|
-
return
|
|
9321
|
+
return path23.relative(process.cwd(), file).split(path23.sep).join("/");
|
|
8934
9322
|
}
|
|
8935
9323
|
function recordIntents(intents, entry) {
|
|
8936
9324
|
const add = (locator, kind) => {
|