@piwitests/reporter 0.47.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/cli/index.js +1 -1
- package/dist/global-setup-module.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +597 -215
- package/dist/internal/capture/capture-fixtures.js +446 -101
- 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 };
|
|
@@ -91,7 +91,7 @@ var DEFAULTS = {
|
|
|
91
91
|
defaultCapture: true,
|
|
92
92
|
checkBaseUrl: false,
|
|
93
93
|
streaming: true,
|
|
94
|
-
streamingBatchSize:
|
|
94
|
+
streamingBatchSize: 20,
|
|
95
95
|
streamingBatchDelay: 2e3,
|
|
96
96
|
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
97
97
|
submitTimeout: 15 * 60 * 1e3,
|
|
@@ -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;
|
|
@@ -1865,6 +1869,20 @@ var StreamManager = class {
|
|
|
1865
1869
|
this.heartbeatStopped = false;
|
|
1866
1870
|
this.lastActivityAt = 0;
|
|
1867
1871
|
this.heartbeatInterval = 15e3;
|
|
1872
|
+
/**
|
|
1873
|
+
* The heartbeat interval while nobody watches the run: during a long test no
|
|
1874
|
+
* other request goes out, so the heartbeat's answer is how the reporter learns
|
|
1875
|
+
* that someone opened the run.
|
|
1876
|
+
*/
|
|
1877
|
+
this.unwatchedHeartbeatInterval = 5e3;
|
|
1878
|
+
/**
|
|
1879
|
+
* Whether anyone has the run open live on the dashboard, from the `watched`
|
|
1880
|
+
* flag of the last answer to an events or heartbeat request; `null` until the
|
|
1881
|
+
* dashboard says, and for a dashboard that does not. Step events are live
|
|
1882
|
+
* progress only (each `complete` event carries its test's steps), so none are
|
|
1883
|
+
* queued while it is `false`.
|
|
1884
|
+
*/
|
|
1885
|
+
this.watched = null;
|
|
1868
1886
|
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
1869
1887
|
this.uploadedCaseFiles = /* @__PURE__ */ new WeakSet();
|
|
1870
1888
|
/** Set when a test-result event left the live stream undelivered: too large to send, or still queued when the drain gave up. */
|
|
@@ -2035,8 +2053,13 @@ var StreamManager = class {
|
|
|
2035
2053
|
this.pendingBeginEvents.enqueue(event);
|
|
2036
2054
|
}
|
|
2037
2055
|
}
|
|
2038
|
-
/**
|
|
2056
|
+
/**
|
|
2057
|
+
* Queue a test-case event. Triggers an immediate flush when the batch size is
|
|
2058
|
+
* reached, otherwise schedules a timer-based flush. A step event is dropped
|
|
2059
|
+
* while nobody watches the run.
|
|
2060
|
+
*/
|
|
2039
2061
|
queueEvent(event) {
|
|
2062
|
+
if (this.watched === false && (event.type === "step-begin" || event.type === "step-end")) return;
|
|
2040
2063
|
this.pendingEvents.enqueue(event);
|
|
2041
2064
|
if (this.pendingEvents.length >= this.options.streamingBatchSize) {
|
|
2042
2065
|
this.flush();
|
|
@@ -2064,11 +2087,12 @@ var StreamManager = class {
|
|
|
2064
2087
|
while (chunks.length > 0) {
|
|
2065
2088
|
const chunk = chunks.shift();
|
|
2066
2089
|
try {
|
|
2067
|
-
await this.httpClient.postJSON(
|
|
2090
|
+
const response = await this.httpClient.postJSON(
|
|
2068
2091
|
`/api/test-runs/${this._runId}/events`,
|
|
2069
2092
|
{ streamToken: this._token, testCases: chunk },
|
|
2070
2093
|
this._auth
|
|
2071
2094
|
);
|
|
2095
|
+
this.noteWatched(response);
|
|
2072
2096
|
this.retryCount = 0;
|
|
2073
2097
|
this.lastActivityAt = Date.now();
|
|
2074
2098
|
} catch (error) {
|
|
@@ -2142,13 +2166,29 @@ var StreamManager = class {
|
|
|
2142
2166
|
if (this.pendingEvents.length > 0) this.flush();
|
|
2143
2167
|
}, delay);
|
|
2144
2168
|
}
|
|
2169
|
+
// Take the dashboard's answer on whether anyone watches the run. An answer
|
|
2170
|
+
// without the flag (an older dashboard) keeps step events flowing.
|
|
2171
|
+
noteWatched(response) {
|
|
2172
|
+
const watched = response?.watched;
|
|
2173
|
+
const next = typeof watched === "boolean" ? watched : null;
|
|
2174
|
+
if (next !== null && next !== this.watched) {
|
|
2175
|
+
this.logger.debug(
|
|
2176
|
+
next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
this.watched = next;
|
|
2180
|
+
}
|
|
2181
|
+
// Idle time after which a heartbeat goes out.
|
|
2182
|
+
heartbeatDelay() {
|
|
2183
|
+
return this.watched === false ? this.unwatchedHeartbeatInterval : this.heartbeatInterval;
|
|
2184
|
+
}
|
|
2145
2185
|
// Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
|
|
2146
2186
|
scheduleHeartbeat() {
|
|
2147
2187
|
if (this.heartbeatStopped || this.heartbeatTimer) return;
|
|
2148
2188
|
this.heartbeatTimer = setTimeout(() => {
|
|
2149
2189
|
this.heartbeatTimer = null;
|
|
2150
2190
|
void this.sendHeartbeat();
|
|
2151
|
-
}, this.
|
|
2191
|
+
}, this.heartbeatDelay());
|
|
2152
2192
|
}
|
|
2153
2193
|
// Ping the server only when the run has actually been idle. Real event traffic
|
|
2154
2194
|
// already bumps the server's activity timestamp, so a recent flush (or pending
|
|
@@ -2156,16 +2196,17 @@ var StreamManager = class {
|
|
|
2156
2196
|
async sendHeartbeat() {
|
|
2157
2197
|
if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token) return;
|
|
2158
2198
|
const idleFor = Date.now() - this.lastActivityAt;
|
|
2159
|
-
if (idleFor < this.
|
|
2199
|
+
if (idleFor < this.heartbeatDelay() || this.pendingEvents.length > 0) {
|
|
2160
2200
|
this.scheduleHeartbeat();
|
|
2161
2201
|
return;
|
|
2162
2202
|
}
|
|
2163
2203
|
try {
|
|
2164
|
-
await this.httpClient.postJSON(
|
|
2204
|
+
const response = await this.httpClient.postJSON(
|
|
2165
2205
|
`/api/test-runs/${this._runId}/heartbeat`,
|
|
2166
2206
|
{ streamToken: this._token },
|
|
2167
2207
|
this._auth
|
|
2168
2208
|
);
|
|
2209
|
+
this.noteWatched(response);
|
|
2169
2210
|
this.lastActivityAt = Date.now();
|
|
2170
2211
|
} catch (error) {
|
|
2171
2212
|
this.logger.debug(`Heartbeat failed: ${errorMessage(error)}`);
|
|
@@ -3743,9 +3784,9 @@ function loadAriaSampleSet(projectName) {
|
|
|
3743
3784
|
let set = null;
|
|
3744
3785
|
try {
|
|
3745
3786
|
const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
3746
|
-
const
|
|
3747
|
-
if (
|
|
3748
|
-
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"));
|
|
3749
3790
|
}
|
|
3750
3791
|
} catch {
|
|
3751
3792
|
set = null;
|
|
@@ -3896,14 +3937,14 @@ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
|
|
|
3896
3937
|
var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
|
|
3897
3938
|
var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
|
|
3898
3939
|
function parseManifestJson(text) {
|
|
3899
|
-
let
|
|
3940
|
+
let parsed2;
|
|
3900
3941
|
try {
|
|
3901
|
-
|
|
3942
|
+
parsed2 = JSON.parse(text);
|
|
3902
3943
|
} catch {
|
|
3903
3944
|
return null;
|
|
3904
3945
|
}
|
|
3905
|
-
if (!
|
|
3906
|
-
const obj =
|
|
3946
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
3947
|
+
const obj = parsed2;
|
|
3907
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) => ({
|
|
3908
3949
|
method: r.method,
|
|
3909
3950
|
pattern: r.pattern,
|
|
@@ -4107,8 +4148,8 @@ function parseRouteKey(routeKey2) {
|
|
|
4107
4148
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
4108
4149
|
}
|
|
4109
4150
|
function patternToRegex(pattern) {
|
|
4110
|
-
const
|
|
4111
|
-
const escaped =
|
|
4151
|
+
const path32 = pattern.split("?")[0] ?? pattern;
|
|
4152
|
+
const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
4112
4153
|
return new RegExp(`^${escaped}$`);
|
|
4113
4154
|
}
|
|
4114
4155
|
function suitePathEqual(a, b) {
|
|
@@ -4131,13 +4172,13 @@ function matchProbeItem(plan, test) {
|
|
|
4131
4172
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
4132
4173
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
4133
4174
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
4134
|
-
let
|
|
4175
|
+
let path32;
|
|
4135
4176
|
try {
|
|
4136
|
-
|
|
4177
|
+
path32 = new URL(url).pathname;
|
|
4137
4178
|
} catch {
|
|
4138
|
-
|
|
4179
|
+
path32 = url.split("?")[0] ?? url;
|
|
4139
4180
|
}
|
|
4140
|
-
return patternToRegex(pattern).test(
|
|
4181
|
+
return patternToRegex(pattern).test(path32);
|
|
4141
4182
|
}
|
|
4142
4183
|
function createProbeState() {
|
|
4143
4184
|
return { navigated: false, matches: 0 };
|
|
@@ -4159,10 +4200,10 @@ function isProbeMode() {
|
|
|
4159
4200
|
function loadPlan() {
|
|
4160
4201
|
if (planCache !== void 0) return planCache;
|
|
4161
4202
|
planCache = null;
|
|
4162
|
-
const
|
|
4163
|
-
if (
|
|
4203
|
+
const path32 = process.env[PIWI_PROBE_ENV.plan];
|
|
4204
|
+
if (path32) {
|
|
4164
4205
|
try {
|
|
4165
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
4206
|
+
planCache = JSON.parse(fs14.readFileSync(path32, "utf8"));
|
|
4166
4207
|
} catch {
|
|
4167
4208
|
planCache = null;
|
|
4168
4209
|
}
|
|
@@ -4184,10 +4225,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
4184
4225
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
4185
4226
|
}
|
|
4186
4227
|
function recordProbeOutcome(line) {
|
|
4187
|
-
const
|
|
4188
|
-
if (!
|
|
4228
|
+
const path32 = process.env[PIWI_PROBE_ENV.results];
|
|
4229
|
+
if (!path32) return;
|
|
4189
4230
|
try {
|
|
4190
|
-
fs14.appendFileSync(
|
|
4231
|
+
fs14.appendFileSync(path32, `${JSON.stringify(line)}
|
|
4191
4232
|
`);
|
|
4192
4233
|
} catch {
|
|
4193
4234
|
}
|
|
@@ -4410,8 +4451,8 @@ var FLAKE_LAB_RUN_METADATA_KEY = "piwiFlakeLab";
|
|
|
4410
4451
|
// ../core/src/page-key.ts
|
|
4411
4452
|
function normalizeRoute(url) {
|
|
4412
4453
|
try {
|
|
4413
|
-
const
|
|
4414
|
-
let pathname =
|
|
4454
|
+
const parsed2 = new URL(url);
|
|
4455
|
+
let pathname = parsed2.pathname;
|
|
4415
4456
|
pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
|
|
4416
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");
|
|
4417
4458
|
pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
|
|
@@ -4419,8 +4460,8 @@ function normalizeRoute(url) {
|
|
|
4419
4460
|
pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
|
|
4420
4461
|
pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
4421
4462
|
let queryString = "";
|
|
4422
|
-
if (
|
|
4423
|
-
const params = new URLSearchParams(
|
|
4463
|
+
if (parsed2.search) {
|
|
4464
|
+
const params = new URLSearchParams(parsed2.search);
|
|
4424
4465
|
const redacted = new URLSearchParams();
|
|
4425
4466
|
for (const [key] of params) {
|
|
4426
4467
|
redacted.set(key, "<redacted>");
|
|
@@ -4472,24 +4513,24 @@ function faultRouteAction(fault) {
|
|
|
4472
4513
|
return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
|
|
4473
4514
|
}
|
|
4474
4515
|
function dropOneField(body) {
|
|
4475
|
-
let
|
|
4516
|
+
let parsed2;
|
|
4476
4517
|
try {
|
|
4477
|
-
|
|
4518
|
+
parsed2 = JSON.parse(body);
|
|
4478
4519
|
} catch {
|
|
4479
4520
|
return body;
|
|
4480
4521
|
}
|
|
4481
|
-
if (Array.isArray(
|
|
4522
|
+
if (Array.isArray(parsed2)) {
|
|
4482
4523
|
let key;
|
|
4483
|
-
for (const el of
|
|
4524
|
+
for (const el of parsed2) {
|
|
4484
4525
|
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
4485
4526
|
key ??= Object.keys(el)[0];
|
|
4486
4527
|
if (key) delete el[key];
|
|
4487
4528
|
}
|
|
4488
4529
|
}
|
|
4489
|
-
return JSON.stringify(
|
|
4530
|
+
return JSON.stringify(parsed2);
|
|
4490
4531
|
}
|
|
4491
|
-
if (
|
|
4492
|
-
const obj =
|
|
4532
|
+
if (parsed2 && typeof parsed2 === "object") {
|
|
4533
|
+
const obj = parsed2;
|
|
4493
4534
|
const key = Object.keys(obj)[0];
|
|
4494
4535
|
if (key) delete obj[key];
|
|
4495
4536
|
return JSON.stringify(obj);
|
|
@@ -5297,8 +5338,8 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
5297
5338
|
|
|
5298
5339
|
// src/internal/submit/run-submitter.ts
|
|
5299
5340
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
5300
|
-
function markSubmitFallback(run,
|
|
5301
|
-
run.metadata.ingestHealth = { submitFallback: { path:
|
|
5341
|
+
function markSubmitFallback(run, path32, reason) {
|
|
5342
|
+
run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
|
|
5302
5343
|
}
|
|
5303
5344
|
var RunSubmitter = class {
|
|
5304
5345
|
/**
|
|
@@ -7858,7 +7899,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
7858
7899
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
7859
7900
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
7860
7901
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
7861
|
-
const
|
|
7902
|
+
const relative14 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
7862
7903
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
7863
7904
|
const perWorker = Math.max(
|
|
7864
7905
|
60,
|
|
@@ -7866,17 +7907,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
7866
7907
|
);
|
|
7867
7908
|
return {
|
|
7868
7909
|
startedAt,
|
|
7869
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
7870
|
-
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") : [],
|
|
7871
7912
|
pages: workers.map(({ worker, events }) => {
|
|
7872
|
-
const points =
|
|
7913
|
+
const points = relative14(events, -startedAt);
|
|
7873
7914
|
const last = points.pop();
|
|
7874
7915
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
7875
7916
|
}),
|
|
7876
7917
|
workers: trees.map((tree) => ({
|
|
7877
7918
|
worker: tree.worker,
|
|
7878
|
-
cpuCores: downsamplePoints(
|
|
7879
|
-
memoryBytes: downsamplePoints(
|
|
7919
|
+
cpuCores: downsamplePoints(relative14(tree.cpuCores, shift), perWorker, "mean"),
|
|
7920
|
+
memoryBytes: downsamplePoints(relative14(tree.memoryBytes, shift), perWorker, "max")
|
|
7880
7921
|
}))
|
|
7881
7922
|
};
|
|
7882
7923
|
}
|
|
@@ -8039,8 +8080,8 @@ function formatTimeout(ms) {
|
|
|
8039
8080
|
}
|
|
8040
8081
|
function routeOf(url) {
|
|
8041
8082
|
try {
|
|
8042
|
-
const
|
|
8043
|
-
return `${
|
|
8083
|
+
const parsed2 = new URL(url);
|
|
8084
|
+
return `${parsed2.pathname}${parsed2.search}` || "/";
|
|
8044
8085
|
} catch {
|
|
8045
8086
|
return url;
|
|
8046
8087
|
}
|
|
@@ -8060,14 +8101,14 @@ function textOfGetByText(locator) {
|
|
|
8060
8101
|
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
8061
8102
|
return m ? m[2] : null;
|
|
8062
8103
|
}
|
|
8063
|
-
function receivedDisplay(
|
|
8064
|
-
const received =
|
|
8065
|
-
if (
|
|
8104
|
+
function receivedDisplay(parsed2) {
|
|
8105
|
+
const received = parsed2.received ?? "";
|
|
8106
|
+
if (parsed2.assertion !== "toHaveURL") return received;
|
|
8066
8107
|
const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
|
|
8067
8108
|
return m ? `"${routeOf(m[1])}"` : received;
|
|
8068
8109
|
}
|
|
8069
|
-
function firstLineFallback(
|
|
8070
|
-
const line = (
|
|
8110
|
+
function firstLineFallback(parsed2) {
|
|
8111
|
+
const line = (parsed2.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
|
|
8071
8112
|
return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
|
|
8072
8113
|
}
|
|
8073
8114
|
var Line = class {
|
|
@@ -8104,148 +8145,148 @@ function timeoutSuffix(timeoutMs) {
|
|
|
8104
8145
|
function timeoutParen(timeoutMs) {
|
|
8105
8146
|
return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
|
|
8106
8147
|
}
|
|
8107
|
-
function buildActionTimeout(
|
|
8148
|
+
function buildActionTimeout(parsed2, opts) {
|
|
8108
8149
|
const line = new Line();
|
|
8109
|
-
const verb = ACTION_VERBS[
|
|
8110
|
-
const state = STATE_PHRASES[
|
|
8111
|
-
if (
|
|
8112
|
-
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));
|
|
8113
8154
|
}
|
|
8114
|
-
if (
|
|
8115
|
-
const count =
|
|
8116
|
-
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));
|
|
8117
8158
|
}
|
|
8118
|
-
if (
|
|
8119
|
-
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));
|
|
8120
8161
|
}
|
|
8121
|
-
const call =
|
|
8122
|
-
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));
|
|
8123
8164
|
}
|
|
8124
|
-
function buildAssertion(
|
|
8165
|
+
function buildAssertion(parsed2, opts) {
|
|
8125
8166
|
const line = new Line();
|
|
8126
|
-
const matcher =
|
|
8127
|
-
const notFound =
|
|
8128
|
-
const expected =
|
|
8129
|
-
const received =
|
|
8130
|
-
if (matcher === "toHaveCount" &&
|
|
8131
|
-
const want = Number(
|
|
8132
|
-
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);
|
|
8133
8174
|
const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
|
|
8134
|
-
const noun2 = countNoun(
|
|
8175
|
+
const noun2 = countNoun(parsed2.locator, Number.isFinite(want) ? want : 2);
|
|
8135
8176
|
const gotText = Number.isFinite(got) ? got === 0 ? "none" : String(got) : received ?? "none";
|
|
8136
|
-
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");
|
|
8137
8178
|
}
|
|
8138
8179
|
const state = STATE_MATCHERS[matcher];
|
|
8139
8180
|
if (state) {
|
|
8140
|
-
const unmet =
|
|
8141
|
-
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));
|
|
8142
8183
|
if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
|
|
8143
|
-
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));
|
|
8144
8185
|
}
|
|
8145
|
-
return subject(line,
|
|
8186
|
+
return subject(line, parsed2.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8146
8187
|
}
|
|
8147
8188
|
const noun = VALUE_MATCHERS[matcher];
|
|
8148
8189
|
if (noun) {
|
|
8149
|
-
if (
|
|
8150
|
-
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));
|
|
8151
8192
|
}
|
|
8152
8193
|
line.text(`Expected ${noun} `);
|
|
8153
8194
|
if (expected) line.value(expected);
|
|
8154
8195
|
else line.text("to match");
|
|
8155
8196
|
if (received) line.text(", got ").value(received);
|
|
8156
|
-
if (
|
|
8157
|
-
line.text(" \u2014 ").locator(opts.locator ??
|
|
8197
|
+
if (parsed2.locator)
|
|
8198
|
+
line.text(" \u2014 ").locator(opts.locator ?? parsed2.locator).text(` ${matcher}`);
|
|
8158
8199
|
else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
|
|
8159
8200
|
return line;
|
|
8160
8201
|
}
|
|
8161
|
-
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));
|
|
8162
8203
|
if (expected || received) {
|
|
8163
8204
|
line.text("Expected ");
|
|
8164
8205
|
if (expected) line.value(expected);
|
|
8165
8206
|
else line.text("a different value");
|
|
8166
8207
|
if (received) line.text(", got ").value(received);
|
|
8167
8208
|
line.text(` \u2014 ${matcher}`);
|
|
8168
|
-
if (
|
|
8209
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8169
8210
|
return line;
|
|
8170
8211
|
}
|
|
8171
|
-
if (
|
|
8172
|
-
return subject(line,
|
|
8212
|
+
if (parsed2.locator) {
|
|
8213
|
+
return subject(line, parsed2.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed2.timeoutMs));
|
|
8173
8214
|
}
|
|
8174
8215
|
return line.text(`${matcher} assertion failed`);
|
|
8175
8216
|
}
|
|
8176
|
-
function buildStrictMode(
|
|
8217
|
+
function buildStrictMode(parsed2, opts) {
|
|
8177
8218
|
const line = new Line();
|
|
8178
|
-
const count =
|
|
8179
|
-
if (!
|
|
8180
|
-
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`);
|
|
8181
8222
|
}
|
|
8182
|
-
function buildNavigation(
|
|
8223
|
+
function buildNavigation(parsed2, opts) {
|
|
8183
8224
|
const line = new Line();
|
|
8184
|
-
const code =
|
|
8225
|
+
const code = parsed2.networkErrorCode?.replace(/^net::/, "") ?? null;
|
|
8185
8226
|
if (code) {
|
|
8186
8227
|
const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
|
|
8187
8228
|
line.text(phrase);
|
|
8188
|
-
if (
|
|
8229
|
+
if (parsed2.url) line.text(" ").value(truncateValue(parsed2.url, opts.valueMax * 2));
|
|
8189
8230
|
return line;
|
|
8190
8231
|
}
|
|
8191
|
-
const route =
|
|
8192
|
-
const timedOut =
|
|
8193
|
-
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") {
|
|
8194
8235
|
line.text("Never navigated to ");
|
|
8195
8236
|
if (route) line.value(truncateValue(route, opts.valueMax));
|
|
8196
8237
|
else line.text("the expected URL");
|
|
8197
|
-
return line.text(` \u2014 ${
|
|
8238
|
+
return line.text(` \u2014 ${parsed2.action} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
|
|
8198
8239
|
}
|
|
8199
8240
|
line.text("Navigation");
|
|
8200
8241
|
if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
|
|
8201
|
-
if (timedOut) return line.text(" timed out").text(timeoutSuffix(
|
|
8242
|
+
if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
|
|
8202
8243
|
return line.text(" failed");
|
|
8203
8244
|
}
|
|
8204
|
-
function buildTestTimeout(
|
|
8205
|
-
const line = new Line().text("Test timed out").text(timeoutSuffix(
|
|
8206
|
-
if (
|
|
8207
|
-
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;
|
|
8208
8249
|
const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
|
|
8209
8250
|
return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
|
|
8210
8251
|
}
|
|
8211
|
-
if (
|
|
8212
|
-
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));
|
|
8213
8254
|
}
|
|
8214
|
-
if (
|
|
8215
|
-
const state = STATE_MATCHERS[
|
|
8255
|
+
if (parsed2.assertion && parsed2.locator) {
|
|
8256
|
+
const state = STATE_MATCHERS[parsed2.assertion];
|
|
8216
8257
|
line.text(state ? ` while waiting for ` : " while expecting ");
|
|
8217
|
-
subject(line,
|
|
8218
|
-
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}`);
|
|
8219
8260
|
}
|
|
8220
|
-
if (
|
|
8221
|
-
const gerund = ACTION_GERUNDS[
|
|
8261
|
+
if (parsed2.action && parsed2.locator) {
|
|
8262
|
+
const gerund = ACTION_GERUNDS[parsed2.action] ?? `${ACTION_VERBS[parsed2.action] ?? parsed2.action} on`;
|
|
8222
8263
|
line.text(` while ${gerund} `);
|
|
8223
|
-
return subject(line,
|
|
8264
|
+
return subject(line, parsed2.locator, opts);
|
|
8224
8265
|
}
|
|
8225
|
-
if (
|
|
8266
|
+
if (parsed2.subject && parsed2.action) return line.text(` during ${parsed2.subject}.${parsed2.action}`);
|
|
8226
8267
|
const step = ctx?.lastStepTitle?.trim();
|
|
8227
8268
|
if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
|
|
8228
8269
|
return line;
|
|
8229
8270
|
}
|
|
8230
|
-
function buildCrash(
|
|
8271
|
+
function buildCrash(parsed2, opts) {
|
|
8231
8272
|
const line = new Line();
|
|
8232
|
-
const what = /Page crashed/i.test(
|
|
8233
|
-
if (
|
|
8234
|
-
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;
|
|
8235
8276
|
line.text(`${what} during ${verb}`);
|
|
8236
|
-
if (
|
|
8277
|
+
if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
|
|
8237
8278
|
return line;
|
|
8238
8279
|
}
|
|
8239
|
-
if (
|
|
8240
|
-
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}`);
|
|
8241
8282
|
}
|
|
8242
8283
|
return line.text(what);
|
|
8243
8284
|
}
|
|
8244
8285
|
var CUSTOM_MESSAGE_MAX_CHARS = 60;
|
|
8245
8286
|
var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
|
|
8246
|
-
function withLeads(line,
|
|
8247
|
-
const hook = ctx?.failedIn && !
|
|
8248
|
-
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;
|
|
8249
8290
|
if (!hook && !message) return line;
|
|
8250
8291
|
const led = new Line();
|
|
8251
8292
|
if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
|
|
@@ -8258,49 +8299,49 @@ function withLeads(line, parsed, ctx) {
|
|
|
8258
8299
|
});
|
|
8259
8300
|
return led;
|
|
8260
8301
|
}
|
|
8261
|
-
function build(
|
|
8262
|
-
return withLeads(buildLine(
|
|
8302
|
+
function build(parsed2, opts, ctx) {
|
|
8303
|
+
return withLeads(buildLine(parsed2, opts, ctx), parsed2, ctx);
|
|
8263
8304
|
}
|
|
8264
|
-
function buildLine(
|
|
8265
|
-
switch (
|
|
8305
|
+
function buildLine(parsed2, opts, ctx) {
|
|
8306
|
+
switch (parsed2.kind) {
|
|
8266
8307
|
case "action-timeout":
|
|
8267
|
-
return buildActionTimeout(
|
|
8308
|
+
return buildActionTimeout(parsed2, opts);
|
|
8268
8309
|
case "assertion":
|
|
8269
8310
|
case "assertion-timeout":
|
|
8270
|
-
return buildAssertion(
|
|
8311
|
+
return buildAssertion(parsed2, opts);
|
|
8271
8312
|
case "strict-mode":
|
|
8272
|
-
return buildStrictMode(
|
|
8313
|
+
return buildStrictMode(parsed2, opts);
|
|
8273
8314
|
case "navigation":
|
|
8274
|
-
return buildNavigation(
|
|
8315
|
+
return buildNavigation(parsed2, opts);
|
|
8275
8316
|
case "test-timeout":
|
|
8276
|
-
return buildTestTimeout(
|
|
8317
|
+
return buildTestTimeout(parsed2, opts, ctx);
|
|
8277
8318
|
case "crash":
|
|
8278
|
-
return buildCrash(
|
|
8319
|
+
return buildCrash(parsed2, opts);
|
|
8279
8320
|
default:
|
|
8280
|
-
return new Line().text(firstLineFallback(
|
|
8321
|
+
return new Line().text(firstLineFallback(parsed2));
|
|
8281
8322
|
}
|
|
8282
8323
|
}
|
|
8283
|
-
function detailOf(
|
|
8284
|
-
switch (
|
|
8324
|
+
function detailOf(parsed2, headline) {
|
|
8325
|
+
switch (parsed2.kind) {
|
|
8285
8326
|
case "test-timeout":
|
|
8286
8327
|
case "action-timeout": {
|
|
8287
|
-
const state =
|
|
8288
|
-
if (!state ||
|
|
8328
|
+
const state = parsed2.lastStateLine;
|
|
8329
|
+
if (!state || parsed2.lastState === "not-found" || headline.includes(state)) return null;
|
|
8289
8330
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8290
8331
|
}
|
|
8291
8332
|
case "assertion":
|
|
8292
8333
|
case "assertion-timeout": {
|
|
8293
|
-
const received =
|
|
8334
|
+
const received = parsed2.received;
|
|
8294
8335
|
if (received && !/element\(s\) not found/.test(received)) {
|
|
8295
|
-
const shown = truncateValue(receivedDisplay(
|
|
8336
|
+
const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
|
|
8296
8337
|
if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
|
|
8297
8338
|
}
|
|
8298
|
-
const state =
|
|
8339
|
+
const state = parsed2.lastStateLine;
|
|
8299
8340
|
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
8300
8341
|
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
8301
8342
|
}
|
|
8302
8343
|
case "navigation":
|
|
8303
|
-
return
|
|
8344
|
+
return parsed2.networkErrorCode ?? parsed2.lastStateLine;
|
|
8304
8345
|
case "strict-mode":
|
|
8305
8346
|
case "crash":
|
|
8306
8347
|
return null;
|
|
@@ -8308,16 +8349,16 @@ function detailOf(parsed, headline) {
|
|
|
8308
8349
|
return null;
|
|
8309
8350
|
}
|
|
8310
8351
|
}
|
|
8311
|
-
function describeFailure(
|
|
8352
|
+
function describeFailure(parsed2, ctx) {
|
|
8312
8353
|
const attempts = [
|
|
8313
|
-
{ locator:
|
|
8314
|
-
{ locator:
|
|
8315
|
-
{ 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 }
|
|
8316
8357
|
];
|
|
8317
|
-
let line = build(
|
|
8358
|
+
let line = build(parsed2, attempts[0], ctx);
|
|
8318
8359
|
for (const opts of attempts.slice(1)) {
|
|
8319
8360
|
if (line.toString().length <= HEADLINE_MAX_CHARS) break;
|
|
8320
|
-
line = build(
|
|
8361
|
+
line = build(parsed2, opts, ctx);
|
|
8321
8362
|
}
|
|
8322
8363
|
let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
|
|
8323
8364
|
let headline = parts.map((p) => p.text).join("");
|
|
@@ -8327,10 +8368,10 @@ function describeFailure(parsed, ctx) {
|
|
|
8327
8368
|
parts.push({ kind: "text", text: "\u2026" });
|
|
8328
8369
|
}
|
|
8329
8370
|
if (!headline.trim()) {
|
|
8330
|
-
headline = firstLineFallback(
|
|
8371
|
+
headline = firstLineFallback(parsed2);
|
|
8331
8372
|
parts = [{ kind: "text", text: headline }];
|
|
8332
8373
|
}
|
|
8333
|
-
return { headline, detail: detailOf(
|
|
8374
|
+
return { headline, detail: detailOf(parsed2, headline), parts };
|
|
8334
8375
|
}
|
|
8335
8376
|
function clipParts(parts, max) {
|
|
8336
8377
|
const out = [];
|
|
@@ -9040,7 +9081,7 @@ async function resolveSelection(options = {}) {
|
|
|
9040
9081
|
}
|
|
9041
9082
|
|
|
9042
9083
|
// src/internal/capture/capture-fixtures.ts
|
|
9043
|
-
var
|
|
9084
|
+
var path28 = __toESM(require("path"));
|
|
9044
9085
|
var import_node_zlib2 = require("zlib");
|
|
9045
9086
|
|
|
9046
9087
|
// ../picker-dom/src/probe.ts
|
|
@@ -10103,6 +10144,83 @@ function showPickerChoices(arg) {
|
|
|
10103
10144
|
doc.body.appendChild(wrap);
|
|
10104
10145
|
}
|
|
10105
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
|
+
|
|
10106
10224
|
// ../core/src/locator-generation.ts
|
|
10107
10225
|
var TAG_TO_ROLE = {
|
|
10108
10226
|
a: "link",
|
|
@@ -11139,11 +11257,11 @@ function deriveFailedLocator(testInfo) {
|
|
|
11139
11257
|
${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
11140
11258
|
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
11141
11259
|
if (!line) continue;
|
|
11142
|
-
const
|
|
11143
|
-
if (!
|
|
11260
|
+
const parsed2 = parseLeafLocatorExpression(line[1].trim());
|
|
11261
|
+
if (!parsed2) continue;
|
|
11144
11262
|
const loc = err.location;
|
|
11145
11263
|
const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
11146
|
-
return { method:
|
|
11264
|
+
return { method: parsed2.method, args: parsed2.args, location };
|
|
11147
11265
|
}
|
|
11148
11266
|
return null;
|
|
11149
11267
|
}
|
|
@@ -11165,25 +11283,16 @@ async function cleanupPicker(page) {
|
|
|
11165
11283
|
} catch {
|
|
11166
11284
|
}
|
|
11167
11285
|
}
|
|
11168
|
-
async function
|
|
11286
|
+
async function pickLocator(page, probe, failing) {
|
|
11169
11287
|
try {
|
|
11170
|
-
|
|
11171
|
-
const rendered = failed ? renderFailing(failed) : null;
|
|
11172
|
-
console.log(
|
|
11173
|
-
`
|
|
11174
|
-
[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).`
|
|
11175
|
-
);
|
|
11176
|
-
const overlayArg = { transport: "global", failing: rendered };
|
|
11288
|
+
const overlayArg = { transport: "global", failing };
|
|
11177
11289
|
await page.evaluate(installPickerOverlay, overlayArg);
|
|
11178
11290
|
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
11179
11291
|
timeout: 0,
|
|
11180
11292
|
polling: 250
|
|
11181
11293
|
});
|
|
11182
11294
|
const state = await page.evaluate(() => globalThis.__piwiPickState);
|
|
11183
|
-
if (state !== "picked")
|
|
11184
|
-
await cleanupPicker(page);
|
|
11185
|
-
return null;
|
|
11186
|
-
}
|
|
11295
|
+
if (state !== "picked") return null;
|
|
11187
11296
|
const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
|
|
11188
11297
|
const attrs = await handle.evaluate(probe.fn, probe.arg);
|
|
11189
11298
|
await handle.dispose();
|
|
@@ -11221,11 +11330,10 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11221
11330
|
);
|
|
11222
11331
|
if (ranked.length === 0) {
|
|
11223
11332
|
console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
|
|
11224
|
-
await cleanupPicker(page);
|
|
11225
11333
|
return null;
|
|
11226
11334
|
}
|
|
11227
11335
|
await page.evaluate(showPickerChoices, {
|
|
11228
|
-
failing
|
|
11336
|
+
failing,
|
|
11229
11337
|
choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
|
|
11230
11338
|
});
|
|
11231
11339
|
await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
|
|
@@ -11233,31 +11341,45 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
11233
11341
|
polling: 250
|
|
11234
11342
|
});
|
|
11235
11343
|
const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
|
|
11236
|
-
await cleanupPicker(page);
|
|
11237
11344
|
if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
|
|
11238
11345
|
const picked = { ...ranked[choice], pickedByUser: true };
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
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 } : {}
|
|
11248
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;
|
|
11249
11374
|
const location = failed?.location ?? null;
|
|
11250
11375
|
console.log(
|
|
11251
11376
|
failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
|
|
11252
|
-
[piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11253
|
-
[piwi] ${picked.locator}`
|
|
11377
|
+
[piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
11378
|
+
[piwi] ${pick.picked.locator}`
|
|
11254
11379
|
);
|
|
11255
11380
|
return {
|
|
11256
11381
|
failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
|
|
11257
|
-
|
|
11258
|
-
alternatives,
|
|
11259
|
-
element,
|
|
11260
|
-
...anchors.length > 0 ? { anchors } : {}
|
|
11382
|
+
...pick
|
|
11261
11383
|
};
|
|
11262
11384
|
} catch {
|
|
11263
11385
|
return null;
|
|
@@ -11290,10 +11412,207 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
11290
11412
|
return true;
|
|
11291
11413
|
}
|
|
11292
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
|
+
|
|
11293
11612
|
// src/internal/capture/code-reach.ts
|
|
11294
11613
|
var crypto3 = __toESM(require("crypto"));
|
|
11295
11614
|
var fs22 = __toESM(require("fs"));
|
|
11296
|
-
var
|
|
11615
|
+
var path27 = __toESM(require("path"));
|
|
11297
11616
|
var import_node_child_process3 = require("child_process");
|
|
11298
11617
|
|
|
11299
11618
|
// ../core/src/source-map.ts
|
|
@@ -11556,12 +11875,12 @@ function repositoryRoot(from) {
|
|
|
11556
11875
|
}).trim();
|
|
11557
11876
|
} catch {
|
|
11558
11877
|
}
|
|
11559
|
-
cachedRepoRoot = { from, root:
|
|
11878
|
+
cachedRepoRoot = { from, root: path27.resolve(root) };
|
|
11560
11879
|
return cachedRepoRoot.root;
|
|
11561
11880
|
}
|
|
11562
11881
|
function codeReachRoots(configDir, configured) {
|
|
11563
11882
|
const repoRoot = repositoryRoot(configDir);
|
|
11564
|
-
const roots = configured?.length ? configured.map((r) =>
|
|
11883
|
+
const roots = configured?.length ? configured.map((r) => path27.resolve(configDir, r)) : [configDir];
|
|
11565
11884
|
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
11566
11885
|
}
|
|
11567
11886
|
function isFile(file) {
|
|
@@ -11574,10 +11893,10 @@ function isFile(file) {
|
|
|
11574
11893
|
function resolveSourcePath(source, roots) {
|
|
11575
11894
|
const normalized = normalizeSourcePath(source);
|
|
11576
11895
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
11577
|
-
if (
|
|
11578
|
-
const
|
|
11896
|
+
if (path27.isAbsolute(normalized) && isFile(normalized)) return path27.resolve(normalized);
|
|
11897
|
+
const relative14 = normalized.replace(/^\/+/, "");
|
|
11579
11898
|
for (const root of roots) {
|
|
11580
|
-
const candidate =
|
|
11899
|
+
const candidate = path27.resolve(root, relative14);
|
|
11581
11900
|
if (isFile(candidate)) return candidate;
|
|
11582
11901
|
}
|
|
11583
11902
|
return null;
|
|
@@ -11593,10 +11912,10 @@ function viteBases(entries) {
|
|
|
11593
11912
|
function fileForUrl(url, roots, bases) {
|
|
11594
11913
|
let pathname;
|
|
11595
11914
|
try {
|
|
11596
|
-
const
|
|
11597
|
-
if (
|
|
11598
|
-
pathname =
|
|
11599
|
-
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);
|
|
11600
11919
|
if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
|
|
11601
11920
|
} catch {
|
|
11602
11921
|
return null;
|
|
@@ -11609,7 +11928,7 @@ function fileForUrl(url, roots, bases) {
|
|
|
11609
11928
|
return file;
|
|
11610
11929
|
}
|
|
11611
11930
|
function relativeToRepo(file, repoRoot) {
|
|
11612
|
-
return
|
|
11931
|
+
return path27.relative(repoRoot, file).split(path27.sep).join("/");
|
|
11613
11932
|
}
|
|
11614
11933
|
function scriptHash(entry) {
|
|
11615
11934
|
return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
@@ -11779,6 +12098,7 @@ function createSink() {
|
|
|
11779
12098
|
flake: null,
|
|
11780
12099
|
pickOffered: false,
|
|
11781
12100
|
userPick: null,
|
|
12101
|
+
pause: null,
|
|
11782
12102
|
locatorPages: new LocatorPageLog(),
|
|
11783
12103
|
codeReach: null
|
|
11784
12104
|
};
|
|
@@ -11795,11 +12115,11 @@ function configuredCodeReachRoots() {
|
|
|
11795
12115
|
const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
|
|
11796
12116
|
if (!raw) return null;
|
|
11797
12117
|
try {
|
|
11798
|
-
const
|
|
11799
|
-
if (Array.isArray(
|
|
12118
|
+
const parsed2 = JSON.parse(raw);
|
|
12119
|
+
if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
|
|
11800
12120
|
} catch {
|
|
11801
12121
|
}
|
|
11802
|
-
return raw.split(
|
|
12122
|
+
return raw.split(path28.delimiter).filter(Boolean);
|
|
11803
12123
|
}
|
|
11804
12124
|
async function stopSinkCodeReach(sink, closing) {
|
|
11805
12125
|
const session = sink.codeReach;
|
|
@@ -11814,7 +12134,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
11814
12134
|
if (!entries) return;
|
|
11815
12135
|
const info = sink.testInfo;
|
|
11816
12136
|
const configFile = info?.config.configFile;
|
|
11817
|
-
const configDir = configFile ?
|
|
12137
|
+
const configDir = configFile ? path28.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
11818
12138
|
session.files = await resolveCodeReach(
|
|
11819
12139
|
entries,
|
|
11820
12140
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -12233,6 +12553,53 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
|
12233
12553
|
} catch {
|
|
12234
12554
|
}
|
|
12235
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
|
+
}
|
|
12236
12603
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
12237
12604
|
return new Proxy(locator, {
|
|
12238
12605
|
get(target, prop) {
|
|
@@ -12254,6 +12621,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12254
12621
|
const isNot = Boolean(callArgs[1]?.isNot);
|
|
12255
12622
|
const callerLocation = sink ? captureCallerLocation() : null;
|
|
12256
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
|
+
}
|
|
12257
12630
|
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
12258
12631
|
return callMethod(target, prop, callArgs);
|
|
12259
12632
|
}
|
|
@@ -12292,6 +12665,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
12292
12665
|
args: originArgs,
|
|
12293
12666
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
12294
12667
|
};
|
|
12668
|
+
if (sink.pause && pausesAt(sink.pause, callerLocation)) {
|
|
12669
|
+
await pauseBefore(sink, page, target, callerLocation, String(prop), used);
|
|
12670
|
+
}
|
|
12295
12671
|
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
12296
12672
|
let seq = -1;
|
|
12297
12673
|
if (!alreadyProbed) {
|
|
@@ -12752,6 +13128,12 @@ var piwiFixtures = {
|
|
|
12752
13128
|
if (conflict) throw new Error(conflict);
|
|
12753
13129
|
const sink = createSink();
|
|
12754
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
|
+
}
|
|
12755
13137
|
if (isFlakeMode()) {
|
|
12756
13138
|
const plan = loadFlakePlan();
|
|
12757
13139
|
const role = flakeRoleForTest(plan, {
|
|
@@ -12831,12 +13213,12 @@ function extendPiwiFixtures(test) {
|
|
|
12831
13213
|
|
|
12832
13214
|
// src/internal/ai/ai-fixtures.ts
|
|
12833
13215
|
var fs24 = __toESM(require("fs"));
|
|
12834
|
-
var
|
|
13216
|
+
var path31 = __toESM(require("path"));
|
|
12835
13217
|
var import_test = require("@playwright/test");
|
|
12836
13218
|
|
|
12837
13219
|
// src/internal/ai/artifact.ts
|
|
12838
13220
|
var fs23 = __toESM(require("fs"));
|
|
12839
|
-
var
|
|
13221
|
+
var path29 = __toESM(require("path"));
|
|
12840
13222
|
var ARTIFACT_VERSION = 1;
|
|
12841
13223
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
12842
13224
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -12940,7 +13322,7 @@ function writeEntry(file, entry) {
|
|
|
12940
13322
|
const canonical = serializeEntry(entry);
|
|
12941
13323
|
return withEntryLock(file, () => {
|
|
12942
13324
|
if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
12943
|
-
fs23.mkdirSync(
|
|
13325
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12944
13326
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
12945
13327
|
fs23.writeFileSync(tmp, canonical);
|
|
12946
13328
|
fs23.renameSync(tmp, file);
|
|
@@ -12949,7 +13331,7 @@ function writeEntry(file, entry) {
|
|
|
12949
13331
|
}
|
|
12950
13332
|
function withEntryLock(file, fn) {
|
|
12951
13333
|
const lock = `${file}.lock`;
|
|
12952
|
-
fs23.mkdirSync(
|
|
13334
|
+
fs23.mkdirSync(path29.dirname(file), { recursive: true });
|
|
12953
13335
|
const deadline = Date.now() + 5e3;
|
|
12954
13336
|
for (; ; ) {
|
|
12955
13337
|
try {
|
|
@@ -12978,7 +13360,7 @@ function withEntryLock(file, fn) {
|
|
|
12978
13360
|
|
|
12979
13361
|
// src/internal/ai/keys.ts
|
|
12980
13362
|
var crypto4 = __toESM(require("crypto"));
|
|
12981
|
-
var
|
|
13363
|
+
var path30 = __toESM(require("path"));
|
|
12982
13364
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
12983
13365
|
function normalizeTemplate(template) {
|
|
12984
13366
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -12992,13 +13374,13 @@ function slug(text, maxLength = 40) {
|
|
|
12992
13374
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
12993
13375
|
}
|
|
12994
13376
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
12995
|
-
return
|
|
13377
|
+
return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
|
|
12996
13378
|
}
|
|
12997
13379
|
function entryPath(params) {
|
|
12998
13380
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
12999
13381
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
13000
13382
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
13001
|
-
return
|
|
13383
|
+
return path30.join(entryDir(params.specFile, params.dir), name);
|
|
13002
13384
|
}
|
|
13003
13385
|
function parseLocation2(location) {
|
|
13004
13386
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -13178,9 +13560,9 @@ function renderSourceArg(arg) {
|
|
|
13178
13560
|
if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
|
|
13179
13561
|
return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
|
|
13180
13562
|
}
|
|
13181
|
-
function
|
|
13563
|
+
function locatorSource2(loc) {
|
|
13182
13564
|
const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
|
|
13183
|
-
const tail = (loc.chain ?? []).map(
|
|
13565
|
+
const tail = (loc.chain ?? []).map(locatorSource2).join(".");
|
|
13184
13566
|
return tail ? `${head}.${tail}` : head;
|
|
13185
13567
|
}
|
|
13186
13568
|
function describePostcondition(post) {
|
|
@@ -13512,18 +13894,18 @@ function testIdentity(testInfo) {
|
|
|
13512
13894
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
13513
13895
|
}
|
|
13514
13896
|
function relativeToCwd(file) {
|
|
13515
|
-
return
|
|
13897
|
+
return path31.relative(process.cwd(), file).split(path31.sep).join("/");
|
|
13516
13898
|
}
|
|
13517
13899
|
function recordIntents(intents, entry) {
|
|
13518
13900
|
const add = (locator, kind) => {
|
|
13519
13901
|
intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
|
|
13520
13902
|
};
|
|
13521
13903
|
if (entry.kind === "locator") {
|
|
13522
|
-
add(
|
|
13904
|
+
add(locatorSource2(entry.locator), "locator");
|
|
13523
13905
|
return;
|
|
13524
13906
|
}
|
|
13525
|
-
for (const step of entry.steps) add(
|
|
13526
|
-
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");
|
|
13527
13909
|
}
|
|
13528
13910
|
function missMessage(template, testTitle, relPath) {
|
|
13529
13911
|
return [
|