@piwitests/reporter 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global-setup-module.js +13 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +665 -227
- package/dist/internal/capture/capture-fixtures.js +445 -100
- package/dist/internal/capture/inspect-on-failure.d.ts +3 -1
- package/dist/internal/capture/inspect-on-failure.js +2 -0
- package/dist/internal/capture/pick-on-failure.d.ts +15 -7
- package/dist/internal/capture/pick-on-failure.js +36 -30
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -56,9 +56,9 @@ function defaultDesktopConfigPath() {
|
|
|
56
56
|
}
|
|
57
57
|
function readDesktopConfig(filePath) {
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
const url =
|
|
61
|
-
const token =
|
|
59
|
+
const parsed2 = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
60
|
+
const url = parsed2?.url;
|
|
61
|
+
const token = parsed2?.token;
|
|
62
62
|
if (typeof url !== "string" || !url) return null;
|
|
63
63
|
if (typeof token !== "string" || !token) return null;
|
|
64
64
|
return { url, token };
|
|
@@ -145,6 +145,10 @@ var PIWI_ENV_KEYS = {
|
|
|
145
145
|
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
146
146
|
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
147
147
|
};
|
|
148
|
+
var PIWI_LOCAL_DEBUG_ENV = {
|
|
149
|
+
pauseAt: "PIWI_PAUSE_AT",
|
|
150
|
+
editorSend: "PIWI_EDITOR_SEND"
|
|
151
|
+
};
|
|
148
152
|
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
149
153
|
var PIWI_DEFAULTED_CAPTURE_ENV = "PIWI_DEFAULTED_CAPTURE";
|
|
150
154
|
var PIWI_SELECTION_ENV = {
|
|
@@ -320,8 +324,8 @@ var Logger = class {
|
|
|
320
324
|
// src/internal/transport/http-client.ts
|
|
321
325
|
function parseErrorMessage(text) {
|
|
322
326
|
try {
|
|
323
|
-
const
|
|
324
|
-
const message =
|
|
327
|
+
const parsed2 = JSON.parse(text);
|
|
328
|
+
const message = parsed2?.message;
|
|
325
329
|
return typeof message === "string" ? message : void 0;
|
|
326
330
|
} catch {
|
|
327
331
|
return void 0;
|
|
@@ -3780,9 +3784,9 @@ function loadAriaSampleSet(projectName) {
|
|
|
3780
3784
|
let set = null;
|
|
3781
3785
|
try {
|
|
3782
3786
|
const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
3783
|
-
const
|
|
3784
|
-
if (
|
|
3785
|
-
set = new Set(
|
|
3787
|
+
const parsed2 = JSON.parse(raw);
|
|
3788
|
+
if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
|
|
3789
|
+
set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
|
|
3786
3790
|
}
|
|
3787
3791
|
} catch {
|
|
3788
3792
|
set = null;
|
|
@@ -3827,11 +3831,17 @@ function wildcard(pattern) {
|
|
|
3827
3831
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
3828
3832
|
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
3829
3833
|
}
|
|
3830
|
-
function selectedProjects(projects, argv = process.argv) {
|
|
3831
|
-
|
|
3832
|
-
if (
|
|
3833
|
-
|
|
3834
|
-
|
|
3834
|
+
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
3835
|
+
let chosen;
|
|
3836
|
+
if (Array.isArray(filtered)) {
|
|
3837
|
+
const names = new Set(filtered.map((p) => p.name).filter(Boolean));
|
|
3838
|
+
chosen = new Set(projects.filter((p) => filtered.includes(p) || !!p.name && names.has(p.name)));
|
|
3839
|
+
} else {
|
|
3840
|
+
const names = cliProjectNames(argv);
|
|
3841
|
+
if (!names || names.length === 0) return projects;
|
|
3842
|
+
const patterns = names.map(wildcard);
|
|
3843
|
+
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
3844
|
+
}
|
|
3835
3845
|
if (!argv.includes("--no-deps")) {
|
|
3836
3846
|
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
3837
3847
|
const queue = [...chosen];
|
|
@@ -3853,8 +3863,9 @@ function asProxy(value) {
|
|
|
3853
3863
|
}
|
|
3854
3864
|
function baseUrlTargets(config, argv = process.argv) {
|
|
3855
3865
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
3866
|
+
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
3856
3867
|
const targets = /* @__PURE__ */ new Map();
|
|
3857
|
-
for (const project of selectedProjects(projects, argv)) {
|
|
3868
|
+
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
3858
3869
|
const url = project.use?.baseURL;
|
|
3859
3870
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
3860
3871
|
const target = targets.get(url);
|
|
@@ -3933,14 +3944,14 @@ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
|
3933
3944
|
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
3934
3945
|
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
3935
3946
|
function parseManifestJson(text) {
|
|
3936
|
-
let
|
|
3947
|
+
let parsed2;
|
|
3937
3948
|
try {
|
|
3938
|
-
|
|
3949
|
+
parsed2 = JSON.parse(text);
|
|
3939
3950
|
} catch {
|
|
3940
3951
|
return null;
|
|
3941
3952
|
}
|
|
3942
|
-
if (!
|
|
3943
|
-
const obj =
|
|
3953
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
3954
|
+
const obj = parsed2;
|
|
3944
3955
|
const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
|
|
3945
3956
|
method: r.method,
|
|
3946
3957
|
pattern: r.pattern,
|
|
@@ -4144,8 +4155,8 @@ function parseRouteKey(routeKey2) {
|
|
|
4144
4155
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
4145
4156
|
}
|
|
4146
4157
|
function patternToRegex(pattern) {
|
|
4147
|
-
const
|
|
4148
|
-
const escaped =
|
|
4158
|
+
const path32 = pattern.split("?")[0] ?? pattern;
|
|
4159
|
+
const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
4149
4160
|
return new RegExp(`^${escaped}$`);
|
|
4150
4161
|
}
|
|
4151
4162
|
function suitePathEqual(a, b) {
|
|
@@ -4168,13 +4179,13 @@ function matchProbeItem(plan, test) {
|
|
|
4168
4179
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
4169
4180
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
4170
4181
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
4171
|
-
let
|
|
4182
|
+
let path32;
|
|
4172
4183
|
try {
|
|
4173
|
-
|
|
4184
|
+
path32 = new URL(url).pathname;
|
|
4174
4185
|
} catch {
|
|
4175
|
-
|
|
4186
|
+
path32 = url.split("?")[0] ?? url;
|
|
4176
4187
|
}
|
|
4177
|
-
return patternToRegex(pattern).test(
|
|
4188
|
+
return patternToRegex(pattern).test(path32);
|
|
4178
4189
|
}
|
|
4179
4190
|
function createProbeState() {
|
|
4180
4191
|
return { navigated: false, matches: 0 };
|
|
@@ -4196,10 +4207,10 @@ function isProbeMode() {
|
|
|
4196
4207
|
function loadPlan() {
|
|
4197
4208
|
if (planCache !== void 0) return planCache;
|
|
4198
4209
|
planCache = null;
|
|
4199
|
-
const
|
|
4200
|
-
if (
|
|
4210
|
+
const path32 = process.env[PIWI_PROBE_ENV.plan];
|
|
4211
|
+
if (path32) {
|
|
4201
4212
|
try {
|
|
4202
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
4213
|
+
planCache = JSON.parse(fs14.readFileSync(path32, "utf8"));
|
|
4203
4214
|
} catch {
|
|
4204
4215
|
planCache = null;
|
|
4205
4216
|
}
|
|
@@ -4221,10 +4232,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
4221
4232
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
4222
4233
|
}
|
|
4223
4234
|
function recordProbeOutcome(line) {
|
|
4224
|
-
const
|
|
4225
|
-
if (!
|
|
4235
|
+
const path32 = process.env[PIWI_PROBE_ENV.results];
|
|
4236
|
+
if (!path32) return;
|
|
4226
4237
|
try {
|
|
4227
|
-
fs14.appendFileSync(
|
|
4238
|
+
fs14.appendFileSync(path32, `${JSON.stringify(line)}
|
|
4228
4239
|
`);
|
|
4229
4240
|
} catch {
|
|
4230
4241
|
}
|
|
@@ -4447,8 +4458,8 @@ var FLAKE_LAB_RUN_METADATA_KEY = "piwiFlakeLab";
|
|
|
4447
4458
|
// ../core/src/page-key.ts
|
|
4448
4459
|
function normalizeRoute(url) {
|
|
4449
4460
|
try {
|
|
4450
|
-
const
|
|
4451
|
-
let pathname =
|
|
4461
|
+
const parsed2 = new URL(url);
|
|
4462
|
+
let pathname = parsed2.pathname;
|
|
4452
4463
|
pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
|
|
4453
4464
|
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");
|
|
4454
4465
|
pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
|
|
@@ -4456,8 +4467,8 @@ function normalizeRoute(url) {
|
|
|
4456
4467
|
pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
|
|
4457
4468
|
pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
4458
4469
|
let queryString = "";
|
|
4459
|
-
if (
|
|
4460
|
-
const params = new URLSearchParams(
|
|
4470
|
+
if (parsed2.search) {
|
|
4471
|
+
const params = new URLSearchParams(parsed2.search);
|
|
4461
4472
|
const redacted = new URLSearchParams();
|
|
4462
4473
|
for (const [key] of params) {
|
|
4463
4474
|
redacted.set(key, "<redacted>");
|
|
@@ -4509,24 +4520,24 @@ function faultRouteAction(fault) {
|
|
|
4509
4520
|
return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
|
|
4510
4521
|
}
|
|
4511
4522
|
function dropOneField(body) {
|
|
4512
|
-
let
|
|
4523
|
+
let parsed2;
|
|
4513
4524
|
try {
|
|
4514
|
-
|
|
4525
|
+
parsed2 = JSON.parse(body);
|
|
4515
4526
|
} catch {
|
|
4516
4527
|
return body;
|
|
4517
4528
|
}
|
|
4518
|
-
if (Array.isArray(
|
|
4529
|
+
if (Array.isArray(parsed2)) {
|
|
4519
4530
|
let key;
|
|
4520
|
-
for (const el of
|
|
4531
|
+
for (const el of parsed2) {
|
|
4521
4532
|
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
4522
4533
|
key ??= Object.keys(el)[0];
|
|
4523
4534
|
if (key) delete el[key];
|
|
4524
4535
|
}
|
|
4525
4536
|
}
|
|
4526
|
-
return JSON.stringify(
|
|
4537
|
+
return JSON.stringify(parsed2);
|
|
4527
4538
|
}
|
|
4528
|
-
if (
|
|
4529
|
-
const obj =
|
|
4539
|
+
if (parsed2 && typeof parsed2 === "object") {
|
|
4540
|
+
const obj = parsed2;
|
|
4530
4541
|
const key = Object.keys(obj)[0];
|
|
4531
4542
|
if (key) delete obj[key];
|
|
4532
4543
|
return JSON.stringify(obj);
|
|
@@ -5334,8 +5345,8 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
5334
5345
|
|
|
5335
5346
|
// src/internal/submit/run-submitter.ts
|
|
5336
5347
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
5337
|
-
function markSubmitFallback(run,
|
|
5338
|
-
run.metadata.ingestHealth = { submitFallback: { path:
|
|
5348
|
+
function markSubmitFallback(run, path32, reason) {
|
|
5349
|
+
run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
|
|
5339
5350
|
}
|
|
5340
5351
|
var RunSubmitter = class {
|
|
5341
5352
|
/**
|
|
@@ -7895,7 +7906,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
7895
7906
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
7896
7907
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
7897
7908
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
7898
|
-
const
|
|
7909
|
+
const relative14 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
7899
7910
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
7900
7911
|
const perWorker = Math.max(
|
|
7901
7912
|
60,
|
|
@@ -7903,17 +7914,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
7903
7914
|
);
|
|
7904
7915
|
return {
|
|
7905
7916
|
startedAt,
|
|
7906
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
7907
|
-
memoryBytes: hasSamples ? downsamplePoints(
|
|
7917
|
+
cpuPct: hasSamples ? downsamplePoints(relative14(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
7918
|
+
memoryBytes: hasSamples ? downsamplePoints(relative14(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
7908
7919
|
pages: workers.map(({ worker, events }) => {
|
|
7909
|
-
const points =
|
|
7920
|
+
const points = relative14(events, -startedAt);
|
|
7910
7921
|
const last = points.pop();
|
|
7911
7922
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
7912
7923
|
}),
|
|
7913
7924
|
workers: trees.map((tree) => ({
|
|
7914
7925
|
worker: tree.worker,
|
|
7915
|
-
cpuCores: downsamplePoints(
|
|
7916
|
-
memoryBytes: downsamplePoints(
|
|
7926
|
+
cpuCores: downsamplePoints(relative14(tree.cpuCores, shift), perWorker, "mean"),
|
|
7927
|
+
memoryBytes: downsamplePoints(relative14(tree.memoryBytes, shift), perWorker, "max")
|
|
7917
7928
|
}))
|
|
7918
7929
|
};
|
|
7919
7930
|
}
|
|
@@ -8076,8 +8087,8 @@ function formatTimeout(ms) {
|
|
|
8076
8087
|
}
|
|
8077
8088
|
function routeOf(url) {
|
|
8078
8089
|
try {
|
|
8079
|
-
const
|
|
8080
|
-
return `${
|
|
8090
|
+
const parsed2 = new URL(url);
|
|
8091
|
+
return `${parsed2.pathname}${parsed2.search}` || "/";
|
|
8081
8092
|
} catch {
|
|
8082
8093
|
return url;
|
|
8083
8094
|
}
|
|
@@ -8097,14 +8108,14 @@ function textOfGetByText(locator) {
|
|
|
8097
8108
|
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
8098
8109
|
return m ? m[2] : null;
|
|
8099
8110
|
}
|
|
8100
|
-
function receivedDisplay(
|
|
8101
|
-
const received =
|
|
8102
|
-
if (
|
|
8111
|
+
function receivedDisplay(parsed2) {
|
|
8112
|
+
const received = parsed2.received ?? "";
|
|
8113
|
+
if (parsed2.assertion !== "toHaveURL") return received;
|
|
8103
8114
|
const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
|
|
8104
8115
|
return m ? `"${routeOf(m[1])}"` : received;
|
|
8105
8116
|
}
|
|
8106
|
-
function firstLineFallback(
|
|
8107
|
-
const line = (
|
|
8117
|
+
function firstLineFallback(parsed2) {
|
|
8118
|
+
const line = (parsed2.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
|
|
8108
8119
|
return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
|
|
8109
8120
|
}
|
|
8110
8121
|
var Line = class {
|
|
@@ -8141,148 +8152,148 @@ function timeoutSuffix(timeoutMs) {
|
|
|
8141
8152
|
function timeoutParen(timeoutMs) {
|
|
8142
8153
|
return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
|
|
8143
8154
|
}
|
|
8144
|
-
function buildActionTimeout(
|
|
8155
|
+
function buildActionTimeout(parsed2, opts) {
|
|
8145
8156
|
const line = new Line();
|
|
8146
|
-
const verb = ACTION_VERBS[
|
|
8147
|
-
const state = STATE_PHRASES[
|
|
8148
|
-
if (
|
|
8149
|
-
return subject(line,
|
|
8157
|
+
const verb = ACTION_VERBS[parsed2.action ?? ""] ?? parsed2.action ?? "action";
|
|
8158
|
+
const state = STATE_PHRASES[parsed2.lastState];
|
|
8159
|
+
if (parsed2.locator && state) {
|
|
8160
|
+
return subject(line, parsed2.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8150
8161
|
}
|
|
8151
|
-
if (
|
|
8152
|
-
const count =
|
|
8153
|
-
return subject(line,
|
|
8162
|
+
if (parsed2.locator && parsed2.lastState === "resolved-count" && parsed2.resolvedCount !== null) {
|
|
8163
|
+
const count = parsed2.resolvedCount;
|
|
8164
|
+
return subject(line, parsed2.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed2.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8154
8165
|
}
|
|
8155
|
-
if (
|
|
8156
|
-
return line.text(`${verb} on `).locator(opts.locator ??
|
|
8166
|
+
if (parsed2.locator) {
|
|
8167
|
+
return line.text(`${verb} on `).locator(opts.locator ?? parsed2.locator).text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8157
8168
|
}
|
|
8158
|
-
const call =
|
|
8159
|
-
return line.text(`${call} timed out`).text(timeoutSuffix(
|
|
8169
|
+
const call = parsed2.subject && parsed2.action ? `${parsed2.subject}.${parsed2.action}` : verb;
|
|
8170
|
+
return line.text(`${call} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8160
8171
|
}
|
|
8161
|
-
function buildAssertion(
|
|
8172
|
+
function buildAssertion(parsed2, opts) {
|
|
8162
8173
|
const line = new Line();
|
|
8163
|
-
const matcher =
|
|
8164
|
-
const notFound =
|
|
8165
|
-
const expected =
|
|
8166
|
-
const received =
|
|
8167
|
-
if (matcher === "toHaveCount" &&
|
|
8168
|
-
const want = Number(
|
|
8169
|
-
const got = notFound ? 0 : Number(
|
|
8174
|
+
const matcher = parsed2.assertion ?? "expect";
|
|
8175
|
+
const notFound = parsed2.lastState === "not-found" || parsed2.lastState === "resolved-count" && parsed2.resolvedCount === 0 || /element\(s\) not found/.test(parsed2.received ?? "");
|
|
8176
|
+
const expected = parsed2.expected ? truncateValue(parsed2.expected, opts.valueMax) : null;
|
|
8177
|
+
const received = parsed2.received ? truncateValue(receivedDisplay(parsed2), opts.valueMax) : null;
|
|
8178
|
+
if (matcher === "toHaveCount" && parsed2.locator) {
|
|
8179
|
+
const want = Number(parsed2.expected);
|
|
8180
|
+
const got = notFound ? 0 : Number(parsed2.received);
|
|
8170
8181
|
const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
|
|
8171
|
-
const noun2 = countNoun(
|
|
8182
|
+
const noun2 = countNoun(parsed2.locator, Number.isFinite(want) ? want : 2);
|
|
8172
8183
|
const gotText = Number.isFinite(got) ? got === 0 ? "none" : String(got) : received ?? "none";
|
|
8173
|
-
return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ??
|
|
8184
|
+
return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed2.locator).text(" toHaveCount");
|
|
8174
8185
|
}
|
|
8175
8186
|
const state = STATE_MATCHERS[matcher];
|
|
8176
8187
|
if (state) {
|
|
8177
|
-
const unmet =
|
|
8178
|
-
if (!
|
|
8188
|
+
const unmet = parsed2.negated ? `stayed ${state.met}` : state.unmet;
|
|
8189
|
+
if (!parsed2.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8179
8190
|
if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
|
|
8180
|
-
return subject(line,
|
|
8191
|
+
return subject(line, parsed2.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8181
8192
|
}
|
|
8182
|
-
return subject(line,
|
|
8193
|
+
return subject(line, parsed2.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8183
8194
|
}
|
|
8184
8195
|
const noun = VALUE_MATCHERS[matcher];
|
|
8185
8196
|
if (noun) {
|
|
8186
|
-
if (
|
|
8187
|
-
return subject(line,
|
|
8197
|
+
if (parsed2.locator && notFound) {
|
|
8198
|
+
return subject(line, parsed2.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed2.timeoutMs));
|
|
8188
8199
|
}
|
|
8189
8200
|
line.text(`Expected ${noun} `);
|
|
8190
8201
|
if (expected) line.value(expected);
|
|
8191
8202
|
else line.text("to match");
|
|
8192
8203
|
if (received) line.text(", got ").value(received);
|
|
8193
|
-
if (
|
|
8194
|
-
line.text(" \u2014 ").locator(opts.locator ??
|
|
8204
|
+
if (parsed2.locator)
|
|
8205
|
+
line.text(" \u2014 ").locator(opts.locator ?? parsed2.locator).text(` ${matcher}`);
|
|
8195
8206
|
else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
|
|
8196
8207
|
return line;
|
|
8197
8208
|
}
|
|
8198
|
-
if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(
|
|
8209
|
+
if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed2.timeoutMs));
|
|
8199
8210
|
if (expected || received) {
|
|
8200
8211
|
line.text("Expected ");
|
|
8201
8212
|
if (expected) line.value(expected);
|
|
8202
8213
|
else line.text("a different value");
|
|
8203
8214
|
if (received) line.text(", got ").value(received);
|
|
8204
8215
|
line.text(` \u2014 ${matcher}`);
|
|
8205
|
-
if (
|
|
8216
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8206
8217
|
return line;
|
|
8207
8218
|
}
|
|
8208
|
-
if (
|
|
8209
|
-
return subject(line,
|
|
8219
|
+
if (parsed2.locator) {
|
|
8220
|
+
return subject(line, parsed2.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8210
8221
|
}
|
|
8211
8222
|
return line.text(`${matcher} assertion failed`);
|
|
8212
8223
|
}
|
|
8213
|
-
function buildStrictMode(
|
|
8224
|
+
function buildStrictMode(parsed2, opts) {
|
|
8214
8225
|
const line = new Line();
|
|
8215
|
-
const count =
|
|
8216
|
-
if (!
|
|
8217
|
-
return line.locator(opts.locator ??
|
|
8226
|
+
const count = parsed2.resolvedCount ?? 0;
|
|
8227
|
+
if (!parsed2.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
|
|
8228
|
+
return line.locator(opts.locator ?? parsed2.locator).text(` matched ${count} ${countNoun(parsed2.locator, count)} \u2014 strict mode`);
|
|
8218
8229
|
}
|
|
8219
|
-
function buildNavigation(
|
|
8230
|
+
function buildNavigation(parsed2, opts) {
|
|
8220
8231
|
const line = new Line();
|
|
8221
|
-
const code =
|
|
8232
|
+
const code = parsed2.networkErrorCode?.replace(/^net::/, "") ?? null;
|
|
8222
8233
|
if (code) {
|
|
8223
8234
|
const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
|
|
8224
8235
|
line.text(phrase);
|
|
8225
|
-
if (
|
|
8236
|
+
if (parsed2.url) line.text(" ").value(truncateValue(parsed2.url, opts.valueMax * 2));
|
|
8226
8237
|
return line;
|
|
8227
8238
|
}
|
|
8228
|
-
const route =
|
|
8229
|
-
const timedOut =
|
|
8230
|
-
if (
|
|
8239
|
+
const route = parsed2.url ? routeOf(parsed2.url) : null;
|
|
8240
|
+
const timedOut = parsed2.timeoutMs !== null || parsed2.errorName === "TimeoutError";
|
|
8241
|
+
if (parsed2.action === "waitForURL" || parsed2.action === "waitForNavigation") {
|
|
8231
8242
|
line.text("Never navigated to ");
|
|
8232
8243
|
if (route) line.value(truncateValue(route, opts.valueMax));
|
|
8233
8244
|
else line.text("the expected URL");
|
|
8234
|
-
return line.text(` \u2014 ${
|
|
8245
|
+
return line.text(` \u2014 ${parsed2.action} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8235
8246
|
}
|
|
8236
8247
|
line.text("Navigation");
|
|
8237
8248
|
if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
|
|
8238
|
-
if (timedOut) return line.text(" timed out").text(timeoutSuffix(
|
|
8249
|
+
if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8239
8250
|
return line.text(" failed");
|
|
8240
8251
|
}
|
|
8241
|
-
function buildTestTimeout(
|
|
8242
|
-
const line = new Line().text("Test timed out").text(timeoutSuffix(
|
|
8243
|
-
if (
|
|
8244
|
-
const phase =
|
|
8252
|
+
function buildTestTimeout(parsed2, opts, ctx) {
|
|
8253
|
+
const line = new Line().text("Test timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8254
|
+
if (parsed2.timeoutPhase) {
|
|
8255
|
+
const phase = parsed2.timeoutPhase;
|
|
8245
8256
|
const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
|
|
8246
8257
|
return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
|
|
8247
8258
|
}
|
|
8248
|
-
if (
|
|
8249
|
-
return line.text(" while navigating to ").value(truncateValue(routeOf(
|
|
8259
|
+
if (parsed2.isNavigationFailure && parsed2.url) {
|
|
8260
|
+
return line.text(" while navigating to ").value(truncateValue(routeOf(parsed2.url), opts.valueMax));
|
|
8250
8261
|
}
|
|
8251
|
-
if (
|
|
8252
|
-
const state = STATE_MATCHERS[
|
|
8262
|
+
if (parsed2.assertion && parsed2.locator) {
|
|
8263
|
+
const state = STATE_MATCHERS[parsed2.assertion];
|
|
8253
8264
|
line.text(state ? ` while waiting for ` : " while expecting ");
|
|
8254
|
-
subject(line,
|
|
8255
|
-
return line.text(state ? ` to be ${state.met}` : ` ${
|
|
8265
|
+
subject(line, parsed2.locator, opts);
|
|
8266
|
+
return line.text(state ? ` to be ${state.met}` : ` ${parsed2.assertion}`);
|
|
8256
8267
|
}
|
|
8257
|
-
if (
|
|
8258
|
-
const gerund = ACTION_GERUNDS[
|
|
8268
|
+
if (parsed2.action && parsed2.locator) {
|
|
8269
|
+
const gerund = ACTION_GERUNDS[parsed2.action] ?? `${ACTION_VERBS[parsed2.action] ?? parsed2.action} on`;
|
|
8259
8270
|
line.text(` while ${gerund} `);
|
|
8260
|
-
return subject(line,
|
|
8271
|
+
return subject(line, parsed2.locator, opts);
|
|
8261
8272
|
}
|
|
8262
|
-
if (
|
|
8273
|
+
if (parsed2.subject && parsed2.action) return line.text(` during ${parsed2.subject}.${parsed2.action}`);
|
|
8263
8274
|
const step = ctx?.lastStepTitle?.trim();
|
|
8264
8275
|
if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
|
|
8265
8276
|
return line;
|
|
8266
8277
|
}
|
|
8267
|
-
function buildCrash(
|
|
8278
|
+
function buildCrash(parsed2, opts) {
|
|
8268
8279
|
const line = new Line();
|
|
8269
|
-
const what = /Page crashed/i.test(
|
|
8270
|
-
if (
|
|
8271
|
-
const verb = ACTION_VERBS[
|
|
8280
|
+
const what = /Page crashed/i.test(parsed2.messageHead) ? "Page crashed" : "Page or browser closed";
|
|
8281
|
+
if (parsed2.action) {
|
|
8282
|
+
const verb = ACTION_VERBS[parsed2.action] ?? parsed2.action;
|
|
8272
8283
|
line.text(`${what} during ${verb}`);
|
|
8273
|
-
if (
|
|
8284
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8274
8285
|
return line;
|
|
8275
8286
|
}
|
|
8276
|
-
if (
|
|
8277
|
-
return line.text(`${what} while expecting `).locator(opts.locator ??
|
|
8287
|
+
if (parsed2.assertion && parsed2.locator) {
|
|
8288
|
+
return line.text(`${what} while expecting `).locator(opts.locator ?? parsed2.locator).text(` ${parsed2.assertion}`);
|
|
8278
8289
|
}
|
|
8279
8290
|
return line.text(what);
|
|
8280
8291
|
}
|
|
8281
8292
|
var CUSTOM_MESSAGE_MAX_CHARS = 60;
|
|
8282
8293
|
var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
|
|
8283
|
-
function withLeads(line,
|
|
8284
|
-
const hook = ctx?.failedIn && !
|
|
8285
|
-
const message =
|
|
8294
|
+
function withLeads(line, parsed2, ctx) {
|
|
8295
|
+
const hook = ctx?.failedIn && !parsed2.timeoutPhase ? ctx.failedIn.hook : null;
|
|
8296
|
+
const message = parsed2.customMessage ? truncateValue(parsed2.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
|
|
8286
8297
|
if (!hook && !message) return line;
|
|
8287
8298
|
const led = new Line();
|
|
8288
8299
|
if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
|
|
@@ -8295,49 +8306,49 @@ function withLeads(line, parsed, ctx) {
|
|
|
8295
8306
|
});
|
|
8296
8307
|
return led;
|
|
8297
8308
|
}
|
|
8298
|
-
function build(
|
|
8299
|
-
return withLeads(buildLine(
|
|
8309
|
+
function build(parsed2, opts, ctx) {
|
|
8310
|
+
return withLeads(buildLine(parsed2, opts, ctx), parsed2, ctx);
|
|
8300
8311
|
}
|
|
8301
|
-
function buildLine(
|
|
8302
|
-
switch (
|
|
8312
|
+
function buildLine(parsed2, opts, ctx) {
|
|
8313
|
+
switch (parsed2.kind) {
|
|
8303
8314
|
case "action-timeout":
|
|
8304
|
-
return buildActionTimeout(
|
|
8315
|
+
return buildActionTimeout(parsed2, opts);
|
|
8305
8316
|
case "assertion":
|
|
8306
8317
|
case "assertion-timeout":
|
|
8307
|
-
return buildAssertion(
|
|
8318
|
+
return buildAssertion(parsed2, opts);
|
|
8308
8319
|
case "strict-mode":
|
|
8309
|
-
return buildStrictMode(
|
|
8320
|
+
return buildStrictMode(parsed2, opts);
|
|
8310
8321
|
case "navigation":
|
|
8311
|
-
return buildNavigation(
|
|
8322
|
+
return buildNavigation(parsed2, opts);
|
|
8312
8323
|
case "test-timeout":
|
|
8313
|
-
return buildTestTimeout(
|
|
8324
|
+
return buildTestTimeout(parsed2, opts, ctx);
|
|
8314
8325
|
case "crash":
|
|
8315
|
-
return buildCrash(
|
|
8326
|
+
return buildCrash(parsed2, opts);
|
|
8316
8327
|
default:
|
|
8317
|
-
return new Line().text(firstLineFallback(
|
|
8328
|
+
return new Line().text(firstLineFallback(parsed2));
|
|
8318
8329
|
}
|
|
8319
8330
|
}
|
|
8320
|
-
function detailOf(
|
|
8321
|
-
switch (
|
|
8331
|
+
function detailOf(parsed2, headline) {
|
|
8332
|
+
switch (parsed2.kind) {
|
|
8322
8333
|
case "test-timeout":
|
|
8323
8334
|
case "action-timeout": {
|
|
8324
|
-
const state =
|
|
8325
|
-
if (!state ||
|
|
8335
|
+
const state = parsed2.lastStateLine;
|
|
8336
|
+
if (!state || parsed2.lastState === "not-found" || headline.includes(state)) return null;
|
|
8326
8337
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8327
8338
|
}
|
|
8328
8339
|
case "assertion":
|
|
8329
8340
|
case "assertion-timeout": {
|
|
8330
|
-
const received =
|
|
8341
|
+
const received = parsed2.received;
|
|
8331
8342
|
if (received && !/element\(s\) not found/.test(received)) {
|
|
8332
|
-
const shown = truncateValue(receivedDisplay(
|
|
8343
|
+
const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
|
|
8333
8344
|
if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
|
|
8334
8345
|
}
|
|
8335
|
-
const state =
|
|
8346
|
+
const state = parsed2.lastStateLine;
|
|
8336
8347
|
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
8337
8348
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8338
8349
|
}
|
|
8339
8350
|
case "navigation":
|
|
8340
|
-
return
|
|
8351
|
+
return parsed2.networkErrorCode ?? parsed2.lastStateLine;
|
|
8341
8352
|
case "strict-mode":
|
|
8342
8353
|
case "crash":
|
|
8343
8354
|
return null;
|
|
@@ -8345,16 +8356,16 @@ function detailOf(parsed, headline) {
|
|
|
8345
8356
|
return null;
|
|
8346
8357
|
}
|
|
8347
8358
|
}
|
|
8348
|
-
function describeFailure(
|
|
8359
|
+
function describeFailure(parsed2, ctx) {
|
|
8349
8360
|
const attempts = [
|
|
8350
|
-
{ locator:
|
|
8351
|
-
{ locator:
|
|
8352
|
-
{ locator:
|
|
8361
|
+
{ locator: parsed2.locator, valueMax: VALUE_MAX_CHARS },
|
|
8362
|
+
{ locator: parsed2.leafLocator ?? parsed2.locator, valueMax: VALUE_MAX_CHARS },
|
|
8363
|
+
{ locator: parsed2.leafLocator ?? parsed2.locator, valueMax: SHORT_VALUE_MAX_CHARS }
|
|
8353
8364
|
];
|
|
8354
|
-
let line = build(
|
|
8365
|
+
let line = build(parsed2, attempts[0], ctx);
|
|
8355
8366
|
for (const opts of attempts.slice(1)) {
|
|
8356
8367
|
if (line.toString().length <= HEADLINE_MAX_CHARS) break;
|
|
8357
|
-
line = build(
|
|
8368
|
+
line = build(parsed2, opts, ctx);
|
|
8358
8369
|
}
|
|
8359
8370
|
let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
|
|
8360
8371
|
let headline = parts.map((p) => p.text).join("");
|
|
@@ -8364,10 +8375,10 @@ function describeFailure(parsed, ctx) {
|
|
|
8364
8375
|
parts.push({ kind: "text", text: "\u2026" });
|
|
8365
8376
|
}
|
|
8366
8377
|
if (!headline.trim()) {
|
|
8367
|
-
headline = firstLineFallback(
|
|
8378
|
+
headline = firstLineFallback(parsed2);
|
|
8368
8379
|
parts = [{ kind: "text", text: headline }];
|
|
8369
8380
|
}
|
|
8370
|
-
return { headline, detail: detailOf(
|
|
8381
|
+
return { headline, detail: detailOf(parsed2, headline), parts };
|
|
8371
8382
|
}
|
|
8372
8383
|
function clipParts(parts, max) {
|
|
8373
8384
|
const out = [];
|
|
@@ -9077,7 +9088,7 @@ async function resolveSelection(options = {}) {
|
|
|
9077
9088
|
}
|
|
9078
9089
|
|
|
9079
9090
|
// src/internal/capture/capture-fixtures.ts
|
|
9080
|
-
var
|
|
9091
|
+
var path28 = __toESM(require("path"));
|
|
9081
9092
|
var import_node_zlib2 = require("zlib");
|
|
9082
9093
|
|
|
9083
9094
|
// ../picker-dom/src/probe.ts
|
|
@@ -10140,6 +10151,83 @@ function showPickerChoices(arg) {
|
|
|
10140
10151
|
doc.body.appendChild(wrap);
|
|
10141
10152
|
}
|
|
10142
10153
|
|
|
10154
|
+
// ../picker-dom/src/overlay-pause.ts
|
|
10155
|
+
function showPauseBar(arg) {
|
|
10156
|
+
const g = globalThis;
|
|
10157
|
+
const doc = g.document;
|
|
10158
|
+
delete g.__piwiPauseState;
|
|
10159
|
+
if (!doc || !doc.body) {
|
|
10160
|
+
g.__piwiPauseState = "resume";
|
|
10161
|
+
return;
|
|
10162
|
+
}
|
|
10163
|
+
if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
|
|
10164
|
+
const Z = 2147483600;
|
|
10165
|
+
const bar = doc.createElement("div");
|
|
10166
|
+
bar.id = "__piwi-pause-bar";
|
|
10167
|
+
bar.setAttribute("role", "toolbar");
|
|
10168
|
+
bar.setAttribute("aria-label", "Piwi: test paused");
|
|
10169
|
+
bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:${Z + 4};display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;background:#111827;color:#f9fafb;border-bottom:2px solid #a78bfa;box-shadow:0 4px 20px rgba(0,0,0,.35);font:13px/1.5 system-ui,sans-serif;`;
|
|
10170
|
+
const where = doc.createElement("div");
|
|
10171
|
+
where.style.cssText = "font-weight:600;white-space:nowrap;";
|
|
10172
|
+
where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
|
|
10173
|
+
const what = doc.createElement("code");
|
|
10174
|
+
what.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d8b4fe;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
|
|
10175
|
+
what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
|
|
10176
|
+
what.title = what.textContent;
|
|
10177
|
+
bar.appendChild(where);
|
|
10178
|
+
bar.appendChild(what);
|
|
10179
|
+
const done = (choice) => {
|
|
10180
|
+
cleanup();
|
|
10181
|
+
g.__piwiPauseState = choice;
|
|
10182
|
+
};
|
|
10183
|
+
const onKey = (e) => {
|
|
10184
|
+
if (e.key !== "Escape") return;
|
|
10185
|
+
e.preventDefault();
|
|
10186
|
+
e.stopImmediatePropagation();
|
|
10187
|
+
done("resume");
|
|
10188
|
+
};
|
|
10189
|
+
const cleanup = () => {
|
|
10190
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
10191
|
+
bar.remove();
|
|
10192
|
+
delete g.__piwiPauseCleanup;
|
|
10193
|
+
};
|
|
10194
|
+
const buttons = [
|
|
10195
|
+
["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
10196
|
+
["Step", "step", "Pause again at the next action", false],
|
|
10197
|
+
["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
|
|
10198
|
+
["Finish", "finish", "Run on and pause no more in this test", false]
|
|
10199
|
+
];
|
|
10200
|
+
for (const [label, choice, hint, primary] of buttons) {
|
|
10201
|
+
const btn = doc.createElement("button");
|
|
10202
|
+
btn.type = "button";
|
|
10203
|
+
btn.textContent = label;
|
|
10204
|
+
btn.title = hint;
|
|
10205
|
+
btn.style.cssText = "border-radius:6px;padding:4px 12px;cursor:pointer;font:600 12px/1.5 system-ui,sans-serif;" + (primary ? "background:#7c3aed;color:#fff;border:1px solid #7c3aed;" : "background:#1f2937;color:#f3f4f6;border:1px solid #374151;");
|
|
10206
|
+
btn.addEventListener("click", (e) => {
|
|
10207
|
+
e.preventDefault();
|
|
10208
|
+
e.stopImmediatePropagation();
|
|
10209
|
+
done(choice);
|
|
10210
|
+
});
|
|
10211
|
+
bar.appendChild(btn);
|
|
10212
|
+
}
|
|
10213
|
+
g.__piwiPauseCleanup = cleanup;
|
|
10214
|
+
doc.addEventListener("keydown", onKey, true);
|
|
10215
|
+
doc.body.appendChild(bar);
|
|
10216
|
+
}
|
|
10217
|
+
function pauseAnswered() {
|
|
10218
|
+
const g = globalThis;
|
|
10219
|
+
if (g.__piwiPauseState !== void 0) return true;
|
|
10220
|
+
return !g.document?.getElementById("__piwi-pause-bar");
|
|
10221
|
+
}
|
|
10222
|
+
function takePauseState() {
|
|
10223
|
+
const g = globalThis;
|
|
10224
|
+
const state = g.__piwiPauseState;
|
|
10225
|
+
if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
|
|
10226
|
+
delete g.__piwiPauseCleanup;
|
|
10227
|
+
delete g.__piwiPauseState;
|
|
10228
|
+
return typeof state === "string" ? state : null;
|
|
10229
|
+
}
|
|
10230
|
+
|
|
10143
10231
|
// ../core/src/locator-generation.ts
|
|
10144
10232
|
var TAG_TO_ROLE = {
|
|
10145
10233
|
a: "link",
|
|
@@ -11176,11 +11264,11 @@ function deriveFailedLocator(testInfo) {
|
|
|
11176
11264
|
${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
11177
11265
|
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
11178
11266
|
if (!line) continue;
|
|
11179
|
-
const
|
|
11180
|
-
if (!
|
|
11267
|
+
const parsed2 = parseLeafLocatorExpression(line[1].trim());
|
|
11268
|
+
if (!parsed2) continue;
|
|
11181
11269
|
const loc = err.location;
|
|
11182
11270
|
const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
11183
|
-
return { method:
|
|
11271
|
+
return { method: parsed2.method, args: parsed2.args, location };
|
|
11184
11272
|
}
|
|
11185
11273
|
return null;
|
|
11186
11274
|
}
|
|
@@ -11202,25 +11290,16 @@ async function cleanupPicker(page) {
|
|
|
11202
11290
|
} catch {
|
|
11203
11291
|
}
|
|
11204
11292
|
}
|
|
11205
|
-
async function
|
|
11293
|
+
async function pickLocator(page, probe, failing) {
|
|
11206
11294
|
try {
|
|
11207
|
-
|
|
11208
|
-
const rendered = failed ? renderFailing(failed) : null;
|
|
11209
|
-
console.log(
|
|
11210
|
-
`
|
|
11211
|
-
[piwi] "${testInfo.title}" ${testInfo.status} \u2014 ${rendered ? "locator picker" : "inspector"} open in the browser: ${rendered ? `click the element that should replace ${rendered}` : "click any element to generate locators for it"} (\u2191/\u2193 to select a parent/child, Esc to skip).`
|
|
11212
|
-
);
|
|
11213
|
-
const overlayArg = { transport: "global", failing: rendered };
|
|
11295
|
+
const overlayArg = { transport: "global", failing };
|
|
11214
11296
|
await page.evaluate(installPickerOverlay, overlayArg);
|
|
11215
11297
|
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
11216
11298
|
timeout: 0,
|
|
11217
11299
|
polling: 250
|
|
11218
11300
|
});
|
|
11219
11301
|
const state = await page.evaluate(() => globalThis.__piwiPickState);
|
|
11220
|
-
if (state !== "picked")
|
|
11221
|
-
await cleanupPicker(page);
|
|
11222
|
-
return null;
|
|
11223
|
-
}
|
|
11302
|
+
if (state !== "picked") return null;
|
|
11224
11303
|
const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
|
|
11225
11304
|
const attrs = await handle.evaluate(probe.fn, probe.arg);
|
|
11226
11305
|
await handle.dispose();
|
|
@@ -11258,11 +11337,10 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11258
11337
|
);
|
|
11259
11338
|
if (ranked.length === 0) {
|
|
11260
11339
|
console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
|
|
11261
|
-
await cleanupPicker(page);
|
|
11262
11340
|
return null;
|
|
11263
11341
|
}
|
|
11264
11342
|
await page.evaluate(showPickerChoices, {
|
|
11265
|
-
failing
|
|
11343
|
+
failing,
|
|
11266
11344
|
choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
|
|
11267
11345
|
});
|
|
11268
11346
|
await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
|
|
@@ -11270,31 +11348,45 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11270
11348
|
polling: 250
|
|
11271
11349
|
});
|
|
11272
11350
|
const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
|
|
11273
|
-
await cleanupPicker(page);
|
|
11274
11351
|
if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
|
|
11275
11352
|
const picked = { ...ranked[choice], pickedByUser: true };
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11353
|
+
return {
|
|
11354
|
+
picked,
|
|
11355
|
+
alternatives: [picked, ...ranked.filter((_, i) => i !== choice)],
|
|
11356
|
+
element: {
|
|
11357
|
+
tagName: attrs.tagName,
|
|
11358
|
+
attributes: attrs.attributes,
|
|
11359
|
+
textContent: attrs.textContent,
|
|
11360
|
+
accessibleName,
|
|
11361
|
+
center: attrs.center,
|
|
11362
|
+
...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
|
|
11363
|
+
...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
|
|
11364
|
+
},
|
|
11365
|
+
...anchors.length > 0 ? { anchors } : {}
|
|
11285
11366
|
};
|
|
11367
|
+
} finally {
|
|
11368
|
+
await cleanupPicker(page);
|
|
11369
|
+
}
|
|
11370
|
+
}
|
|
11371
|
+
async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
11372
|
+
try {
|
|
11373
|
+
testInfo.setTimeout(0);
|
|
11374
|
+
const rendered = failed ? renderFailing(failed) : null;
|
|
11375
|
+
console.log(
|
|
11376
|
+
`
|
|
11377
|
+
[piwi] "${testInfo.title}" ${testInfo.status} \u2014 ${rendered ? "locator picker" : "inspector"} open in the browser: ${rendered ? `click the element that should replace ${rendered}` : "click any element to generate locators for it"} (\u2191/\u2193 to select a parent/child, Esc to skip).`
|
|
11378
|
+
);
|
|
11379
|
+
const pick = await pickLocator(page, probe, rendered);
|
|
11380
|
+
if (!pick) return null;
|
|
11286
11381
|
const location = failed?.location ?? null;
|
|
11287
11382
|
console.log(
|
|
11288
11383
|
failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
|
|
11289
|
-
[piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11290
|
-
[piwi] ${picked.locator}`
|
|
11384
|
+
[piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11385
|
+
[piwi] ${pick.picked.locator}`
|
|
11291
11386
|
);
|
|
11292
11387
|
return {
|
|
11293
11388
|
failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
|
|
11294
|
-
|
|
11295
|
-
alternatives,
|
|
11296
|
-
element,
|
|
11297
|
-
...anchors.length > 0 ? { anchors } : {}
|
|
11389
|
+
...pick
|
|
11298
11390
|
};
|
|
11299
11391
|
} catch {
|
|
11300
11392
|
return null;
|
|
@@ -11327,10 +11419,207 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
11327
11419
|
return true;
|
|
11328
11420
|
}
|
|
11329
11421
|
|
|
11422
|
+
// src/internal/capture/pause-at.ts
|
|
11423
|
+
var path26 = __toESM(require("path"));
|
|
11424
|
+
|
|
11425
|
+
// ../core/src/editor-send.ts
|
|
11426
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
11427
|
+
function parsePairing(text) {
|
|
11428
|
+
const trimmed = text.trim();
|
|
11429
|
+
const hash = trimmed.indexOf("#");
|
|
11430
|
+
if (hash < 0) return null;
|
|
11431
|
+
const url = trimmed.slice(0, hash);
|
|
11432
|
+
const token = trimmed.slice(hash + 1);
|
|
11433
|
+
if (!/^[A-Za-z0-9_-]{16,}$/.test(token)) return null;
|
|
11434
|
+
let parsed2;
|
|
11435
|
+
try {
|
|
11436
|
+
parsed2 = new URL(url);
|
|
11437
|
+
} catch {
|
|
11438
|
+
return null;
|
|
11439
|
+
}
|
|
11440
|
+
if (parsed2.protocol !== "http:" || !LOOPBACK_HOSTS.has(parsed2.hostname) || !parsed2.port) return null;
|
|
11441
|
+
return { url: `${parsed2.origin}${parsed2.pathname}`, token };
|
|
11442
|
+
}
|
|
11443
|
+
function isInsidePath(file) {
|
|
11444
|
+
if (!file || file.length > 1e3 || file.includes("\0")) return false;
|
|
11445
|
+
if (file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:/.test(file)) return false;
|
|
11446
|
+
return file.split(/[\\/]/).every((part) => part !== "..");
|
|
11447
|
+
}
|
|
11448
|
+
|
|
11449
|
+
// src/internal/capture/pause-at.ts
|
|
11450
|
+
var PauseSet = class {
|
|
11451
|
+
constructor(lines) {
|
|
11452
|
+
this.lines = lines;
|
|
11453
|
+
}
|
|
11454
|
+
get size() {
|
|
11455
|
+
return this.lines.size;
|
|
11456
|
+
}
|
|
11457
|
+
/** Whether a call site (`file:line:col`, as `captureCallerLocation` writes it) is on one of the lines. */
|
|
11458
|
+
matches(callerLocation) {
|
|
11459
|
+
if (!callerLocation) return false;
|
|
11460
|
+
const col = callerLocation.lastIndexOf(":");
|
|
11461
|
+
return col > 0 && this.lines.has(callerLocation.slice(0, col));
|
|
11462
|
+
}
|
|
11463
|
+
};
|
|
11464
|
+
var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
|
|
11465
|
+
function callSiteFile(file, cwd) {
|
|
11466
|
+
const slashed = file.replace(/\\/g, "/");
|
|
11467
|
+
const absolute = path26.isAbsolute(file) || path26.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
|
|
11468
|
+
let rel = absolute ? path26.relative(cwd, file) : path26.normalize(slashed);
|
|
11469
|
+
rel = rel.split(path26.sep).join("/").replace(/\\/g, "/");
|
|
11470
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
11471
|
+
return rel;
|
|
11472
|
+
}
|
|
11473
|
+
var ignoredLogged = /* @__PURE__ */ new Set();
|
|
11474
|
+
function parsePauseAt(value, cwd = process.cwd()) {
|
|
11475
|
+
if (!value?.trim()) return EMPTY;
|
|
11476
|
+
const lines = /* @__PURE__ */ new Set();
|
|
11477
|
+
for (const raw of value.split(";")) {
|
|
11478
|
+
const entry = raw.trim();
|
|
11479
|
+
if (!entry) continue;
|
|
11480
|
+
const m = /^(.+):(\d+)$/.exec(entry);
|
|
11481
|
+
const line = m ? Number(m[2]) : 0;
|
|
11482
|
+
if (!m?.[1] || line < 1) {
|
|
11483
|
+
if (!ignoredLogged.has(entry)) {
|
|
11484
|
+
ignoredLogged.add(entry);
|
|
11485
|
+
console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt}: ignoring "${entry}": expected file:line`);
|
|
11486
|
+
}
|
|
11487
|
+
continue;
|
|
11488
|
+
}
|
|
11489
|
+
lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
|
|
11490
|
+
}
|
|
11491
|
+
return lines.size ? new PauseSet(lines) : EMPTY;
|
|
11492
|
+
}
|
|
11493
|
+
var parsed = null;
|
|
11494
|
+
function currentPauseSet() {
|
|
11495
|
+
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
11496
|
+
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
11497
|
+
return parsed.set;
|
|
11498
|
+
}
|
|
11499
|
+
function pauseGateFromTestInfo(testInfo) {
|
|
11500
|
+
const use = testInfo.project?.use ?? {};
|
|
11501
|
+
return { ci: process.env.CI, headless: use.headless };
|
|
11502
|
+
}
|
|
11503
|
+
function pauseSkipReason(gate) {
|
|
11504
|
+
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
11505
|
+
if (gate.headless !== false)
|
|
11506
|
+
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
11507
|
+
return null;
|
|
11508
|
+
}
|
|
11509
|
+
function rearmedTimeout(before, pausedMs) {
|
|
11510
|
+
return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
|
|
11511
|
+
}
|
|
11512
|
+
function pausePlace(callerLocation) {
|
|
11513
|
+
const col = callerLocation.lastIndexOf(":");
|
|
11514
|
+
const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
|
|
11515
|
+
return fileLine.slice(fileLine.lastIndexOf("/") + 1);
|
|
11516
|
+
}
|
|
11517
|
+
function assertionLabel(expression, isNot) {
|
|
11518
|
+
const [first = "", ...rest] = expression.split(".").filter(Boolean);
|
|
11519
|
+
const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
11520
|
+
return `${isNot ? "not." : ""}${name || "expect"}`;
|
|
11521
|
+
}
|
|
11522
|
+
function pickPlace(callerLocation) {
|
|
11523
|
+
const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
|
|
11524
|
+
if (!m) return null;
|
|
11525
|
+
const file = m[1];
|
|
11526
|
+
if (!isInsidePath(file)) return null;
|
|
11527
|
+
return { file, line: Number(m[2]) };
|
|
11528
|
+
}
|
|
11529
|
+
function pickPayload(locator, callerLocation) {
|
|
11530
|
+
const at = pickPlace(callerLocation);
|
|
11531
|
+
return at ? { kind: "locator", text: locator, at } : { kind: "locator", text: locator };
|
|
11532
|
+
}
|
|
11533
|
+
var postFailureLogged = false;
|
|
11534
|
+
async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
|
|
11535
|
+
if (!pairingAddress) return false;
|
|
11536
|
+
const logOnce = (why) => {
|
|
11537
|
+
if (postFailureLogged) return;
|
|
11538
|
+
postFailureLogged = true;
|
|
11539
|
+
console.log(`[piwi] Could not send the picked locator to the editor: ${why}.`);
|
|
11540
|
+
};
|
|
11541
|
+
const pairing = parsePairing(pairingAddress);
|
|
11542
|
+
if (!pairing) {
|
|
11543
|
+
logOnce(`${PIWI_LOCAL_DEBUG_ENV.editorSend} is not a pairing address`);
|
|
11544
|
+
return false;
|
|
11545
|
+
}
|
|
11546
|
+
try {
|
|
11547
|
+
const response = await fetchImpl(pairing.url, {
|
|
11548
|
+
method: "POST",
|
|
11549
|
+
headers: { Authorization: `Bearer ${pairing.token}`, "Content-Type": "application/json" },
|
|
11550
|
+
body: JSON.stringify(payload),
|
|
11551
|
+
signal: AbortSignal.timeout(5e3)
|
|
11552
|
+
});
|
|
11553
|
+
if (response.ok) return true;
|
|
11554
|
+
logOnce(`the editor answered ${response.status}`);
|
|
11555
|
+
} catch (error) {
|
|
11556
|
+
logOnce(error instanceof Error ? error.message : String(error));
|
|
11557
|
+
}
|
|
11558
|
+
return false;
|
|
11559
|
+
}
|
|
11560
|
+
var skipLogged = false;
|
|
11561
|
+
function logPauseSkipOnce(reason) {
|
|
11562
|
+
if (skipLogged) return;
|
|
11563
|
+
skipLogged = true;
|
|
11564
|
+
console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
|
|
11565
|
+
}
|
|
11566
|
+
var HIGHLIGHT_MS = 2e3;
|
|
11567
|
+
async function pauseHere(page, testInfo, point, probe, onPick) {
|
|
11568
|
+
const before = testInfo.timeout;
|
|
11569
|
+
const started = Date.now();
|
|
11570
|
+
let shown = null;
|
|
11571
|
+
try {
|
|
11572
|
+
testInfo.setTimeout(0);
|
|
11573
|
+
const highlight = point.target?.highlight;
|
|
11574
|
+
if (typeof highlight === "function") {
|
|
11575
|
+
let timer;
|
|
11576
|
+
shown = await Promise.race([
|
|
11577
|
+
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
11578
|
+
new Promise((resolve7) => {
|
|
11579
|
+
timer = setTimeout(() => resolve7(null), HIGHLIGHT_MS);
|
|
11580
|
+
})
|
|
11581
|
+
]);
|
|
11582
|
+
clearTimeout(timer);
|
|
11583
|
+
}
|
|
11584
|
+
const arg = {
|
|
11585
|
+
place: pausePlace(point.location),
|
|
11586
|
+
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
11587
|
+
action: point.action,
|
|
11588
|
+
locator: point.locator
|
|
11589
|
+
};
|
|
11590
|
+
console.log(
|
|
11591
|
+
`[piwi] Paused at ${point.location} before ${point.locator ? `${point.action} \xB7 ${point.locator}` : point.action}: Resume, Step, Pick a locator or Finish in the browser (Esc resumes).`
|
|
11592
|
+
);
|
|
11593
|
+
for (; ; ) {
|
|
11594
|
+
await page.evaluate(showPauseBar, arg);
|
|
11595
|
+
await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
|
|
11596
|
+
const choice = await page.evaluate(takePauseState);
|
|
11597
|
+
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
11598
|
+
if (choice !== "pick") continue;
|
|
11599
|
+
const picked = await pickLocator(page, probe, null).catch(() => null);
|
|
11600
|
+
if (picked) await onPick(picked);
|
|
11601
|
+
}
|
|
11602
|
+
} catch {
|
|
11603
|
+
return "resume";
|
|
11604
|
+
} finally {
|
|
11605
|
+
try {
|
|
11606
|
+
const dispose = shown?.dispose;
|
|
11607
|
+
const hide = page.hideHighlight;
|
|
11608
|
+
if (typeof dispose === "function") await dispose.call(shown);
|
|
11609
|
+
else if (typeof hide === "function") await hide.call(page);
|
|
11610
|
+
} catch {
|
|
11611
|
+
}
|
|
11612
|
+
try {
|
|
11613
|
+
testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
|
|
11614
|
+
} catch {
|
|
11615
|
+
}
|
|
11616
|
+
}
|
|
11617
|
+
}
|
|
11618
|
+
|
|
11330
11619
|
// src/internal/capture/code-reach.ts
|
|
11331
11620
|
var crypto3 = __toESM(require("crypto"));
|
|
11332
11621
|
var fs22 = __toESM(require("fs"));
|
|
11333
|
-
var
|
|
11622
|
+
var path27 = __toESM(require("path"));
|
|
11334
11623
|
var import_node_child_process3 = require("child_process");
|
|
11335
11624
|
|
|
11336
11625
|
// ../core/src/source-map.ts
|
|
@@ -11593,12 +11882,12 @@ function repositoryRoot(from) {
|
|
|
11593
11882
|
}).trim();
|
|
11594
11883
|
} catch {
|
|
11595
11884
|
}
|
|
11596
|
-
cachedRepoRoot = { from, root:
|
|
11885
|
+
cachedRepoRoot = { from, root: path27.resolve(root) };
|
|
11597
11886
|
return cachedRepoRoot.root;
|
|
11598
11887
|
}
|
|
11599
11888
|
function codeReachRoots(configDir, configured) {
|
|
11600
11889
|
const repoRoot = repositoryRoot(configDir);
|
|
11601
|
-
const roots = configured?.length ? configured.map((r) =>
|
|
11890
|
+
const roots = configured?.length ? configured.map((r) => path27.resolve(configDir, r)) : [configDir];
|
|
11602
11891
|
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
11603
11892
|
}
|
|
11604
11893
|
function isFile(file) {
|
|
@@ -11611,10 +11900,10 @@ function isFile(file) {
|
|
|
11611
11900
|
function resolveSourcePath(source, roots) {
|
|
11612
11901
|
const normalized = normalizeSourcePath(source);
|
|
11613
11902
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
11614
|
-
if (
|
|
11615
|
-
const
|
|
11903
|
+
if (path27.isAbsolute(normalized) && isFile(normalized)) return path27.resolve(normalized);
|
|
11904
|
+
const relative14 = normalized.replace(/^\/+/, "");
|
|
11616
11905
|
for (const root of roots) {
|
|
11617
|
-
const candidate =
|
|
11906
|
+
const candidate = path27.resolve(root, relative14);
|
|
11618
11907
|
if (isFile(candidate)) return candidate;
|
|
11619
11908
|
}
|
|
11620
11909
|
return null;
|
|
@@ -11630,10 +11919,10 @@ function viteBases(entries) {
|
|
|
11630
11919
|
function fileForUrl(url, roots, bases) {
|
|
11631
11920
|
let pathname;
|
|
11632
11921
|
try {
|
|
11633
|
-
const
|
|
11634
|
-
if (
|
|
11635
|
-
pathname =
|
|
11636
|
-
const base = bases.get(
|
|
11922
|
+
const parsed2 = new URL(url);
|
|
11923
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
|
|
11924
|
+
pathname = parsed2.pathname;
|
|
11925
|
+
const base = bases.get(parsed2.origin);
|
|
11637
11926
|
if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
|
|
11638
11927
|
} catch {
|
|
11639
11928
|
return null;
|
|
@@ -11646,7 +11935,7 @@ function fileForUrl(url, roots, bases) {
|
|
|
11646
11935
|
return file;
|
|
11647
11936
|
}
|
|
11648
11937
|
function relativeToRepo(file, repoRoot) {
|
|
11649
|
-
return
|
|
11938
|
+
return path27.relative(repoRoot, file).split(path27.sep).join("/");
|
|
11650
11939
|
}
|
|
11651
11940
|
function scriptHash(entry) {
|
|
11652
11941
|
return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
@@ -11816,6 +12105,7 @@ function createSink() {
|
|
|
11816
12105
|
flake: null,
|
|
11817
12106
|
pickOffered: false,
|
|
11818
12107
|
userPick: null,
|
|
12108
|
+
pause: null,
|
|
11819
12109
|
locatorPages: new LocatorPageLog(),
|
|
11820
12110
|
codeReach: null
|
|
11821
12111
|
};
|
|
@@ -11832,11 +12122,11 @@ function configuredCodeReachRoots() {
|
|
|
11832
12122
|
const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
|
|
11833
12123
|
if (!raw) return null;
|
|
11834
12124
|
try {
|
|
11835
|
-
const
|
|
11836
|
-
if (Array.isArray(
|
|
12125
|
+
const parsed2 = JSON.parse(raw);
|
|
12126
|
+
if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
|
|
11837
12127
|
} catch {
|
|
11838
12128
|
}
|
|
11839
|
-
return raw.split(
|
|
12129
|
+
return raw.split(path28.delimiter).filter(Boolean);
|
|
11840
12130
|
}
|
|
11841
12131
|
async function stopSinkCodeReach(sink, closing) {
|
|
11842
12132
|
const session = sink.codeReach;
|
|
@@ -11851,7 +12141,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
11851
12141
|
if (!entries) return;
|
|
11852
12142
|
const info = sink.testInfo;
|
|
11853
12143
|
const configFile = info?.config.configFile;
|
|
11854
|
-
const configDir = configFile ?
|
|
12144
|
+
const configDir = configFile ? path28.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
11855
12145
|
session.files = await resolveCodeReach(
|
|
11856
12146
|
entries,
|
|
11857
12147
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -12270,6 +12560,53 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
|
12270
12560
|
} catch {
|
|
12271
12561
|
}
|
|
12272
12562
|
}
|
|
12563
|
+
function pausesAt(pause, callerLocation) {
|
|
12564
|
+
return !pause.finished && (pause.next || pause.set.matches(callerLocation));
|
|
12565
|
+
}
|
|
12566
|
+
function locatorSource(target, method, args) {
|
|
12567
|
+
try {
|
|
12568
|
+
const text = String(target);
|
|
12569
|
+
if (text && !text.startsWith("[object")) return text;
|
|
12570
|
+
} catch {
|
|
12571
|
+
}
|
|
12572
|
+
return renderFailing({ method, args });
|
|
12573
|
+
}
|
|
12574
|
+
async function pauseBefore(sink, page, target, callerLocation, action, origin) {
|
|
12575
|
+
const pause = sink.pause;
|
|
12576
|
+
const testInfo = sink.testInfo;
|
|
12577
|
+
if (!pause || !testInfo || !callerLocation) return;
|
|
12578
|
+
pause.next = false;
|
|
12579
|
+
try {
|
|
12580
|
+
const locator = locatorSource(target, origin.method, origin.args);
|
|
12581
|
+
const onPick = async (picked) => {
|
|
12582
|
+
const pick = {
|
|
12583
|
+
failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
|
|
12584
|
+
...picked
|
|
12585
|
+
};
|
|
12586
|
+
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
12587
|
+
sink.probedLocations.add(callerLocation);
|
|
12588
|
+
const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
|
|
12589
|
+
console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
|
|
12590
|
+
await postPickToEditor(
|
|
12591
|
+
process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
|
|
12592
|
+
pickPayload(picked.picked.locator, callerLocation)
|
|
12593
|
+
);
|
|
12594
|
+
};
|
|
12595
|
+
const choice = await internalCall(
|
|
12596
|
+
page,
|
|
12597
|
+
() => pauseHere(
|
|
12598
|
+
page,
|
|
12599
|
+
testInfo,
|
|
12600
|
+
{ location: callerLocation, action, locator, target },
|
|
12601
|
+
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
12602
|
+
onPick
|
|
12603
|
+
)
|
|
12604
|
+
);
|
|
12605
|
+
if (choice === "step") pause.next = true;
|
|
12606
|
+
else if (choice === "finish") pause.finished = true;
|
|
12607
|
+
} catch {
|
|
12608
|
+
}
|
|
12609
|
+
}
|
|
12273
12610
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
12274
12611
|
return new Proxy(locator, {
|
|
12275
12612
|
get(target, prop) {
|
|
@@ -12291,6 +12628,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12291
12628
|
const isNot = Boolean(callArgs[1]?.isNot);
|
|
12292
12629
|
const callerLocation = sink ? captureCallerLocation() : null;
|
|
12293
12630
|
if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
|
|
12631
|
+
if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
|
|
12632
|
+
await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
|
|
12633
|
+
method: originMethod,
|
|
12634
|
+
args: originArgs
|
|
12635
|
+
});
|
|
12636
|
+
}
|
|
12294
12637
|
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
12295
12638
|
return callMethod(target, prop, callArgs);
|
|
12296
12639
|
}
|
|
@@ -12329,6 +12672,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12329
12672
|
args: originArgs,
|
|
12330
12673
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
12331
12674
|
};
|
|
12675
|
+
if (sink.pause && pausesAt(sink.pause, callerLocation)) {
|
|
12676
|
+
await pauseBefore(sink, page, target, callerLocation, String(prop), used);
|
|
12677
|
+
}
|
|
12332
12678
|
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
12333
12679
|
let seq = -1;
|
|
12334
12680
|
if (!alreadyProbed) {
|
|
@@ -12789,6 +13135,12 @@ var piwiFixtures = {
|
|
|
12789
13135
|
if (conflict) throw new Error(conflict);
|
|
12790
13136
|
const sink = createSink();
|
|
12791
13137
|
sink.testInfo = testInfo;
|
|
13138
|
+
const pauseSet = currentPauseSet();
|
|
13139
|
+
if (pauseSet.size) {
|
|
13140
|
+
const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
|
|
13141
|
+
if (reason) logPauseSkipOnce(reason);
|
|
13142
|
+
else sink.pause = { set: pauseSet, next: false, finished: false };
|
|
13143
|
+
}
|
|
12792
13144
|
if (isFlakeMode()) {
|
|
12793
13145
|
const plan = loadFlakePlan();
|
|
12794
13146
|
const role = flakeRoleForTest(plan, {
|
|
@@ -12868,12 +13220,12 @@ function extendPiwiFixtures(test) {
|
|
|
12868
13220
|
|
|
12869
13221
|
// src/internal/ai/ai-fixtures.ts
|
|
12870
13222
|
var fs24 = __toESM(require("fs"));
|
|
12871
|
-
var
|
|
13223
|
+
var path31 = __toESM(require("path"));
|
|
12872
13224
|
var import_test = require("@playwright/test");
|
|
12873
13225
|
|
|
12874
13226
|
// src/internal/ai/artifact.ts
|
|
12875
13227
|
var fs23 = __toESM(require("fs"));
|
|
12876
|
-
var
|
|
13228
|
+
var path29 = __toESM(require("path"));
|
|
12877
13229
|
var ARTIFACT_VERSION = 1;
|
|
12878
13230
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
12879
13231
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -12977,7 +13329,7 @@ function writeEntry(file, entry) {
|
|
|
12977
13329
|
const canonical = serializeEntry(entry);
|
|
12978
13330
|
return withEntryLock(file, () => {
|
|
12979
13331
|
if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
12980
|
-
fs23.mkdirSync(
|
|
13332
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12981
13333
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
12982
13334
|
fs23.writeFileSync(tmp, canonical);
|
|
12983
13335
|
fs23.renameSync(tmp, file);
|
|
@@ -12986,7 +13338,7 @@ function writeEntry(file, entry) {
|
|
|
12986
13338
|
}
|
|
12987
13339
|
function withEntryLock(file, fn) {
|
|
12988
13340
|
const lock = `${file}.lock`;
|
|
12989
|
-
fs23.mkdirSync(
|
|
13341
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12990
13342
|
const deadline = Date.now() + 5e3;
|
|
12991
13343
|
for (; ; ) {
|
|
12992
13344
|
try {
|
|
@@ -13015,7 +13367,7 @@ function withEntryLock(file, fn) {
|
|
|
13015
13367
|
|
|
13016
13368
|
// src/internal/ai/keys.ts
|
|
13017
13369
|
var crypto4 = __toESM(require("crypto"));
|
|
13018
|
-
var
|
|
13370
|
+
var path30 = __toESM(require("path"));
|
|
13019
13371
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
13020
13372
|
function normalizeTemplate(template) {
|
|
13021
13373
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -13029,13 +13381,13 @@ function slug(text, maxLength = 40) {
|
|
|
13029
13381
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
13030
13382
|
}
|
|
13031
13383
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
13032
|
-
return
|
|
13384
|
+
return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
|
|
13033
13385
|
}
|
|
13034
13386
|
function entryPath(params) {
|
|
13035
13387
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
13036
13388
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
13037
13389
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
13038
|
-
return
|
|
13390
|
+
return path30.join(entryDir(params.specFile, params.dir), name);
|
|
13039
13391
|
}
|
|
13040
13392
|
function parseLocation2(location) {
|
|
13041
13393
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -13215,9 +13567,9 @@ function renderSourceArg(arg) {
|
|
|
13215
13567
|
if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
|
|
13216
13568
|
return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
|
|
13217
13569
|
}
|
|
13218
|
-
function
|
|
13570
|
+
function locatorSource2(loc) {
|
|
13219
13571
|
const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
|
|
13220
|
-
const tail = (loc.chain ?? []).map(
|
|
13572
|
+
const tail = (loc.chain ?? []).map(locatorSource2).join(".");
|
|
13221
13573
|
return tail ? `${head}.${tail}` : head;
|
|
13222
13574
|
}
|
|
13223
13575
|
function describePostcondition(post) {
|
|
@@ -13348,6 +13700,97 @@ function compileFromCandidate(candidate) {
|
|
|
13348
13700
|
return null;
|
|
13349
13701
|
}
|
|
13350
13702
|
|
|
13703
|
+
// src/internal/ai/ground.ts
|
|
13704
|
+
var GROUNDED_METHODS = new Set(LOCATOR_METHODS.filter((method) => method !== "locator"));
|
|
13705
|
+
async function refLocator(page, ref) {
|
|
13706
|
+
if (!/^[a-z0-9]+$/i.test(ref)) return null;
|
|
13707
|
+
const getByRef = page.getByRef;
|
|
13708
|
+
try {
|
|
13709
|
+
const target = typeof getByRef === "function" ? getByRef.call(page, ref) : page.locator(`aria-ref=${ref}`);
|
|
13710
|
+
return await target.count() === 1 ? target : null;
|
|
13711
|
+
} catch {
|
|
13712
|
+
return null;
|
|
13713
|
+
}
|
|
13714
|
+
}
|
|
13715
|
+
async function matchesOnly(candidate, target) {
|
|
13716
|
+
try {
|
|
13717
|
+
return await candidate.count() === 1 && await candidate.and(target).count() === 1;
|
|
13718
|
+
} catch {
|
|
13719
|
+
return false;
|
|
13720
|
+
}
|
|
13721
|
+
}
|
|
13722
|
+
async function includes(candidate, target) {
|
|
13723
|
+
try {
|
|
13724
|
+
return await candidate.and(target).count() === 1;
|
|
13725
|
+
} catch {
|
|
13726
|
+
return false;
|
|
13727
|
+
}
|
|
13728
|
+
}
|
|
13729
|
+
function plainArg(arg, params, mask = true) {
|
|
13730
|
+
switch (arg.type) {
|
|
13731
|
+
case "string":
|
|
13732
|
+
return mask ? maskValues(arg.value, params) : arg.value;
|
|
13733
|
+
case "number":
|
|
13734
|
+
case "boolean":
|
|
13735
|
+
return arg.value;
|
|
13736
|
+
case "object": {
|
|
13737
|
+
const out = {};
|
|
13738
|
+
for (const [key, value] of arg.entries) {
|
|
13739
|
+
const plain = plainArg(value, params, mask);
|
|
13740
|
+
if (plain === void 0) return void 0;
|
|
13741
|
+
out[key] = plain;
|
|
13742
|
+
}
|
|
13743
|
+
return out;
|
|
13744
|
+
}
|
|
13745
|
+
default:
|
|
13746
|
+
return void 0;
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
function chainToStructured(calls, params = {}) {
|
|
13750
|
+
const links = [];
|
|
13751
|
+
for (const call of calls) {
|
|
13752
|
+
if (!GROUNDED_METHODS.has(call.method)) return null;
|
|
13753
|
+
const args = [];
|
|
13754
|
+
for (const arg of call.args) {
|
|
13755
|
+
const plain = plainArg(arg, params, call.method !== "getByTestId");
|
|
13756
|
+
if (plain === void 0) return null;
|
|
13757
|
+
args.push(plain);
|
|
13758
|
+
}
|
|
13759
|
+
links.push({ method: call.method, args });
|
|
13760
|
+
}
|
|
13761
|
+
const [head, ...chain] = links;
|
|
13762
|
+
if (!head) return null;
|
|
13763
|
+
return chain.length > 0 ? { ...head, chain } : head;
|
|
13764
|
+
}
|
|
13765
|
+
async function normalizedLocator(target, params) {
|
|
13766
|
+
if (typeof target.normalize !== "function") return null;
|
|
13767
|
+
try {
|
|
13768
|
+
const chain = tryParseLocatorChain(String(await target.normalize()));
|
|
13769
|
+
return chain ? chainToStructured(chain.calls, params) : null;
|
|
13770
|
+
} catch {
|
|
13771
|
+
return null;
|
|
13772
|
+
}
|
|
13773
|
+
}
|
|
13774
|
+
async function groundElement(page, element, params) {
|
|
13775
|
+
const compiled = compileFromCandidate({
|
|
13776
|
+
role: element.role,
|
|
13777
|
+
name: element.name ?? null,
|
|
13778
|
+
level: element.level ?? null
|
|
13779
|
+
});
|
|
13780
|
+
const target = element.ref ? await refLocator(page, element.ref) : null;
|
|
13781
|
+
if (!target) return compiled;
|
|
13782
|
+
if (compiled) {
|
|
13783
|
+
const byName = buildLocator(page, compiled.locator, params);
|
|
13784
|
+
if (await matchesOnly(byName, target)) return compiled;
|
|
13785
|
+
if (!await includes(byName, target)) return compiled;
|
|
13786
|
+
}
|
|
13787
|
+
const normalized = await normalizedLocator(target, params);
|
|
13788
|
+
if (!normalized || !await matchesOnly(buildLocator(page, normalized, params), target)) return compiled;
|
|
13789
|
+
const fingerprint = compiled?.fingerprint ?? { role: element.role, name: element.name ?? null };
|
|
13790
|
+
if (!compiled && element.level != null) fingerprint.level = element.level;
|
|
13791
|
+
return { locator: normalized, fingerprint, score: compiled?.score ?? 0 };
|
|
13792
|
+
}
|
|
13793
|
+
|
|
13351
13794
|
// src/internal/ai/resolution.ts
|
|
13352
13795
|
var MAX_SNAPSHOT_CHARS = 24e3;
|
|
13353
13796
|
var DEFAULT_MAX_STEPS = 20;
|
|
@@ -13368,12 +13811,6 @@ var ServerStepResolver = class {
|
|
|
13368
13811
|
return await res.json();
|
|
13369
13812
|
}
|
|
13370
13813
|
};
|
|
13371
|
-
function candidateOf(element) {
|
|
13372
|
-
return { role: element.role, name: element.name ?? null, level: element.level ?? null };
|
|
13373
|
-
}
|
|
13374
|
-
function locatorFromElement(element) {
|
|
13375
|
-
return compileFromCandidate(candidateOf(element));
|
|
13376
|
-
}
|
|
13377
13814
|
function lazyLocator(resolveOnce) {
|
|
13378
13815
|
let pending = null;
|
|
13379
13816
|
const ensure = () => pending ??= resolveOnce();
|
|
@@ -13385,20 +13822,21 @@ function lazyLocator(resolveOnce) {
|
|
|
13385
13822
|
};
|
|
13386
13823
|
return new Proxy({}, handler);
|
|
13387
13824
|
}
|
|
13388
|
-
function buildPostcondition(resolved, template) {
|
|
13825
|
+
async function buildPostcondition(resolved, template, ctx) {
|
|
13389
13826
|
if (!resolved)
|
|
13390
13827
|
throw new Error(`piwi AI: flow "${template}" resolved without a postcondition (the oracle is mandatory)`);
|
|
13391
13828
|
if (resolved.assert === "url") {
|
|
13392
13829
|
if (!resolved.url) throw new Error(`piwi AI: flow "${template}" postcondition is a url assert without a url`);
|
|
13393
13830
|
return { assert: "url", url: resolved.url };
|
|
13394
13831
|
}
|
|
13395
|
-
const compiled = resolved.element ?
|
|
13832
|
+
const compiled = resolved.element ? await groundElement(ctx.page, resolved.element, ctx.params) : null;
|
|
13396
13833
|
if (!compiled) throw new Error(`piwi AI: flow "${template}" postcondition element could not be compiled`);
|
|
13397
13834
|
return { assert: resolved.assert, locator: compiled.locator };
|
|
13398
13835
|
}
|
|
13399
13836
|
var readMaskedSnapshot = async (page, params) => {
|
|
13400
13837
|
const raw = await ariaSnapshotBestEffort(page.locator("body"));
|
|
13401
|
-
|
|
13838
|
+
if (!raw) return "";
|
|
13839
|
+
return raw.split(/(\[ref=\w+\])/).map((part, i) => i % 2 === 1 ? part : maskValues(part, params)).join("");
|
|
13402
13840
|
};
|
|
13403
13841
|
var observeXhrResponses = async (page, action, settleMs) => {
|
|
13404
13842
|
const urls = [];
|
|
@@ -13450,7 +13888,7 @@ async function resolveLocator(template, ctx) {
|
|
|
13450
13888
|
...screenshot ? { screenshot } : {}
|
|
13451
13889
|
});
|
|
13452
13890
|
if (!decision.element) throw new Error(`piwi AI: resolver returned no element for "${template}"`);
|
|
13453
|
-
const compiled =
|
|
13891
|
+
const compiled = await groundElement(ctx.page, decision.element, ctx.params);
|
|
13454
13892
|
if (!compiled) throw new Error(`piwi AI: element for "${template}" could not be compiled to a stable locator`);
|
|
13455
13893
|
const entry = {
|
|
13456
13894
|
version: ARTIFACT_VERSION,
|
|
@@ -13483,14 +13921,14 @@ async function resolveRun(template, ctx) {
|
|
|
13483
13921
|
...screenshot ? { screenshot } : {}
|
|
13484
13922
|
});
|
|
13485
13923
|
if (decision.done) {
|
|
13486
|
-
const postcondition = buildPostcondition(decision.postcondition, template);
|
|
13924
|
+
const postcondition = await buildPostcondition(decision.postcondition, template, ctx);
|
|
13487
13925
|
await assertPostcondition(postcondition, { page: ctx.page, params: ctx.params });
|
|
13488
13926
|
return { version: ARTIFACT_VERSION, kind: "run", template, steps, postcondition };
|
|
13489
13927
|
}
|
|
13490
13928
|
if (!decision.element || !decision.action) {
|
|
13491
13929
|
throw new Error(`piwi AI: resolver returned neither a step nor done for "${template}"`);
|
|
13492
13930
|
}
|
|
13493
|
-
const compiled =
|
|
13931
|
+
const compiled = await groundElement(ctx.page, decision.element, ctx.params);
|
|
13494
13932
|
if (!compiled) throw new Error(`piwi AI: a step element for "${template}" could not be compiled`);
|
|
13495
13933
|
const step = { locator: compiled.locator, action: decision.action, fingerprint: compiled.fingerprint };
|
|
13496
13934
|
if (decision.value !== void 0) step.value = decision.value;
|
|
@@ -13549,18 +13987,18 @@ function testIdentity(testInfo) {
|
|
|
13549
13987
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
13550
13988
|
}
|
|
13551
13989
|
function relativeToCwd(file) {
|
|
13552
|
-
return
|
|
13990
|
+
return path31.relative(process.cwd(), file).split(path31.sep).join("/");
|
|
13553
13991
|
}
|
|
13554
13992
|
function recordIntents(intents, entry) {
|
|
13555
13993
|
const add = (locator, kind) => {
|
|
13556
13994
|
intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
|
|
13557
13995
|
};
|
|
13558
13996
|
if (entry.kind === "locator") {
|
|
13559
|
-
add(
|
|
13997
|
+
add(locatorSource2(entry.locator), "locator");
|
|
13560
13998
|
return;
|
|
13561
13999
|
}
|
|
13562
|
-
for (const step of entry.steps) add(
|
|
13563
|
-
if (entry.postcondition.locator) add(
|
|
14000
|
+
for (const step of entry.steps) add(locatorSource2(step.locator), "run");
|
|
14001
|
+
if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
|
|
13564
14002
|
}
|
|
13565
14003
|
function missMessage(template, testTitle, relPath) {
|
|
13566
14004
|
return [
|