@piwitests/reporter 0.48.0 → 0.49.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/index.js +554 -209
- 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/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;
|
|
@@ -3933,14 +3937,14 @@ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
|
3933
3937
|
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
3934
3938
|
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
3935
3939
|
function parseManifestJson(text) {
|
|
3936
|
-
let
|
|
3940
|
+
let parsed2;
|
|
3937
3941
|
try {
|
|
3938
|
-
|
|
3942
|
+
parsed2 = JSON.parse(text);
|
|
3939
3943
|
} catch {
|
|
3940
3944
|
return null;
|
|
3941
3945
|
}
|
|
3942
|
-
if (!
|
|
3943
|
-
const obj =
|
|
3946
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
3947
|
+
const obj = parsed2;
|
|
3944
3948
|
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
3949
|
method: r.method,
|
|
3946
3950
|
pattern: r.pattern,
|
|
@@ -4144,8 +4148,8 @@ function parseRouteKey(routeKey2) {
|
|
|
4144
4148
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
4145
4149
|
}
|
|
4146
4150
|
function patternToRegex(pattern) {
|
|
4147
|
-
const
|
|
4148
|
-
const escaped =
|
|
4151
|
+
const path32 = pattern.split("?")[0] ?? pattern;
|
|
4152
|
+
const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
4149
4153
|
return new RegExp(`^${escaped}$`);
|
|
4150
4154
|
}
|
|
4151
4155
|
function suitePathEqual(a, b) {
|
|
@@ -4168,13 +4172,13 @@ function matchProbeItem(plan, test) {
|
|
|
4168
4172
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
4169
4173
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
4170
4174
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
4171
|
-
let
|
|
4175
|
+
let path32;
|
|
4172
4176
|
try {
|
|
4173
|
-
|
|
4177
|
+
path32 = new URL(url).pathname;
|
|
4174
4178
|
} catch {
|
|
4175
|
-
|
|
4179
|
+
path32 = url.split("?")[0] ?? url;
|
|
4176
4180
|
}
|
|
4177
|
-
return patternToRegex(pattern).test(
|
|
4181
|
+
return patternToRegex(pattern).test(path32);
|
|
4178
4182
|
}
|
|
4179
4183
|
function createProbeState() {
|
|
4180
4184
|
return { navigated: false, matches: 0 };
|
|
@@ -4196,10 +4200,10 @@ function isProbeMode() {
|
|
|
4196
4200
|
function loadPlan() {
|
|
4197
4201
|
if (planCache !== void 0) return planCache;
|
|
4198
4202
|
planCache = null;
|
|
4199
|
-
const
|
|
4200
|
-
if (
|
|
4203
|
+
const path32 = process.env[PIWI_PROBE_ENV.plan];
|
|
4204
|
+
if (path32) {
|
|
4201
4205
|
try {
|
|
4202
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
4206
|
+
planCache = JSON.parse(fs14.readFileSync(path32, "utf8"));
|
|
4203
4207
|
} catch {
|
|
4204
4208
|
planCache = null;
|
|
4205
4209
|
}
|
|
@@ -4221,10 +4225,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
4221
4225
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
4222
4226
|
}
|
|
4223
4227
|
function recordProbeOutcome(line) {
|
|
4224
|
-
const
|
|
4225
|
-
if (!
|
|
4228
|
+
const path32 = process.env[PIWI_PROBE_ENV.results];
|
|
4229
|
+
if (!path32) return;
|
|
4226
4230
|
try {
|
|
4227
|
-
fs14.appendFileSync(
|
|
4231
|
+
fs14.appendFileSync(path32, `${JSON.stringify(line)}
|
|
4228
4232
|
`);
|
|
4229
4233
|
} catch {
|
|
4230
4234
|
}
|
|
@@ -4447,8 +4451,8 @@ var FLAKE_LAB_RUN_METADATA_KEY = "piwiFlakeLab";
|
|
|
4447
4451
|
// ../core/src/page-key.ts
|
|
4448
4452
|
function normalizeRoute(url) {
|
|
4449
4453
|
try {
|
|
4450
|
-
const
|
|
4451
|
-
let pathname =
|
|
4454
|
+
const parsed2 = new URL(url);
|
|
4455
|
+
let pathname = parsed2.pathname;
|
|
4452
4456
|
pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
|
|
4453
4457
|
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
4458
|
pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
|
|
@@ -4456,8 +4460,8 @@ function normalizeRoute(url) {
|
|
|
4456
4460
|
pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
|
|
4457
4461
|
pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
4458
4462
|
let queryString = "";
|
|
4459
|
-
if (
|
|
4460
|
-
const params = new URLSearchParams(
|
|
4463
|
+
if (parsed2.search) {
|
|
4464
|
+
const params = new URLSearchParams(parsed2.search);
|
|
4461
4465
|
const redacted = new URLSearchParams();
|
|
4462
4466
|
for (const [key] of params) {
|
|
4463
4467
|
redacted.set(key, "<redacted>");
|
|
@@ -4509,24 +4513,24 @@ function faultRouteAction(fault) {
|
|
|
4509
4513
|
return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
|
|
4510
4514
|
}
|
|
4511
4515
|
function dropOneField(body) {
|
|
4512
|
-
let
|
|
4516
|
+
let parsed2;
|
|
4513
4517
|
try {
|
|
4514
|
-
|
|
4518
|
+
parsed2 = JSON.parse(body);
|
|
4515
4519
|
} catch {
|
|
4516
4520
|
return body;
|
|
4517
4521
|
}
|
|
4518
|
-
if (Array.isArray(
|
|
4522
|
+
if (Array.isArray(parsed2)) {
|
|
4519
4523
|
let key;
|
|
4520
|
-
for (const el of
|
|
4524
|
+
for (const el of parsed2) {
|
|
4521
4525
|
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
4522
4526
|
key ??= Object.keys(el)[0];
|
|
4523
4527
|
if (key) delete el[key];
|
|
4524
4528
|
}
|
|
4525
4529
|
}
|
|
4526
|
-
return JSON.stringify(
|
|
4530
|
+
return JSON.stringify(parsed2);
|
|
4527
4531
|
}
|
|
4528
|
-
if (
|
|
4529
|
-
const obj =
|
|
4532
|
+
if (parsed2 && typeof parsed2 === "object") {
|
|
4533
|
+
const obj = parsed2;
|
|
4530
4534
|
const key = Object.keys(obj)[0];
|
|
4531
4535
|
if (key) delete obj[key];
|
|
4532
4536
|
return JSON.stringify(obj);
|
|
@@ -5334,8 +5338,8 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
5334
5338
|
|
|
5335
5339
|
// src/internal/submit/run-submitter.ts
|
|
5336
5340
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
5337
|
-
function markSubmitFallback(run,
|
|
5338
|
-
run.metadata.ingestHealth = { submitFallback: { path:
|
|
5341
|
+
function markSubmitFallback(run, path32, reason) {
|
|
5342
|
+
run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
|
|
5339
5343
|
}
|
|
5340
5344
|
var RunSubmitter = class {
|
|
5341
5345
|
/**
|
|
@@ -7895,7 +7899,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
7895
7899
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
7896
7900
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
7897
7901
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
7898
|
-
const
|
|
7902
|
+
const relative14 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
7899
7903
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
7900
7904
|
const perWorker = Math.max(
|
|
7901
7905
|
60,
|
|
@@ -7903,17 +7907,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
7903
7907
|
);
|
|
7904
7908
|
return {
|
|
7905
7909
|
startedAt,
|
|
7906
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
7907
|
-
memoryBytes: hasSamples ? downsamplePoints(
|
|
7910
|
+
cpuPct: hasSamples ? downsamplePoints(relative14(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
7911
|
+
memoryBytes: hasSamples ? downsamplePoints(relative14(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
7908
7912
|
pages: workers.map(({ worker, events }) => {
|
|
7909
|
-
const points =
|
|
7913
|
+
const points = relative14(events, -startedAt);
|
|
7910
7914
|
const last = points.pop();
|
|
7911
7915
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
7912
7916
|
}),
|
|
7913
7917
|
workers: trees.map((tree) => ({
|
|
7914
7918
|
worker: tree.worker,
|
|
7915
|
-
cpuCores: downsamplePoints(
|
|
7916
|
-
memoryBytes: downsamplePoints(
|
|
7919
|
+
cpuCores: downsamplePoints(relative14(tree.cpuCores, shift), perWorker, "mean"),
|
|
7920
|
+
memoryBytes: downsamplePoints(relative14(tree.memoryBytes, shift), perWorker, "max")
|
|
7917
7921
|
}))
|
|
7918
7922
|
};
|
|
7919
7923
|
}
|
|
@@ -8076,8 +8080,8 @@ function formatTimeout(ms) {
|
|
|
8076
8080
|
}
|
|
8077
8081
|
function routeOf(url) {
|
|
8078
8082
|
try {
|
|
8079
|
-
const
|
|
8080
|
-
return `${
|
|
8083
|
+
const parsed2 = new URL(url);
|
|
8084
|
+
return `${parsed2.pathname}${parsed2.search}` || "/";
|
|
8081
8085
|
} catch {
|
|
8082
8086
|
return url;
|
|
8083
8087
|
}
|
|
@@ -8097,14 +8101,14 @@ function textOfGetByText(locator) {
|
|
|
8097
8101
|
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
8098
8102
|
return m ? m[2] : null;
|
|
8099
8103
|
}
|
|
8100
|
-
function receivedDisplay(
|
|
8101
|
-
const received =
|
|
8102
|
-
if (
|
|
8104
|
+
function receivedDisplay(parsed2) {
|
|
8105
|
+
const received = parsed2.received ?? "";
|
|
8106
|
+
if (parsed2.assertion !== "toHaveURL") return received;
|
|
8103
8107
|
const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
|
|
8104
8108
|
return m ? `"${routeOf(m[1])}"` : received;
|
|
8105
8109
|
}
|
|
8106
|
-
function firstLineFallback(
|
|
8107
|
-
const line = (
|
|
8110
|
+
function firstLineFallback(parsed2) {
|
|
8111
|
+
const line = (parsed2.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
|
|
8108
8112
|
return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
|
|
8109
8113
|
}
|
|
8110
8114
|
var Line = class {
|
|
@@ -8141,148 +8145,148 @@ function timeoutSuffix(timeoutMs) {
|
|
|
8141
8145
|
function timeoutParen(timeoutMs) {
|
|
8142
8146
|
return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
|
|
8143
8147
|
}
|
|
8144
|
-
function buildActionTimeout(
|
|
8148
|
+
function buildActionTimeout(parsed2, opts) {
|
|
8145
8149
|
const line = new Line();
|
|
8146
|
-
const verb = ACTION_VERBS[
|
|
8147
|
-
const state = STATE_PHRASES[
|
|
8148
|
-
if (
|
|
8149
|
-
return subject(line,
|
|
8150
|
+
const verb = ACTION_VERBS[parsed2.action ?? ""] ?? parsed2.action ?? "action";
|
|
8151
|
+
const state = STATE_PHRASES[parsed2.lastState];
|
|
8152
|
+
if (parsed2.locator && state) {
|
|
8153
|
+
return subject(line, parsed2.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8150
8154
|
}
|
|
8151
|
-
if (
|
|
8152
|
-
const count =
|
|
8153
|
-
return subject(line,
|
|
8155
|
+
if (parsed2.locator && parsed2.lastState === "resolved-count" && parsed2.resolvedCount !== null) {
|
|
8156
|
+
const count = parsed2.resolvedCount;
|
|
8157
|
+
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
8158
|
}
|
|
8155
|
-
if (
|
|
8156
|
-
return line.text(`${verb} on `).locator(opts.locator ??
|
|
8159
|
+
if (parsed2.locator) {
|
|
8160
|
+
return line.text(`${verb} on `).locator(opts.locator ?? parsed2.locator).text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8157
8161
|
}
|
|
8158
|
-
const call =
|
|
8159
|
-
return line.text(`${call} timed out`).text(timeoutSuffix(
|
|
8162
|
+
const call = parsed2.subject && parsed2.action ? `${parsed2.subject}.${parsed2.action}` : verb;
|
|
8163
|
+
return line.text(`${call} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8160
8164
|
}
|
|
8161
|
-
function buildAssertion(
|
|
8165
|
+
function buildAssertion(parsed2, opts) {
|
|
8162
8166
|
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(
|
|
8167
|
+
const matcher = parsed2.assertion ?? "expect";
|
|
8168
|
+
const notFound = parsed2.lastState === "not-found" || parsed2.lastState === "resolved-count" && parsed2.resolvedCount === 0 || /element\(s\) not found/.test(parsed2.received ?? "");
|
|
8169
|
+
const expected = parsed2.expected ? truncateValue(parsed2.expected, opts.valueMax) : null;
|
|
8170
|
+
const received = parsed2.received ? truncateValue(receivedDisplay(parsed2), opts.valueMax) : null;
|
|
8171
|
+
if (matcher === "toHaveCount" && parsed2.locator) {
|
|
8172
|
+
const want = Number(parsed2.expected);
|
|
8173
|
+
const got = notFound ? 0 : Number(parsed2.received);
|
|
8170
8174
|
const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
|
|
8171
|
-
const noun2 = countNoun(
|
|
8175
|
+
const noun2 = countNoun(parsed2.locator, Number.isFinite(want) ? want : 2);
|
|
8172
8176
|
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 ??
|
|
8177
|
+
return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed2.locator).text(" toHaveCount");
|
|
8174
8178
|
}
|
|
8175
8179
|
const state = STATE_MATCHERS[matcher];
|
|
8176
8180
|
if (state) {
|
|
8177
|
-
const unmet =
|
|
8178
|
-
if (!
|
|
8181
|
+
const unmet = parsed2.negated ? `stayed ${state.met}` : state.unmet;
|
|
8182
|
+
if (!parsed2.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8179
8183
|
if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
|
|
8180
|
-
return subject(line,
|
|
8184
|
+
return subject(line, parsed2.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8181
8185
|
}
|
|
8182
|
-
return subject(line,
|
|
8186
|
+
return subject(line, parsed2.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8183
8187
|
}
|
|
8184
8188
|
const noun = VALUE_MATCHERS[matcher];
|
|
8185
8189
|
if (noun) {
|
|
8186
|
-
if (
|
|
8187
|
-
return subject(line,
|
|
8190
|
+
if (parsed2.locator && notFound) {
|
|
8191
|
+
return subject(line, parsed2.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed2.timeoutMs));
|
|
8188
8192
|
}
|
|
8189
8193
|
line.text(`Expected ${noun} `);
|
|
8190
8194
|
if (expected) line.value(expected);
|
|
8191
8195
|
else line.text("to match");
|
|
8192
8196
|
if (received) line.text(", got ").value(received);
|
|
8193
|
-
if (
|
|
8194
|
-
line.text(" \u2014 ").locator(opts.locator ??
|
|
8197
|
+
if (parsed2.locator)
|
|
8198
|
+
line.text(" \u2014 ").locator(opts.locator ?? parsed2.locator).text(` ${matcher}`);
|
|
8195
8199
|
else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
|
|
8196
8200
|
return line;
|
|
8197
8201
|
}
|
|
8198
|
-
if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(
|
|
8202
|
+
if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed2.timeoutMs));
|
|
8199
8203
|
if (expected || received) {
|
|
8200
8204
|
line.text("Expected ");
|
|
8201
8205
|
if (expected) line.value(expected);
|
|
8202
8206
|
else line.text("a different value");
|
|
8203
8207
|
if (received) line.text(", got ").value(received);
|
|
8204
8208
|
line.text(` \u2014 ${matcher}`);
|
|
8205
|
-
if (
|
|
8209
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8206
8210
|
return line;
|
|
8207
8211
|
}
|
|
8208
|
-
if (
|
|
8209
|
-
return subject(line,
|
|
8212
|
+
if (parsed2.locator) {
|
|
8213
|
+
return subject(line, parsed2.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8210
8214
|
}
|
|
8211
8215
|
return line.text(`${matcher} assertion failed`);
|
|
8212
8216
|
}
|
|
8213
|
-
function buildStrictMode(
|
|
8217
|
+
function buildStrictMode(parsed2, opts) {
|
|
8214
8218
|
const line = new Line();
|
|
8215
|
-
const count =
|
|
8216
|
-
if (!
|
|
8217
|
-
return line.locator(opts.locator ??
|
|
8219
|
+
const count = parsed2.resolvedCount ?? 0;
|
|
8220
|
+
if (!parsed2.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
|
|
8221
|
+
return line.locator(opts.locator ?? parsed2.locator).text(` matched ${count} ${countNoun(parsed2.locator, count)} \u2014 strict mode`);
|
|
8218
8222
|
}
|
|
8219
|
-
function buildNavigation(
|
|
8223
|
+
function buildNavigation(parsed2, opts) {
|
|
8220
8224
|
const line = new Line();
|
|
8221
|
-
const code =
|
|
8225
|
+
const code = parsed2.networkErrorCode?.replace(/^net::/, "") ?? null;
|
|
8222
8226
|
if (code) {
|
|
8223
8227
|
const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
|
|
8224
8228
|
line.text(phrase);
|
|
8225
|
-
if (
|
|
8229
|
+
if (parsed2.url) line.text(" ").value(truncateValue(parsed2.url, opts.valueMax * 2));
|
|
8226
8230
|
return line;
|
|
8227
8231
|
}
|
|
8228
|
-
const route =
|
|
8229
|
-
const timedOut =
|
|
8230
|
-
if (
|
|
8232
|
+
const route = parsed2.url ? routeOf(parsed2.url) : null;
|
|
8233
|
+
const timedOut = parsed2.timeoutMs !== null || parsed2.errorName === "TimeoutError";
|
|
8234
|
+
if (parsed2.action === "waitForURL" || parsed2.action === "waitForNavigation") {
|
|
8231
8235
|
line.text("Never navigated to ");
|
|
8232
8236
|
if (route) line.value(truncateValue(route, opts.valueMax));
|
|
8233
8237
|
else line.text("the expected URL");
|
|
8234
|
-
return line.text(` \u2014 ${
|
|
8238
|
+
return line.text(` \u2014 ${parsed2.action} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8235
8239
|
}
|
|
8236
8240
|
line.text("Navigation");
|
|
8237
8241
|
if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
|
|
8238
|
-
if (timedOut) return line.text(" timed out").text(timeoutSuffix(
|
|
8242
|
+
if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8239
8243
|
return line.text(" failed");
|
|
8240
8244
|
}
|
|
8241
|
-
function buildTestTimeout(
|
|
8242
|
-
const line = new Line().text("Test timed out").text(timeoutSuffix(
|
|
8243
|
-
if (
|
|
8244
|
-
const phase =
|
|
8245
|
+
function buildTestTimeout(parsed2, opts, ctx) {
|
|
8246
|
+
const line = new Line().text("Test timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8247
|
+
if (parsed2.timeoutPhase) {
|
|
8248
|
+
const phase = parsed2.timeoutPhase;
|
|
8245
8249
|
const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
|
|
8246
8250
|
return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
|
|
8247
8251
|
}
|
|
8248
|
-
if (
|
|
8249
|
-
return line.text(" while navigating to ").value(truncateValue(routeOf(
|
|
8252
|
+
if (parsed2.isNavigationFailure && parsed2.url) {
|
|
8253
|
+
return line.text(" while navigating to ").value(truncateValue(routeOf(parsed2.url), opts.valueMax));
|
|
8250
8254
|
}
|
|
8251
|
-
if (
|
|
8252
|
-
const state = STATE_MATCHERS[
|
|
8255
|
+
if (parsed2.assertion && parsed2.locator) {
|
|
8256
|
+
const state = STATE_MATCHERS[parsed2.assertion];
|
|
8253
8257
|
line.text(state ? ` while waiting for ` : " while expecting ");
|
|
8254
|
-
subject(line,
|
|
8255
|
-
return line.text(state ? ` to be ${state.met}` : ` ${
|
|
8258
|
+
subject(line, parsed2.locator, opts);
|
|
8259
|
+
return line.text(state ? ` to be ${state.met}` : ` ${parsed2.assertion}`);
|
|
8256
8260
|
}
|
|
8257
|
-
if (
|
|
8258
|
-
const gerund = ACTION_GERUNDS[
|
|
8261
|
+
if (parsed2.action && parsed2.locator) {
|
|
8262
|
+
const gerund = ACTION_GERUNDS[parsed2.action] ?? `${ACTION_VERBS[parsed2.action] ?? parsed2.action} on`;
|
|
8259
8263
|
line.text(` while ${gerund} `);
|
|
8260
|
-
return subject(line,
|
|
8264
|
+
return subject(line, parsed2.locator, opts);
|
|
8261
8265
|
}
|
|
8262
|
-
if (
|
|
8266
|
+
if (parsed2.subject && parsed2.action) return line.text(` during ${parsed2.subject}.${parsed2.action}`);
|
|
8263
8267
|
const step = ctx?.lastStepTitle?.trim();
|
|
8264
8268
|
if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
|
|
8265
8269
|
return line;
|
|
8266
8270
|
}
|
|
8267
|
-
function buildCrash(
|
|
8271
|
+
function buildCrash(parsed2, opts) {
|
|
8268
8272
|
const line = new Line();
|
|
8269
|
-
const what = /Page crashed/i.test(
|
|
8270
|
-
if (
|
|
8271
|
-
const verb = ACTION_VERBS[
|
|
8273
|
+
const what = /Page crashed/i.test(parsed2.messageHead) ? "Page crashed" : "Page or browser closed";
|
|
8274
|
+
if (parsed2.action) {
|
|
8275
|
+
const verb = ACTION_VERBS[parsed2.action] ?? parsed2.action;
|
|
8272
8276
|
line.text(`${what} during ${verb}`);
|
|
8273
|
-
if (
|
|
8277
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8274
8278
|
return line;
|
|
8275
8279
|
}
|
|
8276
|
-
if (
|
|
8277
|
-
return line.text(`${what} while expecting `).locator(opts.locator ??
|
|
8280
|
+
if (parsed2.assertion && parsed2.locator) {
|
|
8281
|
+
return line.text(`${what} while expecting `).locator(opts.locator ?? parsed2.locator).text(` ${parsed2.assertion}`);
|
|
8278
8282
|
}
|
|
8279
8283
|
return line.text(what);
|
|
8280
8284
|
}
|
|
8281
8285
|
var CUSTOM_MESSAGE_MAX_CHARS = 60;
|
|
8282
8286
|
var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
|
|
8283
|
-
function withLeads(line,
|
|
8284
|
-
const hook = ctx?.failedIn && !
|
|
8285
|
-
const message =
|
|
8287
|
+
function withLeads(line, parsed2, ctx) {
|
|
8288
|
+
const hook = ctx?.failedIn && !parsed2.timeoutPhase ? ctx.failedIn.hook : null;
|
|
8289
|
+
const message = parsed2.customMessage ? truncateValue(parsed2.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
|
|
8286
8290
|
if (!hook && !message) return line;
|
|
8287
8291
|
const led = new Line();
|
|
8288
8292
|
if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
|
|
@@ -8295,49 +8299,49 @@ function withLeads(line, parsed, ctx) {
|
|
|
8295
8299
|
});
|
|
8296
8300
|
return led;
|
|
8297
8301
|
}
|
|
8298
|
-
function build(
|
|
8299
|
-
return withLeads(buildLine(
|
|
8302
|
+
function build(parsed2, opts, ctx) {
|
|
8303
|
+
return withLeads(buildLine(parsed2, opts, ctx), parsed2, ctx);
|
|
8300
8304
|
}
|
|
8301
|
-
function buildLine(
|
|
8302
|
-
switch (
|
|
8305
|
+
function buildLine(parsed2, opts, ctx) {
|
|
8306
|
+
switch (parsed2.kind) {
|
|
8303
8307
|
case "action-timeout":
|
|
8304
|
-
return buildActionTimeout(
|
|
8308
|
+
return buildActionTimeout(parsed2, opts);
|
|
8305
8309
|
case "assertion":
|
|
8306
8310
|
case "assertion-timeout":
|
|
8307
|
-
return buildAssertion(
|
|
8311
|
+
return buildAssertion(parsed2, opts);
|
|
8308
8312
|
case "strict-mode":
|
|
8309
|
-
return buildStrictMode(
|
|
8313
|
+
return buildStrictMode(parsed2, opts);
|
|
8310
8314
|
case "navigation":
|
|
8311
|
-
return buildNavigation(
|
|
8315
|
+
return buildNavigation(parsed2, opts);
|
|
8312
8316
|
case "test-timeout":
|
|
8313
|
-
return buildTestTimeout(
|
|
8317
|
+
return buildTestTimeout(parsed2, opts, ctx);
|
|
8314
8318
|
case "crash":
|
|
8315
|
-
return buildCrash(
|
|
8319
|
+
return buildCrash(parsed2, opts);
|
|
8316
8320
|
default:
|
|
8317
|
-
return new Line().text(firstLineFallback(
|
|
8321
|
+
return new Line().text(firstLineFallback(parsed2));
|
|
8318
8322
|
}
|
|
8319
8323
|
}
|
|
8320
|
-
function detailOf(
|
|
8321
|
-
switch (
|
|
8324
|
+
function detailOf(parsed2, headline) {
|
|
8325
|
+
switch (parsed2.kind) {
|
|
8322
8326
|
case "test-timeout":
|
|
8323
8327
|
case "action-timeout": {
|
|
8324
|
-
const state =
|
|
8325
|
-
if (!state ||
|
|
8328
|
+
const state = parsed2.lastStateLine;
|
|
8329
|
+
if (!state || parsed2.lastState === "not-found" || headline.includes(state)) return null;
|
|
8326
8330
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8327
8331
|
}
|
|
8328
8332
|
case "assertion":
|
|
8329
8333
|
case "assertion-timeout": {
|
|
8330
|
-
const received =
|
|
8334
|
+
const received = parsed2.received;
|
|
8331
8335
|
if (received && !/element\(s\) not found/.test(received)) {
|
|
8332
|
-
const shown = truncateValue(receivedDisplay(
|
|
8336
|
+
const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
|
|
8333
8337
|
if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
|
|
8334
8338
|
}
|
|
8335
|
-
const state =
|
|
8339
|
+
const state = parsed2.lastStateLine;
|
|
8336
8340
|
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
8337
8341
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8338
8342
|
}
|
|
8339
8343
|
case "navigation":
|
|
8340
|
-
return
|
|
8344
|
+
return parsed2.networkErrorCode ?? parsed2.lastStateLine;
|
|
8341
8345
|
case "strict-mode":
|
|
8342
8346
|
case "crash":
|
|
8343
8347
|
return null;
|
|
@@ -8345,16 +8349,16 @@ function detailOf(parsed, headline) {
|
|
|
8345
8349
|
return null;
|
|
8346
8350
|
}
|
|
8347
8351
|
}
|
|
8348
|
-
function describeFailure(
|
|
8352
|
+
function describeFailure(parsed2, ctx) {
|
|
8349
8353
|
const attempts = [
|
|
8350
|
-
{ locator:
|
|
8351
|
-
{ locator:
|
|
8352
|
-
{ locator:
|
|
8354
|
+
{ locator: parsed2.locator, valueMax: VALUE_MAX_CHARS },
|
|
8355
|
+
{ locator: parsed2.leafLocator ?? parsed2.locator, valueMax: VALUE_MAX_CHARS },
|
|
8356
|
+
{ locator: parsed2.leafLocator ?? parsed2.locator, valueMax: SHORT_VALUE_MAX_CHARS }
|
|
8353
8357
|
];
|
|
8354
|
-
let line = build(
|
|
8358
|
+
let line = build(parsed2, attempts[0], ctx);
|
|
8355
8359
|
for (const opts of attempts.slice(1)) {
|
|
8356
8360
|
if (line.toString().length <= HEADLINE_MAX_CHARS) break;
|
|
8357
|
-
line = build(
|
|
8361
|
+
line = build(parsed2, opts, ctx);
|
|
8358
8362
|
}
|
|
8359
8363
|
let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
|
|
8360
8364
|
let headline = parts.map((p) => p.text).join("");
|
|
@@ -8364,10 +8368,10 @@ function describeFailure(parsed, ctx) {
|
|
|
8364
8368
|
parts.push({ kind: "text", text: "\u2026" });
|
|
8365
8369
|
}
|
|
8366
8370
|
if (!headline.trim()) {
|
|
8367
|
-
headline = firstLineFallback(
|
|
8371
|
+
headline = firstLineFallback(parsed2);
|
|
8368
8372
|
parts = [{ kind: "text", text: headline }];
|
|
8369
8373
|
}
|
|
8370
|
-
return { headline, detail: detailOf(
|
|
8374
|
+
return { headline, detail: detailOf(parsed2, headline), parts };
|
|
8371
8375
|
}
|
|
8372
8376
|
function clipParts(parts, max) {
|
|
8373
8377
|
const out = [];
|
|
@@ -9077,7 +9081,7 @@ async function resolveSelection(options = {}) {
|
|
|
9077
9081
|
}
|
|
9078
9082
|
|
|
9079
9083
|
// src/internal/capture/capture-fixtures.ts
|
|
9080
|
-
var
|
|
9084
|
+
var path28 = __toESM(require("path"));
|
|
9081
9085
|
var import_node_zlib2 = require("zlib");
|
|
9082
9086
|
|
|
9083
9087
|
// ../picker-dom/src/probe.ts
|
|
@@ -10140,6 +10144,83 @@ function showPickerChoices(arg) {
|
|
|
10140
10144
|
doc.body.appendChild(wrap);
|
|
10141
10145
|
}
|
|
10142
10146
|
|
|
10147
|
+
// ../picker-dom/src/overlay-pause.ts
|
|
10148
|
+
function showPauseBar(arg) {
|
|
10149
|
+
const g = globalThis;
|
|
10150
|
+
const doc = g.document;
|
|
10151
|
+
delete g.__piwiPauseState;
|
|
10152
|
+
if (!doc || !doc.body) {
|
|
10153
|
+
g.__piwiPauseState = "resume";
|
|
10154
|
+
return;
|
|
10155
|
+
}
|
|
10156
|
+
if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
|
|
10157
|
+
const Z = 2147483600;
|
|
10158
|
+
const bar = doc.createElement("div");
|
|
10159
|
+
bar.id = "__piwi-pause-bar";
|
|
10160
|
+
bar.setAttribute("role", "toolbar");
|
|
10161
|
+
bar.setAttribute("aria-label", "Piwi: test paused");
|
|
10162
|
+
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;`;
|
|
10163
|
+
const where = doc.createElement("div");
|
|
10164
|
+
where.style.cssText = "font-weight:600;white-space:nowrap;";
|
|
10165
|
+
where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
|
|
10166
|
+
const what = doc.createElement("code");
|
|
10167
|
+
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;";
|
|
10168
|
+
what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
|
|
10169
|
+
what.title = what.textContent;
|
|
10170
|
+
bar.appendChild(where);
|
|
10171
|
+
bar.appendChild(what);
|
|
10172
|
+
const done = (choice) => {
|
|
10173
|
+
cleanup();
|
|
10174
|
+
g.__piwiPauseState = choice;
|
|
10175
|
+
};
|
|
10176
|
+
const onKey = (e) => {
|
|
10177
|
+
if (e.key !== "Escape") return;
|
|
10178
|
+
e.preventDefault();
|
|
10179
|
+
e.stopImmediatePropagation();
|
|
10180
|
+
done("resume");
|
|
10181
|
+
};
|
|
10182
|
+
const cleanup = () => {
|
|
10183
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
10184
|
+
bar.remove();
|
|
10185
|
+
delete g.__piwiPauseCleanup;
|
|
10186
|
+
};
|
|
10187
|
+
const buttons = [
|
|
10188
|
+
["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
10189
|
+
["Step", "step", "Pause again at the next action", false],
|
|
10190
|
+
["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
|
|
10191
|
+
["Finish", "finish", "Run on and pause no more in this test", false]
|
|
10192
|
+
];
|
|
10193
|
+
for (const [label, choice, hint, primary] of buttons) {
|
|
10194
|
+
const btn = doc.createElement("button");
|
|
10195
|
+
btn.type = "button";
|
|
10196
|
+
btn.textContent = label;
|
|
10197
|
+
btn.title = hint;
|
|
10198
|
+
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;");
|
|
10199
|
+
btn.addEventListener("click", (e) => {
|
|
10200
|
+
e.preventDefault();
|
|
10201
|
+
e.stopImmediatePropagation();
|
|
10202
|
+
done(choice);
|
|
10203
|
+
});
|
|
10204
|
+
bar.appendChild(btn);
|
|
10205
|
+
}
|
|
10206
|
+
g.__piwiPauseCleanup = cleanup;
|
|
10207
|
+
doc.addEventListener("keydown", onKey, true);
|
|
10208
|
+
doc.body.appendChild(bar);
|
|
10209
|
+
}
|
|
10210
|
+
function pauseAnswered() {
|
|
10211
|
+
const g = globalThis;
|
|
10212
|
+
if (g.__piwiPauseState !== void 0) return true;
|
|
10213
|
+
return !g.document?.getElementById("__piwi-pause-bar");
|
|
10214
|
+
}
|
|
10215
|
+
function takePauseState() {
|
|
10216
|
+
const g = globalThis;
|
|
10217
|
+
const state = g.__piwiPauseState;
|
|
10218
|
+
if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
|
|
10219
|
+
delete g.__piwiPauseCleanup;
|
|
10220
|
+
delete g.__piwiPauseState;
|
|
10221
|
+
return typeof state === "string" ? state : null;
|
|
10222
|
+
}
|
|
10223
|
+
|
|
10143
10224
|
// ../core/src/locator-generation.ts
|
|
10144
10225
|
var TAG_TO_ROLE = {
|
|
10145
10226
|
a: "link",
|
|
@@ -11176,11 +11257,11 @@ function deriveFailedLocator(testInfo) {
|
|
|
11176
11257
|
${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
11177
11258
|
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
11178
11259
|
if (!line) continue;
|
|
11179
|
-
const
|
|
11180
|
-
if (!
|
|
11260
|
+
const parsed2 = parseLeafLocatorExpression(line[1].trim());
|
|
11261
|
+
if (!parsed2) continue;
|
|
11181
11262
|
const loc = err.location;
|
|
11182
11263
|
const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
11183
|
-
return { method:
|
|
11264
|
+
return { method: parsed2.method, args: parsed2.args, location };
|
|
11184
11265
|
}
|
|
11185
11266
|
return null;
|
|
11186
11267
|
}
|
|
@@ -11202,25 +11283,16 @@ async function cleanupPicker(page) {
|
|
|
11202
11283
|
} catch {
|
|
11203
11284
|
}
|
|
11204
11285
|
}
|
|
11205
|
-
async function
|
|
11286
|
+
async function pickLocator(page, probe, failing) {
|
|
11206
11287
|
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 };
|
|
11288
|
+
const overlayArg = { transport: "global", failing };
|
|
11214
11289
|
await page.evaluate(installPickerOverlay, overlayArg);
|
|
11215
11290
|
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
11216
11291
|
timeout: 0,
|
|
11217
11292
|
polling: 250
|
|
11218
11293
|
});
|
|
11219
11294
|
const state = await page.evaluate(() => globalThis.__piwiPickState);
|
|
11220
|
-
if (state !== "picked")
|
|
11221
|
-
await cleanupPicker(page);
|
|
11222
|
-
return null;
|
|
11223
|
-
}
|
|
11295
|
+
if (state !== "picked") return null;
|
|
11224
11296
|
const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
|
|
11225
11297
|
const attrs = await handle.evaluate(probe.fn, probe.arg);
|
|
11226
11298
|
await handle.dispose();
|
|
@@ -11258,11 +11330,10 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11258
11330
|
);
|
|
11259
11331
|
if (ranked.length === 0) {
|
|
11260
11332
|
console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
|
|
11261
|
-
await cleanupPicker(page);
|
|
11262
11333
|
return null;
|
|
11263
11334
|
}
|
|
11264
11335
|
await page.evaluate(showPickerChoices, {
|
|
11265
|
-
failing
|
|
11336
|
+
failing,
|
|
11266
11337
|
choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
|
|
11267
11338
|
});
|
|
11268
11339
|
await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
|
|
@@ -11270,31 +11341,45 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11270
11341
|
polling: 250
|
|
11271
11342
|
});
|
|
11272
11343
|
const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
|
|
11273
|
-
await cleanupPicker(page);
|
|
11274
11344
|
if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
|
|
11275
11345
|
const picked = { ...ranked[choice], pickedByUser: true };
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11346
|
+
return {
|
|
11347
|
+
picked,
|
|
11348
|
+
alternatives: [picked, ...ranked.filter((_, i) => i !== choice)],
|
|
11349
|
+
element: {
|
|
11350
|
+
tagName: attrs.tagName,
|
|
11351
|
+
attributes: attrs.attributes,
|
|
11352
|
+
textContent: attrs.textContent,
|
|
11353
|
+
accessibleName,
|
|
11354
|
+
center: attrs.center,
|
|
11355
|
+
...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
|
|
11356
|
+
...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
|
|
11357
|
+
},
|
|
11358
|
+
...anchors.length > 0 ? { anchors } : {}
|
|
11285
11359
|
};
|
|
11360
|
+
} finally {
|
|
11361
|
+
await cleanupPicker(page);
|
|
11362
|
+
}
|
|
11363
|
+
}
|
|
11364
|
+
async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
11365
|
+
try {
|
|
11366
|
+
testInfo.setTimeout(0);
|
|
11367
|
+
const rendered = failed ? renderFailing(failed) : null;
|
|
11368
|
+
console.log(
|
|
11369
|
+
`
|
|
11370
|
+
[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).`
|
|
11371
|
+
);
|
|
11372
|
+
const pick = await pickLocator(page, probe, rendered);
|
|
11373
|
+
if (!pick) return null;
|
|
11286
11374
|
const location = failed?.location ?? null;
|
|
11287
11375
|
console.log(
|
|
11288
11376
|
failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
|
|
11289
|
-
[piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11290
|
-
[piwi] ${picked.locator}`
|
|
11377
|
+
[piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11378
|
+
[piwi] ${pick.picked.locator}`
|
|
11291
11379
|
);
|
|
11292
11380
|
return {
|
|
11293
11381
|
failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
|
|
11294
|
-
|
|
11295
|
-
alternatives,
|
|
11296
|
-
element,
|
|
11297
|
-
...anchors.length > 0 ? { anchors } : {}
|
|
11382
|
+
...pick
|
|
11298
11383
|
};
|
|
11299
11384
|
} catch {
|
|
11300
11385
|
return null;
|
|
@@ -11327,10 +11412,207 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
11327
11412
|
return true;
|
|
11328
11413
|
}
|
|
11329
11414
|
|
|
11415
|
+
// src/internal/capture/pause-at.ts
|
|
11416
|
+
var path26 = __toESM(require("path"));
|
|
11417
|
+
|
|
11418
|
+
// ../core/src/editor-send.ts
|
|
11419
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
11420
|
+
function parsePairing(text) {
|
|
11421
|
+
const trimmed = text.trim();
|
|
11422
|
+
const hash = trimmed.indexOf("#");
|
|
11423
|
+
if (hash < 0) return null;
|
|
11424
|
+
const url = trimmed.slice(0, hash);
|
|
11425
|
+
const token = trimmed.slice(hash + 1);
|
|
11426
|
+
if (!/^[A-Za-z0-9_-]{16,}$/.test(token)) return null;
|
|
11427
|
+
let parsed2;
|
|
11428
|
+
try {
|
|
11429
|
+
parsed2 = new URL(url);
|
|
11430
|
+
} catch {
|
|
11431
|
+
return null;
|
|
11432
|
+
}
|
|
11433
|
+
if (parsed2.protocol !== "http:" || !LOOPBACK_HOSTS.has(parsed2.hostname) || !parsed2.port) return null;
|
|
11434
|
+
return { url: `${parsed2.origin}${parsed2.pathname}`, token };
|
|
11435
|
+
}
|
|
11436
|
+
function isInsidePath(file) {
|
|
11437
|
+
if (!file || file.length > 1e3 || file.includes("\0")) return false;
|
|
11438
|
+
if (file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:/.test(file)) return false;
|
|
11439
|
+
return file.split(/[\\/]/).every((part) => part !== "..");
|
|
11440
|
+
}
|
|
11441
|
+
|
|
11442
|
+
// src/internal/capture/pause-at.ts
|
|
11443
|
+
var PauseSet = class {
|
|
11444
|
+
constructor(lines) {
|
|
11445
|
+
this.lines = lines;
|
|
11446
|
+
}
|
|
11447
|
+
get size() {
|
|
11448
|
+
return this.lines.size;
|
|
11449
|
+
}
|
|
11450
|
+
/** Whether a call site (`file:line:col`, as `captureCallerLocation` writes it) is on one of the lines. */
|
|
11451
|
+
matches(callerLocation) {
|
|
11452
|
+
if (!callerLocation) return false;
|
|
11453
|
+
const col = callerLocation.lastIndexOf(":");
|
|
11454
|
+
return col > 0 && this.lines.has(callerLocation.slice(0, col));
|
|
11455
|
+
}
|
|
11456
|
+
};
|
|
11457
|
+
var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
|
|
11458
|
+
function callSiteFile(file, cwd) {
|
|
11459
|
+
const slashed = file.replace(/\\/g, "/");
|
|
11460
|
+
const absolute = path26.isAbsolute(file) || path26.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
|
|
11461
|
+
let rel = absolute ? path26.relative(cwd, file) : path26.normalize(slashed);
|
|
11462
|
+
rel = rel.split(path26.sep).join("/").replace(/\\/g, "/");
|
|
11463
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
11464
|
+
return rel;
|
|
11465
|
+
}
|
|
11466
|
+
var ignoredLogged = /* @__PURE__ */ new Set();
|
|
11467
|
+
function parsePauseAt(value, cwd = process.cwd()) {
|
|
11468
|
+
if (!value?.trim()) return EMPTY;
|
|
11469
|
+
const lines = /* @__PURE__ */ new Set();
|
|
11470
|
+
for (const raw of value.split(";")) {
|
|
11471
|
+
const entry = raw.trim();
|
|
11472
|
+
if (!entry) continue;
|
|
11473
|
+
const m = /^(.+):(\d+)$/.exec(entry);
|
|
11474
|
+
const line = m ? Number(m[2]) : 0;
|
|
11475
|
+
if (!m?.[1] || line < 1) {
|
|
11476
|
+
if (!ignoredLogged.has(entry)) {
|
|
11477
|
+
ignoredLogged.add(entry);
|
|
11478
|
+
console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt}: ignoring "${entry}": expected file:line`);
|
|
11479
|
+
}
|
|
11480
|
+
continue;
|
|
11481
|
+
}
|
|
11482
|
+
lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
|
|
11483
|
+
}
|
|
11484
|
+
return lines.size ? new PauseSet(lines) : EMPTY;
|
|
11485
|
+
}
|
|
11486
|
+
var parsed = null;
|
|
11487
|
+
function currentPauseSet() {
|
|
11488
|
+
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
11489
|
+
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
11490
|
+
return parsed.set;
|
|
11491
|
+
}
|
|
11492
|
+
function pauseGateFromTestInfo(testInfo) {
|
|
11493
|
+
const use = testInfo.project?.use ?? {};
|
|
11494
|
+
return { ci: process.env.CI, headless: use.headless };
|
|
11495
|
+
}
|
|
11496
|
+
function pauseSkipReason(gate) {
|
|
11497
|
+
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
11498
|
+
if (gate.headless !== false)
|
|
11499
|
+
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
11500
|
+
return null;
|
|
11501
|
+
}
|
|
11502
|
+
function rearmedTimeout(before, pausedMs) {
|
|
11503
|
+
return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
|
|
11504
|
+
}
|
|
11505
|
+
function pausePlace(callerLocation) {
|
|
11506
|
+
const col = callerLocation.lastIndexOf(":");
|
|
11507
|
+
const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
|
|
11508
|
+
return fileLine.slice(fileLine.lastIndexOf("/") + 1);
|
|
11509
|
+
}
|
|
11510
|
+
function assertionLabel(expression, isNot) {
|
|
11511
|
+
const [first = "", ...rest] = expression.split(".").filter(Boolean);
|
|
11512
|
+
const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
11513
|
+
return `${isNot ? "not." : ""}${name || "expect"}`;
|
|
11514
|
+
}
|
|
11515
|
+
function pickPlace(callerLocation) {
|
|
11516
|
+
const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
|
|
11517
|
+
if (!m) return null;
|
|
11518
|
+
const file = m[1];
|
|
11519
|
+
if (!isInsidePath(file)) return null;
|
|
11520
|
+
return { file, line: Number(m[2]) };
|
|
11521
|
+
}
|
|
11522
|
+
function pickPayload(locator, callerLocation) {
|
|
11523
|
+
const at = pickPlace(callerLocation);
|
|
11524
|
+
return at ? { kind: "locator", text: locator, at } : { kind: "locator", text: locator };
|
|
11525
|
+
}
|
|
11526
|
+
var postFailureLogged = false;
|
|
11527
|
+
async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
|
|
11528
|
+
if (!pairingAddress) return false;
|
|
11529
|
+
const logOnce = (why) => {
|
|
11530
|
+
if (postFailureLogged) return;
|
|
11531
|
+
postFailureLogged = true;
|
|
11532
|
+
console.log(`[piwi] Could not send the picked locator to the editor: ${why}.`);
|
|
11533
|
+
};
|
|
11534
|
+
const pairing = parsePairing(pairingAddress);
|
|
11535
|
+
if (!pairing) {
|
|
11536
|
+
logOnce(`${PIWI_LOCAL_DEBUG_ENV.editorSend} is not a pairing address`);
|
|
11537
|
+
return false;
|
|
11538
|
+
}
|
|
11539
|
+
try {
|
|
11540
|
+
const response = await fetchImpl(pairing.url, {
|
|
11541
|
+
method: "POST",
|
|
11542
|
+
headers: { Authorization: `Bearer ${pairing.token}`, "Content-Type": "application/json" },
|
|
11543
|
+
body: JSON.stringify(payload),
|
|
11544
|
+
signal: AbortSignal.timeout(5e3)
|
|
11545
|
+
});
|
|
11546
|
+
if (response.ok) return true;
|
|
11547
|
+
logOnce(`the editor answered ${response.status}`);
|
|
11548
|
+
} catch (error) {
|
|
11549
|
+
logOnce(error instanceof Error ? error.message : String(error));
|
|
11550
|
+
}
|
|
11551
|
+
return false;
|
|
11552
|
+
}
|
|
11553
|
+
var skipLogged = false;
|
|
11554
|
+
function logPauseSkipOnce(reason) {
|
|
11555
|
+
if (skipLogged) return;
|
|
11556
|
+
skipLogged = true;
|
|
11557
|
+
console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
|
|
11558
|
+
}
|
|
11559
|
+
var HIGHLIGHT_MS = 2e3;
|
|
11560
|
+
async function pauseHere(page, testInfo, point, probe, onPick) {
|
|
11561
|
+
const before = testInfo.timeout;
|
|
11562
|
+
const started = Date.now();
|
|
11563
|
+
let shown = null;
|
|
11564
|
+
try {
|
|
11565
|
+
testInfo.setTimeout(0);
|
|
11566
|
+
const highlight = point.target?.highlight;
|
|
11567
|
+
if (typeof highlight === "function") {
|
|
11568
|
+
let timer;
|
|
11569
|
+
shown = await Promise.race([
|
|
11570
|
+
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
11571
|
+
new Promise((resolve7) => {
|
|
11572
|
+
timer = setTimeout(() => resolve7(null), HIGHLIGHT_MS);
|
|
11573
|
+
})
|
|
11574
|
+
]);
|
|
11575
|
+
clearTimeout(timer);
|
|
11576
|
+
}
|
|
11577
|
+
const arg = {
|
|
11578
|
+
place: pausePlace(point.location),
|
|
11579
|
+
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
11580
|
+
action: point.action,
|
|
11581
|
+
locator: point.locator
|
|
11582
|
+
};
|
|
11583
|
+
console.log(
|
|
11584
|
+
`[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).`
|
|
11585
|
+
);
|
|
11586
|
+
for (; ; ) {
|
|
11587
|
+
await page.evaluate(showPauseBar, arg);
|
|
11588
|
+
await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
|
|
11589
|
+
const choice = await page.evaluate(takePauseState);
|
|
11590
|
+
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
11591
|
+
if (choice !== "pick") continue;
|
|
11592
|
+
const picked = await pickLocator(page, probe, null).catch(() => null);
|
|
11593
|
+
if (picked) await onPick(picked);
|
|
11594
|
+
}
|
|
11595
|
+
} catch {
|
|
11596
|
+
return "resume";
|
|
11597
|
+
} finally {
|
|
11598
|
+
try {
|
|
11599
|
+
const dispose = shown?.dispose;
|
|
11600
|
+
const hide = page.hideHighlight;
|
|
11601
|
+
if (typeof dispose === "function") await dispose.call(shown);
|
|
11602
|
+
else if (typeof hide === "function") await hide.call(page);
|
|
11603
|
+
} catch {
|
|
11604
|
+
}
|
|
11605
|
+
try {
|
|
11606
|
+
testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
|
|
11607
|
+
} catch {
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
|
|
11330
11612
|
// src/internal/capture/code-reach.ts
|
|
11331
11613
|
var crypto3 = __toESM(require("crypto"));
|
|
11332
11614
|
var fs22 = __toESM(require("fs"));
|
|
11333
|
-
var
|
|
11615
|
+
var path27 = __toESM(require("path"));
|
|
11334
11616
|
var import_node_child_process3 = require("child_process");
|
|
11335
11617
|
|
|
11336
11618
|
// ../core/src/source-map.ts
|
|
@@ -11593,12 +11875,12 @@ function repositoryRoot(from) {
|
|
|
11593
11875
|
}).trim();
|
|
11594
11876
|
} catch {
|
|
11595
11877
|
}
|
|
11596
|
-
cachedRepoRoot = { from, root:
|
|
11878
|
+
cachedRepoRoot = { from, root: path27.resolve(root) };
|
|
11597
11879
|
return cachedRepoRoot.root;
|
|
11598
11880
|
}
|
|
11599
11881
|
function codeReachRoots(configDir, configured) {
|
|
11600
11882
|
const repoRoot = repositoryRoot(configDir);
|
|
11601
|
-
const roots = configured?.length ? configured.map((r) =>
|
|
11883
|
+
const roots = configured?.length ? configured.map((r) => path27.resolve(configDir, r)) : [configDir];
|
|
11602
11884
|
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
11603
11885
|
}
|
|
11604
11886
|
function isFile(file) {
|
|
@@ -11611,10 +11893,10 @@ function isFile(file) {
|
|
|
11611
11893
|
function resolveSourcePath(source, roots) {
|
|
11612
11894
|
const normalized = normalizeSourcePath(source);
|
|
11613
11895
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
11614
|
-
if (
|
|
11615
|
-
const
|
|
11896
|
+
if (path27.isAbsolute(normalized) && isFile(normalized)) return path27.resolve(normalized);
|
|
11897
|
+
const relative14 = normalized.replace(/^\/+/, "");
|
|
11616
11898
|
for (const root of roots) {
|
|
11617
|
-
const candidate =
|
|
11899
|
+
const candidate = path27.resolve(root, relative14);
|
|
11618
11900
|
if (isFile(candidate)) return candidate;
|
|
11619
11901
|
}
|
|
11620
11902
|
return null;
|
|
@@ -11630,10 +11912,10 @@ function viteBases(entries) {
|
|
|
11630
11912
|
function fileForUrl(url, roots, bases) {
|
|
11631
11913
|
let pathname;
|
|
11632
11914
|
try {
|
|
11633
|
-
const
|
|
11634
|
-
if (
|
|
11635
|
-
pathname =
|
|
11636
|
-
const base = bases.get(
|
|
11915
|
+
const parsed2 = new URL(url);
|
|
11916
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
|
|
11917
|
+
pathname = parsed2.pathname;
|
|
11918
|
+
const base = bases.get(parsed2.origin);
|
|
11637
11919
|
if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
|
|
11638
11920
|
} catch {
|
|
11639
11921
|
return null;
|
|
@@ -11646,7 +11928,7 @@ function fileForUrl(url, roots, bases) {
|
|
|
11646
11928
|
return file;
|
|
11647
11929
|
}
|
|
11648
11930
|
function relativeToRepo(file, repoRoot) {
|
|
11649
|
-
return
|
|
11931
|
+
return path27.relative(repoRoot, file).split(path27.sep).join("/");
|
|
11650
11932
|
}
|
|
11651
11933
|
function scriptHash(entry) {
|
|
11652
11934
|
return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
@@ -11816,6 +12098,7 @@ function createSink() {
|
|
|
11816
12098
|
flake: null,
|
|
11817
12099
|
pickOffered: false,
|
|
11818
12100
|
userPick: null,
|
|
12101
|
+
pause: null,
|
|
11819
12102
|
locatorPages: new LocatorPageLog(),
|
|
11820
12103
|
codeReach: null
|
|
11821
12104
|
};
|
|
@@ -11832,11 +12115,11 @@ function configuredCodeReachRoots() {
|
|
|
11832
12115
|
const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
|
|
11833
12116
|
if (!raw) return null;
|
|
11834
12117
|
try {
|
|
11835
|
-
const
|
|
11836
|
-
if (Array.isArray(
|
|
12118
|
+
const parsed2 = JSON.parse(raw);
|
|
12119
|
+
if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
|
|
11837
12120
|
} catch {
|
|
11838
12121
|
}
|
|
11839
|
-
return raw.split(
|
|
12122
|
+
return raw.split(path28.delimiter).filter(Boolean);
|
|
11840
12123
|
}
|
|
11841
12124
|
async function stopSinkCodeReach(sink, closing) {
|
|
11842
12125
|
const session = sink.codeReach;
|
|
@@ -11851,7 +12134,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
11851
12134
|
if (!entries) return;
|
|
11852
12135
|
const info = sink.testInfo;
|
|
11853
12136
|
const configFile = info?.config.configFile;
|
|
11854
|
-
const configDir = configFile ?
|
|
12137
|
+
const configDir = configFile ? path28.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
11855
12138
|
session.files = await resolveCodeReach(
|
|
11856
12139
|
entries,
|
|
11857
12140
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -12270,6 +12553,53 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
|
12270
12553
|
} catch {
|
|
12271
12554
|
}
|
|
12272
12555
|
}
|
|
12556
|
+
function pausesAt(pause, callerLocation) {
|
|
12557
|
+
return !pause.finished && (pause.next || pause.set.matches(callerLocation));
|
|
12558
|
+
}
|
|
12559
|
+
function locatorSource(target, method, args) {
|
|
12560
|
+
try {
|
|
12561
|
+
const text = String(target);
|
|
12562
|
+
if (text && !text.startsWith("[object")) return text;
|
|
12563
|
+
} catch {
|
|
12564
|
+
}
|
|
12565
|
+
return renderFailing({ method, args });
|
|
12566
|
+
}
|
|
12567
|
+
async function pauseBefore(sink, page, target, callerLocation, action, origin) {
|
|
12568
|
+
const pause = sink.pause;
|
|
12569
|
+
const testInfo = sink.testInfo;
|
|
12570
|
+
if (!pause || !testInfo || !callerLocation) return;
|
|
12571
|
+
pause.next = false;
|
|
12572
|
+
try {
|
|
12573
|
+
const locator = locatorSource(target, origin.method, origin.args);
|
|
12574
|
+
const onPick = async (picked) => {
|
|
12575
|
+
const pick = {
|
|
12576
|
+
failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
|
|
12577
|
+
...picked
|
|
12578
|
+
};
|
|
12579
|
+
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
12580
|
+
sink.probedLocations.add(callerLocation);
|
|
12581
|
+
const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
|
|
12582
|
+
console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
|
|
12583
|
+
await postPickToEditor(
|
|
12584
|
+
process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
|
|
12585
|
+
pickPayload(picked.picked.locator, callerLocation)
|
|
12586
|
+
);
|
|
12587
|
+
};
|
|
12588
|
+
const choice = await internalCall(
|
|
12589
|
+
page,
|
|
12590
|
+
() => pauseHere(
|
|
12591
|
+
page,
|
|
12592
|
+
testInfo,
|
|
12593
|
+
{ location: callerLocation, action, locator, target },
|
|
12594
|
+
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
12595
|
+
onPick
|
|
12596
|
+
)
|
|
12597
|
+
);
|
|
12598
|
+
if (choice === "step") pause.next = true;
|
|
12599
|
+
else if (choice === "finish") pause.finished = true;
|
|
12600
|
+
} catch {
|
|
12601
|
+
}
|
|
12602
|
+
}
|
|
12273
12603
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
12274
12604
|
return new Proxy(locator, {
|
|
12275
12605
|
get(target, prop) {
|
|
@@ -12291,6 +12621,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12291
12621
|
const isNot = Boolean(callArgs[1]?.isNot);
|
|
12292
12622
|
const callerLocation = sink ? captureCallerLocation() : null;
|
|
12293
12623
|
if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
|
|
12624
|
+
if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
|
|
12625
|
+
await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
|
|
12626
|
+
method: originMethod,
|
|
12627
|
+
args: originArgs
|
|
12628
|
+
});
|
|
12629
|
+
}
|
|
12294
12630
|
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
12295
12631
|
return callMethod(target, prop, callArgs);
|
|
12296
12632
|
}
|
|
@@ -12329,6 +12665,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12329
12665
|
args: originArgs,
|
|
12330
12666
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
12331
12667
|
};
|
|
12668
|
+
if (sink.pause && pausesAt(sink.pause, callerLocation)) {
|
|
12669
|
+
await pauseBefore(sink, page, target, callerLocation, String(prop), used);
|
|
12670
|
+
}
|
|
12332
12671
|
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
12333
12672
|
let seq = -1;
|
|
12334
12673
|
if (!alreadyProbed) {
|
|
@@ -12789,6 +13128,12 @@ var piwiFixtures = {
|
|
|
12789
13128
|
if (conflict) throw new Error(conflict);
|
|
12790
13129
|
const sink = createSink();
|
|
12791
13130
|
sink.testInfo = testInfo;
|
|
13131
|
+
const pauseSet = currentPauseSet();
|
|
13132
|
+
if (pauseSet.size) {
|
|
13133
|
+
const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
|
|
13134
|
+
if (reason) logPauseSkipOnce(reason);
|
|
13135
|
+
else sink.pause = { set: pauseSet, next: false, finished: false };
|
|
13136
|
+
}
|
|
12792
13137
|
if (isFlakeMode()) {
|
|
12793
13138
|
const plan = loadFlakePlan();
|
|
12794
13139
|
const role = flakeRoleForTest(plan, {
|
|
@@ -12868,12 +13213,12 @@ function extendPiwiFixtures(test) {
|
|
|
12868
13213
|
|
|
12869
13214
|
// src/internal/ai/ai-fixtures.ts
|
|
12870
13215
|
var fs24 = __toESM(require("fs"));
|
|
12871
|
-
var
|
|
13216
|
+
var path31 = __toESM(require("path"));
|
|
12872
13217
|
var import_test = require("@playwright/test");
|
|
12873
13218
|
|
|
12874
13219
|
// src/internal/ai/artifact.ts
|
|
12875
13220
|
var fs23 = __toESM(require("fs"));
|
|
12876
|
-
var
|
|
13221
|
+
var path29 = __toESM(require("path"));
|
|
12877
13222
|
var ARTIFACT_VERSION = 1;
|
|
12878
13223
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
12879
13224
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -12977,7 +13322,7 @@ function writeEntry(file, entry) {
|
|
|
12977
13322
|
const canonical = serializeEntry(entry);
|
|
12978
13323
|
return withEntryLock(file, () => {
|
|
12979
13324
|
if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
12980
|
-
fs23.mkdirSync(
|
|
13325
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12981
13326
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
12982
13327
|
fs23.writeFileSync(tmp, canonical);
|
|
12983
13328
|
fs23.renameSync(tmp, file);
|
|
@@ -12986,7 +13331,7 @@ function writeEntry(file, entry) {
|
|
|
12986
13331
|
}
|
|
12987
13332
|
function withEntryLock(file, fn) {
|
|
12988
13333
|
const lock = `${file}.lock`;
|
|
12989
|
-
fs23.mkdirSync(
|
|
13334
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12990
13335
|
const deadline = Date.now() + 5e3;
|
|
12991
13336
|
for (; ; ) {
|
|
12992
13337
|
try {
|
|
@@ -13015,7 +13360,7 @@ function withEntryLock(file, fn) {
|
|
|
13015
13360
|
|
|
13016
13361
|
// src/internal/ai/keys.ts
|
|
13017
13362
|
var crypto4 = __toESM(require("crypto"));
|
|
13018
|
-
var
|
|
13363
|
+
var path30 = __toESM(require("path"));
|
|
13019
13364
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
13020
13365
|
function normalizeTemplate(template) {
|
|
13021
13366
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -13029,13 +13374,13 @@ function slug(text, maxLength = 40) {
|
|
|
13029
13374
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
13030
13375
|
}
|
|
13031
13376
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
13032
|
-
return
|
|
13377
|
+
return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
|
|
13033
13378
|
}
|
|
13034
13379
|
function entryPath(params) {
|
|
13035
13380
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
13036
13381
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
13037
13382
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
13038
|
-
return
|
|
13383
|
+
return path30.join(entryDir(params.specFile, params.dir), name);
|
|
13039
13384
|
}
|
|
13040
13385
|
function parseLocation2(location) {
|
|
13041
13386
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -13215,9 +13560,9 @@ function renderSourceArg(arg) {
|
|
|
13215
13560
|
if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
|
|
13216
13561
|
return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
|
|
13217
13562
|
}
|
|
13218
|
-
function
|
|
13563
|
+
function locatorSource2(loc) {
|
|
13219
13564
|
const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
|
|
13220
|
-
const tail = (loc.chain ?? []).map(
|
|
13565
|
+
const tail = (loc.chain ?? []).map(locatorSource2).join(".");
|
|
13221
13566
|
return tail ? `${head}.${tail}` : head;
|
|
13222
13567
|
}
|
|
13223
13568
|
function describePostcondition(post) {
|
|
@@ -13549,18 +13894,18 @@ function testIdentity(testInfo) {
|
|
|
13549
13894
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
13550
13895
|
}
|
|
13551
13896
|
function relativeToCwd(file) {
|
|
13552
|
-
return
|
|
13897
|
+
return path31.relative(process.cwd(), file).split(path31.sep).join("/");
|
|
13553
13898
|
}
|
|
13554
13899
|
function recordIntents(intents, entry) {
|
|
13555
13900
|
const add = (locator, kind) => {
|
|
13556
13901
|
intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
|
|
13557
13902
|
};
|
|
13558
13903
|
if (entry.kind === "locator") {
|
|
13559
|
-
add(
|
|
13904
|
+
add(locatorSource2(entry.locator), "locator");
|
|
13560
13905
|
return;
|
|
13561
13906
|
}
|
|
13562
|
-
for (const step of entry.steps) add(
|
|
13563
|
-
if (entry.postcondition.locator) add(
|
|
13907
|
+
for (const step of entry.steps) add(locatorSource2(step.locator), "run");
|
|
13908
|
+
if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
|
|
13564
13909
|
}
|
|
13565
13910
|
function missMessage(template, testTitle, relPath) {
|
|
13566
13911
|
return [
|