@piwitests/reporter 0.61.0 → 0.62.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 +369 -223
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1794 -266
- package/dist/internal/capture/attachments.d.ts +24 -1
- package/dist/internal/capture/attachments.js +21 -2
- package/dist/internal/capture/capture-fixtures.d.ts +114 -6
- package/dist/internal/capture/capture-fixtures.js +1597 -176
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -2
- package/templates/skills/fix-a-reported-bug/SKILL.md +11 -8
- package/templates/skills/investigate-failure/SKILL.md +3 -3
- package/templates/skills/setup-piwi/SKILL.md +11 -8
- package/templates/skills/stabilize-flaky-tests/SKILL.md +3 -3
- package/templates/skills/write-the-missing-test/SKILL.md +13 -10
|
@@ -42,6 +42,8 @@ __export(capture_fixtures_exports, {
|
|
|
42
42
|
computeCoreVitals: () => computeCoreVitals,
|
|
43
43
|
configuredCodeReachRoots: () => configuredCodeReachRoots,
|
|
44
44
|
extendPiwiFixtures: () => extendPiwiFixtures,
|
|
45
|
+
failureError: () => failureError,
|
|
46
|
+
intentAt: () => intentAt,
|
|
45
47
|
internalCall: () => internalCall,
|
|
46
48
|
noteLocatorCall: () => noteLocatorCall,
|
|
47
49
|
noteNavigation: () => noteNavigation,
|
|
@@ -823,14 +825,14 @@ function showAnchorPicker(arg) {
|
|
|
823
825
|
} catch {
|
|
824
826
|
shadowRoots.length = 0;
|
|
825
827
|
}
|
|
826
|
-
const
|
|
828
|
+
const within2 = (node2, scope) => {
|
|
827
829
|
if (scope === doc) return true;
|
|
828
830
|
for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
|
|
829
831
|
return false;
|
|
830
832
|
};
|
|
831
833
|
const queryAll = (scope, sel) => {
|
|
832
834
|
const found = Array.from(scope.querySelectorAll(sel));
|
|
833
|
-
for (const sr of shadowRoots) if (
|
|
835
|
+
for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
|
|
834
836
|
return found;
|
|
835
837
|
};
|
|
836
838
|
const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
|
|
@@ -911,8 +913,8 @@ function showAnchorPicker(arg) {
|
|
|
911
913
|
return;
|
|
912
914
|
}
|
|
913
915
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
+
const outline2 = doc.createElement("div");
|
|
917
|
+
outline2.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
|
|
916
918
|
const outlineLabel = doc.createElement("div");
|
|
917
919
|
outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
918
920
|
const pickedOutline = doc.createElement("div");
|
|
@@ -1001,11 +1003,11 @@ function showAnchorPicker(arg) {
|
|
|
1001
1003
|
line.appendChild(text);
|
|
1002
1004
|
line.addEventListener("mouseenter", () => {
|
|
1003
1005
|
const r = row.node.getBoundingClientRect();
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1006
|
+
outline2.style.display = "block";
|
|
1007
|
+
outline2.style.left = r.left + "px";
|
|
1008
|
+
outline2.style.top = r.top + "px";
|
|
1009
|
+
outline2.style.width = r.width + "px";
|
|
1010
|
+
outline2.style.height = r.height + "px";
|
|
1009
1011
|
outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
1010
1012
|
outlineLabel.style.display = "block";
|
|
1011
1013
|
const lr = outlineLabel.getBoundingClientRect();
|
|
@@ -1015,7 +1017,7 @@ function showAnchorPicker(arg) {
|
|
|
1015
1017
|
outlineLabel.style.top = top + "px";
|
|
1016
1018
|
});
|
|
1017
1019
|
line.addEventListener("mouseleave", () => {
|
|
1018
|
-
|
|
1020
|
+
outline2.style.display = "none";
|
|
1019
1021
|
outlineLabel.style.display = "none";
|
|
1020
1022
|
});
|
|
1021
1023
|
box.addEventListener("change", () => {
|
|
@@ -1029,7 +1031,7 @@ function showAnchorPicker(arg) {
|
|
|
1029
1031
|
const cleanup = () => {
|
|
1030
1032
|
doc.removeEventListener("keydown", onKey, true);
|
|
1031
1033
|
panel.remove();
|
|
1032
|
-
|
|
1034
|
+
outline2.remove();
|
|
1033
1035
|
outlineLabel.remove();
|
|
1034
1036
|
pickedOutline.remove();
|
|
1035
1037
|
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
@@ -1094,7 +1096,7 @@ function showAnchorPicker(arg) {
|
|
|
1094
1096
|
);
|
|
1095
1097
|
}
|
|
1096
1098
|
};
|
|
1097
|
-
mount(pickedOutline,
|
|
1099
|
+
mount(pickedOutline, outline2, outlineLabel, panel);
|
|
1098
1100
|
}
|
|
1099
1101
|
|
|
1100
1102
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -1198,7 +1200,7 @@ function showPauseBar(arg) {
|
|
|
1198
1200
|
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;`;
|
|
1199
1201
|
const where = doc.createElement("div");
|
|
1200
1202
|
where.style.cssText = "font-weight:600;white-space:nowrap;";
|
|
1201
|
-
where.textContent =
|
|
1203
|
+
where.textContent = `${arg.failure ? "Failed at" : "Paused at"} ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
|
|
1202
1204
|
const what = doc.createElement("code");
|
|
1203
1205
|
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;";
|
|
1204
1206
|
what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
|
|
@@ -1221,13 +1223,13 @@ function showPauseBar(arg) {
|
|
|
1221
1223
|
delete g.__piwiPauseCleanup;
|
|
1222
1224
|
};
|
|
1223
1225
|
const buttons = [
|
|
1224
|
-
["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
1225
|
-
["Step", "step", "Pause again before the next call", false],
|
|
1226
|
+
arg.failure ? ["Resume", "resume", "Let the test end as it failed (Esc)", true] : ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
1227
|
+
...arg.failure ? [] : [["Step", "step", "Pause again before the next call", false]],
|
|
1226
1228
|
["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
|
|
1227
1229
|
...arg.record ? [
|
|
1228
1230
|
["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
|
|
1229
1231
|
] : [],
|
|
1230
|
-
["Finish", "finish", "Run on and pause no more in this test", false]
|
|
1232
|
+
...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]]
|
|
1231
1233
|
];
|
|
1232
1234
|
for (const [label, choice, hint, primary] of buttons) {
|
|
1233
1235
|
const btn = doc.createElement("button");
|
|
@@ -2414,6 +2416,7 @@ var ATTACHMENT_NAMES = {
|
|
|
2414
2416
|
pauses: "piwi-pauses"
|
|
2415
2417
|
};
|
|
2416
2418
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
2419
|
+
var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
2417
2420
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
2418
2421
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
2419
2422
|
|
|
@@ -2746,6 +2749,10 @@ var PIWI_LOCAL_DEBUG_ENV = {
|
|
|
2746
2749
|
pauseAt: "PIWI_PAUSE_AT",
|
|
2747
2750
|
editorSend: "PIWI_EDITOR_SEND"
|
|
2748
2751
|
};
|
|
2752
|
+
var PIWI_ORIGIN_ENV = {
|
|
2753
|
+
kind: "PIWI_ORIGIN",
|
|
2754
|
+
ref: "PIWI_ORIGIN_REF"
|
|
2755
|
+
};
|
|
2749
2756
|
var PIWI_PROBE_ENV = {
|
|
2750
2757
|
flag: "PIWI_PROBE",
|
|
2751
2758
|
plan: "PIWI_PROBE_PLAN",
|
|
@@ -2875,9 +2882,9 @@ function loadPlan() {
|
|
|
2875
2882
|
function probeItemForTest(test) {
|
|
2876
2883
|
const plan = loadPlan();
|
|
2877
2884
|
if (!plan) return null;
|
|
2878
|
-
const
|
|
2885
|
+
const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
|
|
2879
2886
|
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
2880
|
-
return matchProbeItem(plan, { title: test.title, filePath, suitePath });
|
|
2887
|
+
return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
|
|
2881
2888
|
}
|
|
2882
2889
|
function classifyProbeHandled(signals) {
|
|
2883
2890
|
const blank = signals.blankPage === true;
|
|
@@ -4066,6 +4073,258 @@ function maskUrlFragment(hash) {
|
|
|
4066
4073
|
const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
|
|
4067
4074
|
return hash.startsWith("#") ? `#${out}` : out;
|
|
4068
4075
|
}
|
|
4076
|
+
var HIDDEN_VALUE = "<hidden>";
|
|
4077
|
+
var SECRET_AUTOCOMPLETE = [
|
|
4078
|
+
"current-password",
|
|
4079
|
+
"new-password",
|
|
4080
|
+
"one-time-code",
|
|
4081
|
+
"cc-number",
|
|
4082
|
+
"cc-csc"
|
|
4083
|
+
];
|
|
4084
|
+
var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
4085
|
+
"pass",
|
|
4086
|
+
"password",
|
|
4087
|
+
"passwd",
|
|
4088
|
+
"passphrase",
|
|
4089
|
+
"passcode",
|
|
4090
|
+
"pwd",
|
|
4091
|
+
"secret",
|
|
4092
|
+
"secrets",
|
|
4093
|
+
"token",
|
|
4094
|
+
"tokens",
|
|
4095
|
+
"jwt",
|
|
4096
|
+
"bearer",
|
|
4097
|
+
"sig",
|
|
4098
|
+
"signature",
|
|
4099
|
+
"key",
|
|
4100
|
+
"keys",
|
|
4101
|
+
"apikey",
|
|
4102
|
+
"auth",
|
|
4103
|
+
"oauth",
|
|
4104
|
+
"authorization",
|
|
4105
|
+
"session",
|
|
4106
|
+
"sessionid",
|
|
4107
|
+
"jsessionid",
|
|
4108
|
+
"sid",
|
|
4109
|
+
"cookie",
|
|
4110
|
+
"cookies",
|
|
4111
|
+
"credential",
|
|
4112
|
+
"credentials",
|
|
4113
|
+
"otp",
|
|
4114
|
+
"refresh",
|
|
4115
|
+
"csrf",
|
|
4116
|
+
"xsrf"
|
|
4117
|
+
]);
|
|
4118
|
+
var SECRET_NAMES = /* @__PURE__ */ new Set(["code", "pin"]);
|
|
4119
|
+
function nameWords(name) {
|
|
4120
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
4121
|
+
}
|
|
4122
|
+
function isSecretName(name) {
|
|
4123
|
+
let secret = namesRead.get(name);
|
|
4124
|
+
if (secret === void 0) {
|
|
4125
|
+
secret = SECRET_NAMES.has(name.trim().toLowerCase()) || nameWords(name).some((word) => SECRET_WORDS.has(word));
|
|
4126
|
+
if (namesRead.size >= 1024) namesRead.clear();
|
|
4127
|
+
namesRead.set(name, secret);
|
|
4128
|
+
}
|
|
4129
|
+
return secret;
|
|
4130
|
+
}
|
|
4131
|
+
var namesRead = /* @__PURE__ */ new Map();
|
|
4132
|
+
var SECRET_SHAPES = [
|
|
4133
|
+
/\beyJ[\w-]{5,}\.[\w-]{5,}\.[\w-]*/g,
|
|
4134
|
+
/\bBearer(?:\s+|%20)[\w.~+/-]{8,}=*/gi,
|
|
4135
|
+
/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
4136
|
+
/\bwhsec_[A-Za-z0-9]{20,}/g,
|
|
4137
|
+
/\bsk-(?=[\w-]{0,200}\d)(?=[\w-]{0,200}[A-Z])[\w-]{32,}/g,
|
|
4138
|
+
/\bglpat-[\w-]{20,}/g,
|
|
4139
|
+
/\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
4140
|
+
/\bSK[0-9a-f]{32}\b/g,
|
|
4141
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
4142
|
+
/\bAIza[\w-]{30,}/g,
|
|
4143
|
+
/\bgh[opsu]_[A-Za-z0-9]{20,}/g,
|
|
4144
|
+
/\bgithub_pat_\w{20,}/g,
|
|
4145
|
+
/\bxox[abprs]-[\w-]{8,}/g
|
|
4146
|
+
];
|
|
4147
|
+
var WHOLE_SHAPES = SECRET_SHAPES.map(
|
|
4148
|
+
(re) => new RegExp(`^(?:${re.source})$`, re.flags.replace("g", ""))
|
|
4149
|
+
);
|
|
4150
|
+
var BASIC = /\bBasic\s+([A-Za-z0-9+/]{4,}={0,2})(?![\w+/=])/g;
|
|
4151
|
+
var PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]{0,40}PRIVATE KEY)-----/g;
|
|
4152
|
+
var SET_COOKIE = /(\bSet-Cookie:[ \t]*[^=;\s]{1,256}=)([^;\r\n]{1,4096})/gi;
|
|
4153
|
+
var COOKIE_LINE = /((?<![\w-])Cookie:[ \t]*)([^\r\n]{1,16384})/gi;
|
|
4154
|
+
var JSON_FIELD = /("((?:[^"\\\r\n]|\\.){1,256})"\s*:\s*")((?:[^"\\\r\n]|\\.){1,4096})"/g;
|
|
4155
|
+
var TOKEN_RUN = /[A-Za-z0-9+/_-]{40,}={0,2}/g;
|
|
4156
|
+
var MIN_RANDOM = 40;
|
|
4157
|
+
var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4158
|
+
function base64OfText(run) {
|
|
4159
|
+
let bits = 0;
|
|
4160
|
+
let acc = 0;
|
|
4161
|
+
let bytes = 0;
|
|
4162
|
+
let printable = 0;
|
|
4163
|
+
for (const c of run.replace(/=+$/, "")) {
|
|
4164
|
+
const digit = BASE64_DIGITS.indexOf(c === "-" ? "+" : c === "_" ? "/" : c);
|
|
4165
|
+
if (digit < 0) return false;
|
|
4166
|
+
acc = (acc << 6 | digit) & 65535;
|
|
4167
|
+
bits += 6;
|
|
4168
|
+
if (bits < 8) continue;
|
|
4169
|
+
bits -= 8;
|
|
4170
|
+
const byte = acc >> bits & 255;
|
|
4171
|
+
bytes++;
|
|
4172
|
+
if (byte >= 32 && byte < 127 || byte === 9 || byte === 10 || byte === 13) printable++;
|
|
4173
|
+
}
|
|
4174
|
+
return bytes > 0 && printable * 10 >= bytes * 9;
|
|
4175
|
+
}
|
|
4176
|
+
function bitsPerChar(text) {
|
|
4177
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4178
|
+
for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
4179
|
+
let bits = 0;
|
|
4180
|
+
for (const n of counts.values()) bits -= n / text.length * Math.log2(n / text.length);
|
|
4181
|
+
return bits;
|
|
4182
|
+
}
|
|
4183
|
+
function looksRandom(text) {
|
|
4184
|
+
if (text.length < MIN_RANDOM || !/[A-Z]/.test(text) || !/[a-z]/.test(text) || !/\d/.test(text)) return false;
|
|
4185
|
+
let letters = 0;
|
|
4186
|
+
for (const c of text) if (c >= "a" && c <= "z" || c >= "A" && c <= "Z") letters++;
|
|
4187
|
+
let wordy = 0;
|
|
4188
|
+
for (const run of text.matchAll(/[a-z]{6,}|[A-Z]{6,}/g)) wordy += run[0].length;
|
|
4189
|
+
return wordy * 5 <= letters && bitsPerChar(text) >= 4 && !base64OfText(text);
|
|
4190
|
+
}
|
|
4191
|
+
function isBasicCredential(encoded) {
|
|
4192
|
+
const text = encoded.replace(/=+$/, "");
|
|
4193
|
+
if (text.length % 4 === 1) return false;
|
|
4194
|
+
let decoded = "";
|
|
4195
|
+
let bits = 0;
|
|
4196
|
+
let acc = 0;
|
|
4197
|
+
for (const c of text) {
|
|
4198
|
+
const digit = BASE64_DIGITS.indexOf(c);
|
|
4199
|
+
if (digit < 0) return false;
|
|
4200
|
+
acc = (acc << 6 | digit) & 65535;
|
|
4201
|
+
bits += 6;
|
|
4202
|
+
if (bits < 8) continue;
|
|
4203
|
+
bits -= 8;
|
|
4204
|
+
decoded += String.fromCharCode(acc >> bits & 255);
|
|
4205
|
+
}
|
|
4206
|
+
return /^[\x20-\x7e]+:[\x20-\x7e]*$/.test(decoded);
|
|
4207
|
+
}
|
|
4208
|
+
function isSecretValue(value) {
|
|
4209
|
+
const text = value.trim();
|
|
4210
|
+
if (text.length < 10 || text.length > 4096) return false;
|
|
4211
|
+
if (WHOLE_SHAPES.some((re) => re.test(text))) return true;
|
|
4212
|
+
const basic = /^Basic\s+([A-Za-z0-9+/]{4,}={0,2})$/.exec(text);
|
|
4213
|
+
if (basic && isBasicCredential(basic[1])) return true;
|
|
4214
|
+
if (/^-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/.test(text)) return true;
|
|
4215
|
+
return /^[A-Za-z0-9+/_-]{40,}={0,2}$/.test(text) && looksRandom(text);
|
|
4216
|
+
}
|
|
4217
|
+
var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s"'<>`]+/gi;
|
|
4218
|
+
var URL_PARTS = /^(?:([a-z][a-z0-9+.-]*:)?\/\/([^/?#]*))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?$/i;
|
|
4219
|
+
function decodeSafe(text) {
|
|
4220
|
+
try {
|
|
4221
|
+
return decodeURIComponent(text);
|
|
4222
|
+
} catch {
|
|
4223
|
+
return text;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
function hider(count) {
|
|
4227
|
+
return () => {
|
|
4228
|
+
count.n++;
|
|
4229
|
+
return HIDDEN_VALUE;
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
function hideParams(params, count) {
|
|
4233
|
+
const hide = hider(count);
|
|
4234
|
+
return params.split(/([&;])/).map((part, i) => {
|
|
4235
|
+
const eq = part.indexOf("=");
|
|
4236
|
+
if (i % 2 === 1 || eq < 0) return part;
|
|
4237
|
+
const name = part.slice(0, eq);
|
|
4238
|
+
const value = part.slice(eq + 1);
|
|
4239
|
+
const decoded = decodeSafe(value);
|
|
4240
|
+
if (!value || decoded === HIDDEN_VALUE) return part;
|
|
4241
|
+
return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
|
|
4242
|
+
}).join("");
|
|
4243
|
+
}
|
|
4244
|
+
function hidePath(path15, count) {
|
|
4245
|
+
const hide = hider(count);
|
|
4246
|
+
return path15.split("/").map((segment) => {
|
|
4247
|
+
const [own = "", ...params] = segment.split(";");
|
|
4248
|
+
const decoded = decodeSafe(own);
|
|
4249
|
+
const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
|
|
4250
|
+
return params.length ? `${kept};${hideParams(params.join(";"), count)}` : kept;
|
|
4251
|
+
}).join("/");
|
|
4252
|
+
}
|
|
4253
|
+
function hideSecretsInUrl(url, count = { n: 0 }) {
|
|
4254
|
+
const parts = URL_PARTS.exec(url);
|
|
4255
|
+
if (!parts) return url;
|
|
4256
|
+
const [, scheme = "", authority, path15 = "", query = "", fragment = ""] = parts;
|
|
4257
|
+
let out = "";
|
|
4258
|
+
if (authority !== void 0) {
|
|
4259
|
+
const at = authority.lastIndexOf("@");
|
|
4260
|
+
const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
|
|
4261
|
+
out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
|
|
4262
|
+
}
|
|
4263
|
+
out += hidePath(path15, count);
|
|
4264
|
+
if (query) out += `?${hideParams(query.slice(1), count)}`;
|
|
4265
|
+
if (fragment) {
|
|
4266
|
+
const body = fragment.slice(1);
|
|
4267
|
+
const ask = body.indexOf("?");
|
|
4268
|
+
out += `#${ask >= 0 ? `${hidePath(body.slice(0, ask), count)}?${hideParams(body.slice(ask + 1), count)}` : body.includes("=") ? hideParams(body, count) : hidePath(body, count)}`;
|
|
4269
|
+
}
|
|
4270
|
+
return out;
|
|
4271
|
+
}
|
|
4272
|
+
var QUERY_PAIR = /([?&;#])([^=&#?;\s"'<>]{1,256})=([^&;#\s"'<>]*)/g;
|
|
4273
|
+
var URL_PASSWORD = /(\/\/[^/:@\s"'<>]{1,256}:)([^@/\s"'<>]{1,256})(@)/g;
|
|
4274
|
+
function hidePem(text, hide) {
|
|
4275
|
+
let out = "";
|
|
4276
|
+
let last = 0;
|
|
4277
|
+
let nextEnd = -2;
|
|
4278
|
+
PEM_BEGIN.lastIndex = 0;
|
|
4279
|
+
for (let begin = PEM_BEGIN.exec(text); begin; begin = PEM_BEGIN.exec(text)) {
|
|
4280
|
+
if (begin.index < last) continue;
|
|
4281
|
+
if (nextEnd !== -1 && nextEnd < begin.index) nextEnd = text.indexOf("-----END ", begin.index);
|
|
4282
|
+
const footer = nextEnd >= 0 ? /^-----END [A-Z0-9 ]{0,40}-----/.exec(text.slice(nextEnd, nextEnd + 60)) : null;
|
|
4283
|
+
const until = footer ? nextEnd + footer[0].length : begin.index + begin[0].length + (/^(?:\s*[A-Za-z0-9+/=]+)*/.exec(text.slice(begin.index + begin[0].length))?.[0].length ?? 0);
|
|
4284
|
+
out += `${text.slice(last, begin.index)}${hide()}`;
|
|
4285
|
+
last = until;
|
|
4286
|
+
PEM_BEGIN.lastIndex = until;
|
|
4287
|
+
}
|
|
4288
|
+
return out + text.slice(last);
|
|
4289
|
+
}
|
|
4290
|
+
function hideInText(text, count) {
|
|
4291
|
+
if (text.length < 4 || !/[A-Za-z0-9]/.test(text)) return text;
|
|
4292
|
+
const hide = hider(count);
|
|
4293
|
+
let out = hidePem(text, hide);
|
|
4294
|
+
out = out.replace(QUERY_PAIR, (all, sep9, name, value) => {
|
|
4295
|
+
const decoded = decodeSafe(value);
|
|
4296
|
+
if (!value || decoded === HIDDEN_VALUE) return all;
|
|
4297
|
+
return isSecretName(name) || isSecretValue(decoded) ? `${sep9}${name}=${hide()}` : all;
|
|
4298
|
+
});
|
|
4299
|
+
out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
|
|
4300
|
+
out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
|
|
4301
|
+
out = out.replace(COOKIE_LINE, (_all, before, pairs) => {
|
|
4302
|
+
const values = pairs.replace(
|
|
4303
|
+
/([^=;\s]{1,256}=)([^;]*)/g,
|
|
4304
|
+
(pair, name, value) => value.trim() && value.trim() !== HIDDEN_VALUE ? `${name}${hide()}` : pair
|
|
4305
|
+
);
|
|
4306
|
+
return `${before}${values}`;
|
|
4307
|
+
});
|
|
4308
|
+
out = out.replace(
|
|
4309
|
+
JSON_FIELD,
|
|
4310
|
+
(all, before, name, value) => isSecretName(name) && value !== HIDDEN_VALUE ? `${before}${hide()}"` : all
|
|
4311
|
+
);
|
|
4312
|
+
out = out.replace(BASIC, (all, encoded) => isBasicCredential(encoded) ? hide() : all);
|
|
4313
|
+
for (const shape of SECRET_SHAPES) out = out.replace(shape, () => hide());
|
|
4314
|
+
return out.replace(
|
|
4315
|
+
TOKEN_RUN,
|
|
4316
|
+
(run) => run.includes("/") && !/[+=]/.test(run) ? run.split("/").map((part) => looksRandom(part) ? hide() : part).join("/") : looksRandom(run) ? hide() : run
|
|
4317
|
+
);
|
|
4318
|
+
}
|
|
4319
|
+
function hideSecretsInText(text, count = { n: 0 }) {
|
|
4320
|
+
let out = "";
|
|
4321
|
+
let last = 0;
|
|
4322
|
+
for (const url of text.matchAll(URL_IN_TEXT)) {
|
|
4323
|
+
out += hideInText(text.slice(last, url.index), count) + hideSecretsInUrl(url[0], count);
|
|
4324
|
+
last = url.index + url[0].length;
|
|
4325
|
+
}
|
|
4326
|
+
return out + hideInText(text.slice(last), count);
|
|
4327
|
+
}
|
|
4069
4328
|
|
|
4070
4329
|
// src/internal/capture/inspect-on-failure.ts
|
|
4071
4330
|
function isCi(ci) {
|
|
@@ -4321,6 +4580,7 @@ var path7 = __toESM(require("path"));
|
|
|
4321
4580
|
// src/internal/support/reporter-version.ts
|
|
4322
4581
|
var fs5 = __toESM(require("fs"));
|
|
4323
4582
|
var path6 = __toESM(require("path"));
|
|
4583
|
+
var cachedVersion = null;
|
|
4324
4584
|
var CANDIDATE_OFFSETS = ["..", "../../.."];
|
|
4325
4585
|
function findOwnPackageJson(fromDir) {
|
|
4326
4586
|
for (const offset of CANDIDATE_OFFSETS) {
|
|
@@ -4335,6 +4595,11 @@ function findOwnPackageJson(fromDir) {
|
|
|
4335
4595
|
}
|
|
4336
4596
|
return void 0;
|
|
4337
4597
|
}
|
|
4598
|
+
function getReporterVersion() {
|
|
4599
|
+
if (cachedVersion) return cachedVersion;
|
|
4600
|
+
cachedVersion = findOwnPackageJson(__dirname)?.version ?? "unknown";
|
|
4601
|
+
return cachedVersion;
|
|
4602
|
+
}
|
|
4338
4603
|
|
|
4339
4604
|
// src/internal/capture/quiet-capture.ts
|
|
4340
4605
|
var BOXED_PREFIX_EXPORTS = [
|
|
@@ -4460,11 +4725,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
|
|
|
4460
4725
|
return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
4461
4726
|
}
|
|
4462
4727
|
var parsed = null;
|
|
4728
|
+
var sent = null;
|
|
4463
4729
|
function currentPauseSet() {
|
|
4730
|
+
if (sent) return sent;
|
|
4464
4731
|
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
4465
4732
|
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
4466
4733
|
return parsed.set;
|
|
4467
4734
|
}
|
|
4735
|
+
function replacePauseSet(places, cwd = process.cwd()) {
|
|
4736
|
+
if (!places) {
|
|
4737
|
+
sent = null;
|
|
4738
|
+
return currentPauseSet();
|
|
4739
|
+
}
|
|
4740
|
+
const lines = new Set(places.map((place) => placeLine(place, cwd)));
|
|
4741
|
+
sent = lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
4742
|
+
return sent;
|
|
4743
|
+
}
|
|
4744
|
+
function callSitePlace(callSite) {
|
|
4745
|
+
const m = /^(.+):(\d+):(\d+)$/.exec(callSite);
|
|
4746
|
+
if (!m || Number(m[2]) < 1) return null;
|
|
4747
|
+
const column = Number(m[3]);
|
|
4748
|
+
return column > 0 ? { file: m[1], line: Number(m[2]), column } : { file: m[1], line: Number(m[2]) };
|
|
4749
|
+
}
|
|
4750
|
+
function placeLine(place, cwd = process.cwd()) {
|
|
4751
|
+
return `${callSiteFile(place.file, cwd)}:${place.line}`;
|
|
4752
|
+
}
|
|
4468
4753
|
function pauseGateFromTestInfo(testInfo) {
|
|
4469
4754
|
const use = testInfo.project?.use ?? {};
|
|
4470
4755
|
const debug = testInfo._configInternal?.configCLIOverrides?.debug;
|
|
@@ -4472,7 +4757,7 @@ function pauseGateFromTestInfo(testInfo) {
|
|
|
4472
4757
|
}
|
|
4473
4758
|
function pauseSkipReason(gate) {
|
|
4474
4759
|
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
4475
|
-
if (gate.headless !== false)
|
|
4760
|
+
if (gate.headless !== false && !gate.channel)
|
|
4476
4761
|
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
4477
4762
|
if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
|
|
4478
4763
|
return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
|
|
@@ -4480,20 +4765,42 @@ function pauseSkipReason(gate) {
|
|
|
4480
4765
|
return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
|
|
4481
4766
|
return null;
|
|
4482
4767
|
}
|
|
4768
|
+
function pauseState(set, pause = false) {
|
|
4769
|
+
return { set, step: false, finished: false, pause, runTo: null };
|
|
4770
|
+
}
|
|
4771
|
+
function wantsStops(state) {
|
|
4772
|
+
return state.step || state.pause || state.runTo !== null || !state.finished && state.set.size > 0;
|
|
4773
|
+
}
|
|
4774
|
+
function atBreakpoint(state, callSite) {
|
|
4775
|
+
return !state.finished && state.set.matches(callSite);
|
|
4776
|
+
}
|
|
4483
4777
|
function atStop(state, callSite) {
|
|
4484
|
-
if (callSite
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4778
|
+
if (callSite) {
|
|
4779
|
+
const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && callSiteLine(callSite) === state.runTo ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
|
|
4780
|
+
if (reason) {
|
|
4781
|
+
stopShown(state);
|
|
4782
|
+
return { show: reason };
|
|
4488
4783
|
}
|
|
4489
|
-
if (state.set.matches(callSite)) return { show: "breakpoint" };
|
|
4490
4784
|
}
|
|
4491
4785
|
return { go: goOn(state) };
|
|
4492
4786
|
}
|
|
4787
|
+
function stopShown(state) {
|
|
4788
|
+
state.step = false;
|
|
4789
|
+
state.pause = false;
|
|
4790
|
+
state.runTo = null;
|
|
4791
|
+
}
|
|
4792
|
+
function askedOnce(state) {
|
|
4793
|
+
return { step: state.step, pause: state.pause, runTo: state.runTo };
|
|
4794
|
+
}
|
|
4795
|
+
function askAgain(state, asked) {
|
|
4796
|
+
state.step ||= asked.step;
|
|
4797
|
+
state.pause ||= asked.pause;
|
|
4798
|
+
state.runTo ??= asked.runTo;
|
|
4799
|
+
}
|
|
4493
4800
|
function goOn(state, answer) {
|
|
4494
4801
|
if (answer === "step") state.step = true;
|
|
4495
4802
|
else if (answer === "finish") state.finished = true;
|
|
4496
|
-
return
|
|
4803
|
+
return wantsStops(state) ? "next" : "resume";
|
|
4497
4804
|
}
|
|
4498
4805
|
function contextDebugger(context) {
|
|
4499
4806
|
const dbg = context.debugger;
|
|
@@ -4629,38 +4936,83 @@ function logPauseSkipOnce(reason) {
|
|
|
4629
4936
|
var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
|
|
4630
4937
|
var MAX_NAVIGATED = 10;
|
|
4631
4938
|
var HIGHLIGHT_MS = 2e3;
|
|
4632
|
-
|
|
4633
|
-
|
|
4939
|
+
var BarIntents = class {
|
|
4940
|
+
constructor() {
|
|
4941
|
+
this.take = null;
|
|
4942
|
+
this.queued = null;
|
|
4943
|
+
this.busy = false;
|
|
4944
|
+
}
|
|
4945
|
+
/** Hand the bar an intent; false while it runs one, or holds one already. */
|
|
4946
|
+
offer(intent) {
|
|
4947
|
+
if (this.busy || this.queued) return false;
|
|
4948
|
+
const take = this.take;
|
|
4949
|
+
this.take = null;
|
|
4950
|
+
if (take) take(intent);
|
|
4951
|
+
else this.queued = intent;
|
|
4952
|
+
return true;
|
|
4953
|
+
}
|
|
4954
|
+
/** The bar waits for its answer: settles with the next intent handed to it. */
|
|
4955
|
+
wait() {
|
|
4956
|
+
this.busy = false;
|
|
4957
|
+
const queued = this.queued;
|
|
4958
|
+
this.queued = null;
|
|
4959
|
+
if (queued) return Promise.resolve(queued);
|
|
4960
|
+
return new Promise((resolve5) => {
|
|
4961
|
+
this.take = resolve5;
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
/** The bar got its answer: it takes no intent until it waits again. */
|
|
4965
|
+
answered() {
|
|
4966
|
+
this.take = null;
|
|
4967
|
+
this.busy = true;
|
|
4968
|
+
}
|
|
4969
|
+
};
|
|
4970
|
+
var TAKE_DOWN_MS = 1e3;
|
|
4971
|
+
async function within(run, ms) {
|
|
4972
|
+
let timer;
|
|
4973
|
+
try {
|
|
4974
|
+
return await Promise.race([
|
|
4975
|
+
run,
|
|
4976
|
+
new Promise((resolve5) => timer = setTimeout(() => resolve5(void 0), ms))
|
|
4977
|
+
]);
|
|
4978
|
+
} finally {
|
|
4979
|
+
clearTimeout(timer);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
|
|
4983
|
+
let shown2 = null;
|
|
4634
4984
|
try {
|
|
4635
4985
|
const highlight = point.target?.highlight;
|
|
4636
4986
|
if (typeof highlight === "function") {
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
4640
|
-
new Promise((resolve5) => {
|
|
4641
|
-
timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
|
|
4642
|
-
})
|
|
4643
|
-
]);
|
|
4644
|
-
clearTimeout(timer);
|
|
4987
|
+
const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
|
|
4988
|
+
shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
|
|
4645
4989
|
}
|
|
4646
4990
|
const arg = {
|
|
4647
4991
|
place: pausePlace(point.location),
|
|
4648
4992
|
attempt: point.attempt,
|
|
4649
4993
|
action: point.call,
|
|
4650
4994
|
locator: null,
|
|
4651
|
-
record: !!onRecord
|
|
4995
|
+
record: !!onRecord,
|
|
4996
|
+
...point.failure ? { failure: true } : {}
|
|
4652
4997
|
};
|
|
4653
4998
|
console.log(
|
|
4654
|
-
`[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
|
|
4999
|
+
point.failure ? `[piwi] The test failed at ${callSiteLine(point.location)}: Resume, Pick a locator${onRecord ? " or Record here" : ""} in the browser (Esc resumes, and the test ends as it failed).` : `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
|
|
4655
5000
|
);
|
|
4656
5001
|
let navigated = 0;
|
|
4657
5002
|
for (; ; ) {
|
|
4658
5003
|
try {
|
|
4659
5004
|
if (ended?.aborted) return "resume";
|
|
4660
|
-
await page.evaluate(showPauseBar, arg);
|
|
4661
|
-
|
|
5005
|
+
await unlessEnded(page.evaluate(showPauseBar, arg), ended);
|
|
5006
|
+
const answered = page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }).then(() => null);
|
|
5007
|
+
const intent = await unlessEnded(intents ? Promise.race([answered, intents.wait()]) : answered, ended);
|
|
5008
|
+
intents?.answered();
|
|
5009
|
+
if (ended?.aborted) return "resume";
|
|
5010
|
+
const taken = await unlessEnded(page.evaluate(takePauseState), ended);
|
|
5011
|
+
if (ended?.aborted) return "resume";
|
|
5012
|
+
const choice = taken ?? intent ?? null;
|
|
5013
|
+
const raised = intent && choice === intent ? page.bringToFront().catch(() => void 0) : null;
|
|
5014
|
+
if (raised) await unlessEnded(raised, ended);
|
|
4662
5015
|
if (ended?.aborted) return "resume";
|
|
4663
|
-
const choice = await page.evaluate(takePauseState);
|
|
4664
5016
|
navigated = 0;
|
|
4665
5017
|
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
4666
5018
|
if (choice === "record" && onRecord) {
|
|
@@ -4670,26 +5022,285 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
|
|
|
4670
5022
|
);
|
|
4671
5023
|
continue;
|
|
4672
5024
|
}
|
|
5025
|
+
if ((choice === "assert" || choice === "assert-region") && onIntent) {
|
|
5026
|
+
await unlessEnded(
|
|
5027
|
+
onIntent(choice, ended).catch(() => void 0),
|
|
5028
|
+
ended
|
|
5029
|
+
);
|
|
5030
|
+
continue;
|
|
5031
|
+
}
|
|
4673
5032
|
if (choice !== "pick") continue;
|
|
4674
5033
|
const picked = await pickLocator(page, probe, null, ended).catch(() => null);
|
|
4675
5034
|
if (picked && !ended?.aborted) await onPick(picked);
|
|
4676
5035
|
} catch (error) {
|
|
4677
5036
|
if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
|
|
4678
|
-
await
|
|
5037
|
+
await unlessEnded(
|
|
5038
|
+
page.waitForLoadState("domcontentloaded").catch(() => void 0),
|
|
5039
|
+
ended
|
|
5040
|
+
);
|
|
4679
5041
|
}
|
|
4680
5042
|
}
|
|
4681
5043
|
} catch {
|
|
4682
5044
|
return "resume";
|
|
4683
5045
|
} finally {
|
|
5046
|
+
const takeDown = async () => {
|
|
5047
|
+
try {
|
|
5048
|
+
if (ended?.aborted) await page.evaluate(takePauseState);
|
|
5049
|
+
const dispose = shown2?.dispose;
|
|
5050
|
+
const hide = page.hideHighlight;
|
|
5051
|
+
if (typeof dispose === "function") await dispose.call(shown2);
|
|
5052
|
+
else if (typeof hide === "function") await hide.call(page);
|
|
5053
|
+
} catch {
|
|
5054
|
+
}
|
|
5055
|
+
};
|
|
5056
|
+
await within(takeDown(), TAKE_DOWN_MS);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
// src/internal/capture/chain-locator.ts
|
|
5061
|
+
function argValue(page, arg) {
|
|
5062
|
+
switch (arg.type) {
|
|
5063
|
+
case "string":
|
|
5064
|
+
case "number":
|
|
5065
|
+
case "boolean":
|
|
5066
|
+
return arg.value;
|
|
5067
|
+
case "regex":
|
|
5068
|
+
return new RegExp(arg.source, arg.flags);
|
|
5069
|
+
case "object":
|
|
5070
|
+
return Object.fromEntries(arg.entries.map(([key, value]) => [key, argValue(page, value)]));
|
|
5071
|
+
case "chain":
|
|
5072
|
+
return chainOn(page, arg.chain);
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
function chainOn(page, chain) {
|
|
5076
|
+
let current = page;
|
|
5077
|
+
for (const call of chain.calls) {
|
|
5078
|
+
if (typeof current[call.method] !== "function") {
|
|
5079
|
+
throw new Error(`${call.method}() cannot be called there.`);
|
|
5080
|
+
}
|
|
5081
|
+
current = callMethod(
|
|
5082
|
+
current,
|
|
5083
|
+
call.method,
|
|
5084
|
+
call.args.map((arg) => argValue(page, arg))
|
|
5085
|
+
);
|
|
5086
|
+
}
|
|
5087
|
+
if (!current || typeof current.count !== "function") {
|
|
5088
|
+
throw new Error("The chain does not end on a locator.");
|
|
5089
|
+
}
|
|
5090
|
+
return current;
|
|
5091
|
+
}
|
|
5092
|
+
function locatorOn(page, source) {
|
|
5093
|
+
const chain = tryParseLocatorChain(source);
|
|
5094
|
+
if (!chain?.calls.length) return null;
|
|
5095
|
+
try {
|
|
5096
|
+
return chainOn(page, chain);
|
|
5097
|
+
} catch {
|
|
5098
|
+
return null;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
// src/internal/capture/region-snapshot.ts
|
|
5103
|
+
function regionSnapshotWithoutSecrets(snapshot, secrets) {
|
|
5104
|
+
const hidden = new Set(secrets.filter(Boolean));
|
|
5105
|
+
const kept = [];
|
|
5106
|
+
for (const line of snapshot.split("\n")) {
|
|
5107
|
+
const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
|
|
5108
|
+
if (box && hidden.size && hidden.has(yamlScalar(box[2]))) {
|
|
5109
|
+
kept.push({ line: box[1], lostChild: false });
|
|
5110
|
+
continue;
|
|
5111
|
+
}
|
|
5112
|
+
const url = /^\s*- \/url: (.*)$/.exec(line);
|
|
5113
|
+
if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
|
|
5114
|
+
let parent = kept.length - 1;
|
|
5115
|
+
while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
|
|
5116
|
+
if (parent >= 0) kept[parent].lostChild = true;
|
|
5117
|
+
continue;
|
|
5118
|
+
}
|
|
5119
|
+
kept.push({ line, lostChild: false });
|
|
5120
|
+
}
|
|
5121
|
+
return kept.map(({ line, lostChild }, i) => {
|
|
5122
|
+
const next = kept[i + 1];
|
|
5123
|
+
const childless = !next || indentOf(next.line) <= indentOf(line);
|
|
5124
|
+
return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
|
|
5125
|
+
}).join("\n");
|
|
5126
|
+
}
|
|
5127
|
+
function indentOf(line) {
|
|
5128
|
+
return line.length - line.trimStart().length;
|
|
5129
|
+
}
|
|
5130
|
+
function yamlScalar(text) {
|
|
5131
|
+
const value = text.trim();
|
|
5132
|
+
if (value.startsWith('"')) {
|
|
4684
5133
|
try {
|
|
4685
|
-
|
|
4686
|
-
const dispose = shown?.dispose;
|
|
4687
|
-
const hide = page.hideHighlight;
|
|
4688
|
-
if (typeof dispose === "function") await dispose.call(shown);
|
|
4689
|
-
else if (typeof hide === "function") await hide.call(page);
|
|
5134
|
+
return String(JSON.parse(value));
|
|
4690
5135
|
} catch {
|
|
5136
|
+
return value;
|
|
4691
5137
|
}
|
|
4692
5138
|
}
|
|
5139
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
|
|
5140
|
+
return value;
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
// ../core/src/run-channel.ts
|
|
5144
|
+
var RUN_CHANNEL_ENV = "PIWI_RUN_CHANNEL";
|
|
5145
|
+
var RUN_HOLDS = ["failure"];
|
|
5146
|
+
var RUN_INTENTS = ["pick", "record", "assert", "assert-region"];
|
|
5147
|
+
var RUN_TAKES = [...RUN_INTENTS, "requests", "mock", "locator"];
|
|
5148
|
+
var RUN_INTENT_NAMES = {
|
|
5149
|
+
pick: "Pick a locator",
|
|
5150
|
+
record: "Record here",
|
|
5151
|
+
assert: "Assert",
|
|
5152
|
+
"assert-region": "Assert a region"
|
|
5153
|
+
};
|
|
5154
|
+
var MAX_KEPT_RESPONSES = 50;
|
|
5155
|
+
var MAX_KEPT_BODY = 256 * 1024;
|
|
5156
|
+
var MAX_REGION_SNAPSHOT = 1024 * 1024;
|
|
5157
|
+
var MAX_STOP_ERROR = 1e3;
|
|
5158
|
+
function isObject2(value) {
|
|
5159
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5160
|
+
}
|
|
5161
|
+
function readJson(line) {
|
|
5162
|
+
try {
|
|
5163
|
+
const value = JSON.parse(line);
|
|
5164
|
+
return isObject2(value) ? value : null;
|
|
5165
|
+
} catch {
|
|
5166
|
+
return null;
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
function isInt(value, min) {
|
|
5170
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min;
|
|
5171
|
+
}
|
|
5172
|
+
function filePath(value) {
|
|
5173
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
|
|
5174
|
+
}
|
|
5175
|
+
function readPlace(value) {
|
|
5176
|
+
if (!isObject2(value) || !filePath(value.file) || !isInt(value.line, 1)) return null;
|
|
5177
|
+
if (value.column === void 0) return { file: value.file, line: value.line };
|
|
5178
|
+
return isInt(value.column, 1) ? { file: value.file, line: value.line, column: value.column } : null;
|
|
5179
|
+
}
|
|
5180
|
+
function isIntent(value) {
|
|
5181
|
+
return RUN_INTENTS.includes(value);
|
|
5182
|
+
}
|
|
5183
|
+
function readHolds(value) {
|
|
5184
|
+
if (!Array.isArray(value)) return void 0;
|
|
5185
|
+
return value.length > 16 ? [] : RUN_HOLDS.filter((hold) => value.includes(hold));
|
|
5186
|
+
}
|
|
5187
|
+
function isLocatorText(value) {
|
|
5188
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1e3 && !/[\r\n\u2028\u2029]/.test(value);
|
|
5189
|
+
}
|
|
5190
|
+
var MAX_PLACES = 1e4;
|
|
5191
|
+
function parseHubToRun(line) {
|
|
5192
|
+
const value = readJson(line);
|
|
5193
|
+
if (!value) return null;
|
|
5194
|
+
const unknown = (what) => ({ type: "unknown", what: what.slice(0, 64) });
|
|
5195
|
+
switch (value.type) {
|
|
5196
|
+
case "attached": {
|
|
5197
|
+
const holds = readHolds(value.holds);
|
|
5198
|
+
return holds ? { type: "attached", holds } : null;
|
|
5199
|
+
}
|
|
5200
|
+
case "breakpoints": {
|
|
5201
|
+
if (!Array.isArray(value.places) || value.places.length > MAX_PLACES) return null;
|
|
5202
|
+
const places = value.places.map(readPlace);
|
|
5203
|
+
return places.every((p) => p !== null) ? { type: "breakpoints", places } : null;
|
|
5204
|
+
}
|
|
5205
|
+
case "control": {
|
|
5206
|
+
const { action, stop } = value;
|
|
5207
|
+
if (action === "pause") return { type: "control", action };
|
|
5208
|
+
if (action === "step" || action === "resume" || action === "finish") {
|
|
5209
|
+
return isInt(stop, 1) ? { type: "control", action, stop } : null;
|
|
5210
|
+
}
|
|
5211
|
+
return typeof action === "string" ? unknown(`control ${action}`) : null;
|
|
5212
|
+
}
|
|
5213
|
+
case "run-to": {
|
|
5214
|
+
const place = readPlace(value.place);
|
|
5215
|
+
return isInt(value.stop, 1) && place ? { type: "run-to", stop: value.stop, place } : null;
|
|
5216
|
+
}
|
|
5217
|
+
case "request": {
|
|
5218
|
+
const { request, what, stop } = value;
|
|
5219
|
+
if (!isInt(request, 1)) return null;
|
|
5220
|
+
if (what === "image") return { type: "request", request, what: "image" };
|
|
5221
|
+
if (typeof what !== "string") return null;
|
|
5222
|
+
if (!isIntent(what) && what !== "requests" && what !== "mock" && what !== "locator") {
|
|
5223
|
+
return unknown(`request ${what}`);
|
|
5224
|
+
}
|
|
5225
|
+
if (!isInt(stop, 1)) return null;
|
|
5226
|
+
if (isIntent(what) || what === "requests") return { type: "request", request, what, stop };
|
|
5227
|
+
if (what === "mock") return isInt(value.id, 1) ? { type: "request", request, what, stop, id: value.id } : null;
|
|
5228
|
+
const { locator } = value;
|
|
5229
|
+
return locator === null || isLocatorText(locator) ? { type: "request", request, what, stop, locator } : null;
|
|
5230
|
+
}
|
|
5231
|
+
default:
|
|
5232
|
+
return typeof value.type === "string" && value.type ? unknown(value.type) : null;
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
// src/internal/capture/kept-responses.ts
|
|
5237
|
+
var keeping = null;
|
|
5238
|
+
var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
|
|
5239
|
+
function mimeOf(contentType) {
|
|
5240
|
+
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
5241
|
+
}
|
|
5242
|
+
function textOf2(body) {
|
|
5243
|
+
try {
|
|
5244
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
5245
|
+
return text.includes("\0") ? null : text;
|
|
5246
|
+
} catch {
|
|
5247
|
+
return null;
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
function startKeeping(now = Date.now()) {
|
|
5251
|
+
keeping = { startedAt: now, kept: [], last: 0 };
|
|
5252
|
+
}
|
|
5253
|
+
function stopKeeping() {
|
|
5254
|
+
keeping = null;
|
|
5255
|
+
}
|
|
5256
|
+
async function keepResponse(request, response) {
|
|
5257
|
+
const into = keeping;
|
|
5258
|
+
if (!into || !response) return;
|
|
5259
|
+
try {
|
|
5260
|
+
const kind = request.resourceType();
|
|
5261
|
+
if (kind !== "fetch" && kind !== "xhr") return;
|
|
5262
|
+
const url = hideSecretsInUrl(request.url());
|
|
5263
|
+
const method = request.method().toUpperCase();
|
|
5264
|
+
if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
|
|
5265
|
+
const headers = response.headers();
|
|
5266
|
+
const type = mimeOf(headers["content-type"]);
|
|
5267
|
+
if (type && !TEXT_TYPE.test(type)) return;
|
|
5268
|
+
const status = response.status();
|
|
5269
|
+
if (status < 100 || status > 599) return;
|
|
5270
|
+
const time = Math.max(0, Math.round(Date.now() - into.startedAt));
|
|
5271
|
+
const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
|
|
5272
|
+
if (size !== null && size > MAX_KEPT_BODY) return;
|
|
5273
|
+
into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
|
|
5274
|
+
if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
|
|
5275
|
+
} catch {
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
function keptRequests() {
|
|
5279
|
+
return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
|
|
5280
|
+
id,
|
|
5281
|
+
method,
|
|
5282
|
+
url,
|
|
5283
|
+
status,
|
|
5284
|
+
type,
|
|
5285
|
+
size,
|
|
5286
|
+
time
|
|
5287
|
+
}));
|
|
5288
|
+
}
|
|
5289
|
+
async function keptResponse(id) {
|
|
5290
|
+
const found = keeping?.kept.find((kept) => kept.id === id);
|
|
5291
|
+
if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
|
|
5292
|
+
let body;
|
|
5293
|
+
try {
|
|
5294
|
+
body = await internalCall(found.response, () => found.response.body());
|
|
5295
|
+
} catch {
|
|
5296
|
+
return {
|
|
5297
|
+
refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
|
|
5301
|
+
const text = textOf2(body);
|
|
5302
|
+
if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
|
|
5303
|
+
return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
|
|
4693
5304
|
}
|
|
4694
5305
|
|
|
4695
5306
|
// src/internal/capture/record-handoff.ts
|
|
@@ -4710,7 +5321,7 @@ function parseRecordChannel(value) {
|
|
|
4710
5321
|
const path15 = text.slice(0, hash);
|
|
4711
5322
|
return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
|
|
4712
5323
|
}
|
|
4713
|
-
function
|
|
5324
|
+
function isObject3(value) {
|
|
4714
5325
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4715
5326
|
}
|
|
4716
5327
|
function parseHandoffToTest(line) {
|
|
@@ -4720,7 +5331,7 @@ function parseHandoffToTest(line) {
|
|
|
4720
5331
|
} catch {
|
|
4721
5332
|
return null;
|
|
4722
5333
|
}
|
|
4723
|
-
if (!
|
|
5334
|
+
if (!isObject3(value)) return null;
|
|
4724
5335
|
switch (value.type) {
|
|
4725
5336
|
case "refused":
|
|
4726
5337
|
return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
|
|
@@ -4783,8 +5394,8 @@ async function recordFromHere(options) {
|
|
|
4783
5394
|
const use = testInfo.project?.use ?? {};
|
|
4784
5395
|
const context = page.context();
|
|
4785
5396
|
return new Promise((resolve5) => {
|
|
4786
|
-
const
|
|
4787
|
-
|
|
5397
|
+
const socket2 = net.connect(channel.path);
|
|
5398
|
+
socket2.setEncoding("utf8");
|
|
4788
5399
|
const answers = /* @__PURE__ */ new Map();
|
|
4789
5400
|
let calls = 0;
|
|
4790
5401
|
let attached = false;
|
|
@@ -4792,7 +5403,7 @@ async function recordFromHere(options) {
|
|
|
4792
5403
|
let settled = false;
|
|
4793
5404
|
let initScript;
|
|
4794
5405
|
const send = (message) => {
|
|
4795
|
-
if (!
|
|
5406
|
+
if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
|
|
4796
5407
|
`);
|
|
4797
5408
|
};
|
|
4798
5409
|
const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
|
|
@@ -4842,7 +5453,7 @@ async function recordFromHere(options) {
|
|
|
4842
5453
|
page.off("close", onGone);
|
|
4843
5454
|
page.off("crash", onGone);
|
|
4844
5455
|
ended?.removeEventListener("abort", onEnded);
|
|
4845
|
-
|
|
5456
|
+
socket2.end();
|
|
4846
5457
|
resolve5(recording ? null : said);
|
|
4847
5458
|
};
|
|
4848
5459
|
const relayed = (source, payload) => new Promise((answer) => {
|
|
@@ -4882,7 +5493,7 @@ async function recordFromHere(options) {
|
|
|
4882
5493
|
context.on("page", onPage);
|
|
4883
5494
|
send({ type: "loaded" });
|
|
4884
5495
|
};
|
|
4885
|
-
|
|
5496
|
+
socket2.on("connect", () => {
|
|
4886
5497
|
attached = true;
|
|
4887
5498
|
send({
|
|
4888
5499
|
type: "attach",
|
|
@@ -4891,10 +5502,11 @@ async function recordFromHere(options) {
|
|
|
4891
5502
|
line: place.line,
|
|
4892
5503
|
reason: options.reason,
|
|
4893
5504
|
testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
|
|
4894
|
-
baseURL: typeof use.baseURL === "string" ? use.baseURL : null
|
|
5505
|
+
baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
|
|
5506
|
+
...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
|
|
4895
5507
|
});
|
|
4896
5508
|
});
|
|
4897
|
-
|
|
5509
|
+
socket2.on(
|
|
4898
5510
|
"data",
|
|
4899
5511
|
lineReader(
|
|
4900
5512
|
(line) => {
|
|
@@ -4927,11 +5539,255 @@ async function recordFromHere(options) {
|
|
|
4927
5539
|
() => finish("the editor sent a message too long to read.")
|
|
4928
5540
|
)
|
|
4929
5541
|
);
|
|
4930
|
-
|
|
4931
|
-
|
|
5542
|
+
socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
|
|
5543
|
+
socket2.on("close", () => finish("the editor closed the recording."));
|
|
4932
5544
|
});
|
|
4933
5545
|
}
|
|
4934
5546
|
|
|
5547
|
+
// src/internal/capture/run-channel.ts
|
|
5548
|
+
var net2 = __toESM(require("net"));
|
|
5549
|
+
var CONNECT_MS = 2e3;
|
|
5550
|
+
var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
|
|
5551
|
+
var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
|
|
5552
|
+
var WENT_ON = "The test went on.";
|
|
5553
|
+
var socket = null;
|
|
5554
|
+
var control = null;
|
|
5555
|
+
var shown = /* @__PURE__ */ new Set();
|
|
5556
|
+
var stops = 0;
|
|
5557
|
+
var held = null;
|
|
5558
|
+
var outline = null;
|
|
5559
|
+
var counting = Promise.resolve();
|
|
5560
|
+
async function openRunChannel(worker, handlers, env = process.env) {
|
|
5561
|
+
const channel = parseRecordChannel(env[RUN_CHANNEL_ENV]);
|
|
5562
|
+
const ref = env[PIWI_ORIGIN_ENV.ref];
|
|
5563
|
+
if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
|
|
5564
|
+
const opening = net2.connect(channel.path);
|
|
5565
|
+
opening.setEncoding("utf8");
|
|
5566
|
+
const opened = await new Promise((resolve5) => {
|
|
5567
|
+
const timer = setTimeout(() => resolve5(false), CONNECT_MS);
|
|
5568
|
+
opening.once("connect", () => {
|
|
5569
|
+
clearTimeout(timer);
|
|
5570
|
+
resolve5(true);
|
|
5571
|
+
});
|
|
5572
|
+
opening.once("error", () => {
|
|
5573
|
+
clearTimeout(timer);
|
|
5574
|
+
resolve5(false);
|
|
5575
|
+
});
|
|
5576
|
+
});
|
|
5577
|
+
if (!opened || opening.destroyed) {
|
|
5578
|
+
opening.destroy();
|
|
5579
|
+
return false;
|
|
5580
|
+
}
|
|
5581
|
+
socket = opening;
|
|
5582
|
+
control = handlers;
|
|
5583
|
+
shown = /* @__PURE__ */ new Set();
|
|
5584
|
+
opening.unref();
|
|
5585
|
+
opening.on(
|
|
5586
|
+
"data",
|
|
5587
|
+
lineReader(
|
|
5588
|
+
(line) => onMessage(parseHubToRun(line)),
|
|
5589
|
+
() => opening.destroy()
|
|
5590
|
+
)
|
|
5591
|
+
);
|
|
5592
|
+
opening.on("error", () => opening.destroy());
|
|
5593
|
+
opening.on("close", () => {
|
|
5594
|
+
if (socket === opening) socket = null;
|
|
5595
|
+
if (!held || held.bar) return;
|
|
5596
|
+
console.log("[piwi] The connection to the editor closed: the test goes on.");
|
|
5597
|
+
held.answer("resume");
|
|
5598
|
+
});
|
|
5599
|
+
tellHub({
|
|
5600
|
+
type: "run-attach",
|
|
5601
|
+
token: channel.token,
|
|
5602
|
+
ref,
|
|
5603
|
+
cwd: process.cwd(),
|
|
5604
|
+
workerIndex: worker.workerIndex,
|
|
5605
|
+
parallelIndex: worker.parallelIndex,
|
|
5606
|
+
pid: process.pid,
|
|
5607
|
+
playwright: worker.config.version,
|
|
5608
|
+
reporter: getReporterVersion(),
|
|
5609
|
+
takes: [...RUN_TAKES],
|
|
5610
|
+
holds: [...RUN_HOLDS]
|
|
5611
|
+
});
|
|
5612
|
+
return true;
|
|
5613
|
+
}
|
|
5614
|
+
function editorShows(what) {
|
|
5615
|
+
return runChannelOpen() && shown.has(what);
|
|
5616
|
+
}
|
|
5617
|
+
var HIDDEN_ENV = [RUN_CHANNEL_ENV, PIWI_LOCAL_DEBUG_ENV.editorSend];
|
|
5618
|
+
var hiddenEditorSend;
|
|
5619
|
+
function hideRunChannel(env = process.env) {
|
|
5620
|
+
const hidden = HIDDEN_ENV.flatMap((name) => {
|
|
5621
|
+
const value = env[name];
|
|
5622
|
+
return value === void 0 ? [] : [{ name, value }];
|
|
5623
|
+
});
|
|
5624
|
+
for (const { name } of hidden) delete env[name];
|
|
5625
|
+
hiddenEditorSend = hidden.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
|
|
5626
|
+
return () => {
|
|
5627
|
+
for (const { name, value } of hidden) env[name] = value;
|
|
5628
|
+
hiddenEditorSend = void 0;
|
|
5629
|
+
};
|
|
5630
|
+
}
|
|
5631
|
+
function editorSendAddress(env = process.env) {
|
|
5632
|
+
return env[PIWI_LOCAL_DEBUG_ENV.editorSend] ?? hiddenEditorSend;
|
|
5633
|
+
}
|
|
5634
|
+
function closeRunChannel() {
|
|
5635
|
+
socket?.end();
|
|
5636
|
+
socket = null;
|
|
5637
|
+
control = null;
|
|
5638
|
+
}
|
|
5639
|
+
function runChannelOpen() {
|
|
5640
|
+
return !!socket && !socket.destroyed;
|
|
5641
|
+
}
|
|
5642
|
+
function tellHub(message) {
|
|
5643
|
+
if (socket && !socket.destroyed) socket.write(`${JSON.stringify(message)}
|
|
5644
|
+
`);
|
|
5645
|
+
}
|
|
5646
|
+
function holdStop(stop, onAnswer) {
|
|
5647
|
+
const id = ++stops;
|
|
5648
|
+
let answered = false;
|
|
5649
|
+
const holding = {
|
|
5650
|
+
stop: id,
|
|
5651
|
+
page: stop.page,
|
|
5652
|
+
bar: stop.bar,
|
|
5653
|
+
answer: (choice) => {
|
|
5654
|
+
if (answered) return;
|
|
5655
|
+
answered = true;
|
|
5656
|
+
onAnswer(choice);
|
|
5657
|
+
},
|
|
5658
|
+
intent: (what) => answered ? { ok: false, message: WENT_ON } : stop.intent?.(what) ?? {
|
|
5659
|
+
ok: false,
|
|
5660
|
+
message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
|
|
5661
|
+
}
|
|
5662
|
+
};
|
|
5663
|
+
held = holding;
|
|
5664
|
+
tellHub({
|
|
5665
|
+
type: "stopped",
|
|
5666
|
+
stop: id,
|
|
5667
|
+
reason: stop.reason,
|
|
5668
|
+
title: stop.title,
|
|
5669
|
+
place: stop.place,
|
|
5670
|
+
locator: stop.locator,
|
|
5671
|
+
bar: stop.bar,
|
|
5672
|
+
...stop.error ? { error: stop.error } : {}
|
|
5673
|
+
});
|
|
5674
|
+
if (!runChannelOpen() && !stop.bar) holding.answer("resume");
|
|
5675
|
+
return {
|
|
5676
|
+
stop: id,
|
|
5677
|
+
release: async () => {
|
|
5678
|
+
answered = true;
|
|
5679
|
+
if (held === holding) held = null;
|
|
5680
|
+
await counting;
|
|
5681
|
+
if (outline?.stop === id) await removeOutline();
|
|
5682
|
+
tellHub({ type: "running", stop: id });
|
|
5683
|
+
}
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
async function removeOutline() {
|
|
5687
|
+
const shown2 = outline;
|
|
5688
|
+
outline = null;
|
|
5689
|
+
await shown2?.remove().catch(() => void 0);
|
|
5690
|
+
}
|
|
5691
|
+
async function countLocator(holding, source) {
|
|
5692
|
+
await removeOutline();
|
|
5693
|
+
if (source === null) return { ok: true, message: "The outline is gone.", count: null };
|
|
5694
|
+
const page = holding.page;
|
|
5695
|
+
if (!page || page.isClosed()) return { ok: false, message: "This stop has no page to count on.", count: null };
|
|
5696
|
+
const locator = locatorOn(page, source);
|
|
5697
|
+
if (!locator) return { ok: false, message: `${source} is not a locator Piwi can build.`, count: null };
|
|
5698
|
+
try {
|
|
5699
|
+
const count = await internalCall(page, () => locator.count());
|
|
5700
|
+
if (holding.bar && count > 0 && held === holding) {
|
|
5701
|
+
const shown2 = await internalCall(page, () => locator.highlight({ style: OUTLINE_STYLE }));
|
|
5702
|
+
const dispose = shown2?.dispose;
|
|
5703
|
+
outline = {
|
|
5704
|
+
stop: holding.stop,
|
|
5705
|
+
remove: () => internalCall(page, () => typeof dispose === "function" ? dispose.call(shown2) : page.hideHighlight())
|
|
5706
|
+
};
|
|
5707
|
+
}
|
|
5708
|
+
return {
|
|
5709
|
+
ok: true,
|
|
5710
|
+
message: count === 0 ? "No element matches." : count === 1 ? "1 element matches." : `${count} elements match.`,
|
|
5711
|
+
count
|
|
5712
|
+
};
|
|
5713
|
+
} catch (error) {
|
|
5714
|
+
return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
function onMessage(message) {
|
|
5718
|
+
if (!message) return;
|
|
5719
|
+
switch (message.type) {
|
|
5720
|
+
case "attached":
|
|
5721
|
+
shown = new Set(message.holds);
|
|
5722
|
+
return;
|
|
5723
|
+
case "breakpoints":
|
|
5724
|
+
control?.breakpoints(message.places);
|
|
5725
|
+
return;
|
|
5726
|
+
case "control":
|
|
5727
|
+
if (message.action === "pause") {
|
|
5728
|
+
if (!held) control?.pause();
|
|
5729
|
+
} else if (held?.stop === message.stop) held.answer(message.action);
|
|
5730
|
+
return;
|
|
5731
|
+
case "run-to":
|
|
5732
|
+
if (held?.stop !== message.stop) return;
|
|
5733
|
+
control?.runTo(message.place);
|
|
5734
|
+
held.answer("resume");
|
|
5735
|
+
return;
|
|
5736
|
+
case "request": {
|
|
5737
|
+
const { request } = message;
|
|
5738
|
+
const at = message.what !== "image" && held?.stop === message.stop ? held : null;
|
|
5739
|
+
switch (message.what) {
|
|
5740
|
+
case "image":
|
|
5741
|
+
void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request, what: "image", value }));
|
|
5742
|
+
return;
|
|
5743
|
+
case "requests": {
|
|
5744
|
+
const requests = at ? keptRequests() : [];
|
|
5745
|
+
const said = !at ? WENT_ON : requests.length ? `The test kept ${requests.length === 1 ? "1 response" : `${requests.length} responses`}.` : "The test kept no response yet: Piwi keeps the text answers of its fetch and XHR requests.";
|
|
5746
|
+
tellHub({ type: "answer", request, what: "requests", value: { ok: !!at, message: said, requests } });
|
|
5747
|
+
return;
|
|
5748
|
+
}
|
|
5749
|
+
case "mock": {
|
|
5750
|
+
void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
|
|
5751
|
+
const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
|
|
5752
|
+
tellHub({ type: "answer", request, what: "mock", value });
|
|
5753
|
+
});
|
|
5754
|
+
return;
|
|
5755
|
+
}
|
|
5756
|
+
case "locator": {
|
|
5757
|
+
const counted = at ? counting.then(
|
|
5758
|
+
() => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
|
|
5759
|
+
) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
|
|
5760
|
+
counting = counted.catch(() => void 0);
|
|
5761
|
+
void counted.then((value) => tellHub({ type: "answer", request, what: "locator", value }));
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
default: {
|
|
5765
|
+
const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
|
|
5766
|
+
tellHub({ type: "answer", request, what: message.what, value });
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
case "unknown":
|
|
5772
|
+
tellHub({
|
|
5773
|
+
type: "notice",
|
|
5774
|
+
message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
|
|
5775
|
+
});
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
async function imageOf(page) {
|
|
5779
|
+
if (!page || page.isClosed()) return null;
|
|
5780
|
+
try {
|
|
5781
|
+
const shot = await internalCall(
|
|
5782
|
+
page,
|
|
5783
|
+
() => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN, timeout: 5e3 })
|
|
5784
|
+
);
|
|
5785
|
+
return { mime: "image/jpeg", data: shot.toString("base64") };
|
|
5786
|
+
} catch {
|
|
5787
|
+
return null;
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
|
|
4935
5791
|
// src/internal/collect/coverage-settings.ts
|
|
4936
5792
|
var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
|
|
4937
5793
|
function isServerGranularity(value) {
|
|
@@ -4953,7 +5809,7 @@ function browserFunctionCoverage(files) {
|
|
|
4953
5809
|
files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
|
|
4954
5810
|
});
|
|
4955
5811
|
}
|
|
4956
|
-
function newKnownFunctions(known,
|
|
5812
|
+
function newKnownFunctions(known, sent2) {
|
|
4957
5813
|
const parts = [];
|
|
4958
5814
|
let files = [];
|
|
4959
5815
|
let budget = MAX_COVERAGE_RANGES;
|
|
@@ -4965,10 +5821,10 @@ function newKnownFunctions(known, sent) {
|
|
|
4965
5821
|
for (const [raw, ranges] of known) {
|
|
4966
5822
|
const path15 = normalizeCoveragePath(raw);
|
|
4967
5823
|
if (path15 === null) continue;
|
|
4968
|
-
let named =
|
|
5824
|
+
let named = sent2.get(path15);
|
|
4969
5825
|
let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
|
|
4970
5826
|
if (fresh.length === 0) continue;
|
|
4971
|
-
if (!named)
|
|
5827
|
+
if (!named) sent2.set(path15, named = /* @__PURE__ */ new Set());
|
|
4972
5828
|
for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
|
|
4973
5829
|
while (fresh.length > 0) {
|
|
4974
5830
|
if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
|
|
@@ -4991,15 +5847,15 @@ var os3 = __toESM(require("os"));
|
|
|
4991
5847
|
var fs7 = __toESM(require("fs"));
|
|
4992
5848
|
|
|
4993
5849
|
// src/internal/support/instance-id.ts
|
|
4994
|
-
var
|
|
5850
|
+
var crypto2 = __toESM(require("crypto"));
|
|
4995
5851
|
var os2 = __toESM(require("os"));
|
|
4996
5852
|
function hashForProject(projectName) {
|
|
4997
|
-
return
|
|
5853
|
+
return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
4998
5854
|
}
|
|
4999
5855
|
|
|
5000
5856
|
// src/internal/support/aria-sampling.ts
|
|
5001
|
-
function ariaSampleIdentity(
|
|
5002
|
-
return `${
|
|
5857
|
+
function ariaSampleIdentity(filePath2, title) {
|
|
5858
|
+
return `${filePath2}\0${title}`;
|
|
5003
5859
|
}
|
|
5004
5860
|
function getAriaSampleFilePath(projectName) {
|
|
5005
5861
|
return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
@@ -5031,7 +5887,7 @@ function isDueForAriaSample(testInfo) {
|
|
|
5031
5887
|
}
|
|
5032
5888
|
|
|
5033
5889
|
// src/internal/capture/code-reach.ts
|
|
5034
|
-
var
|
|
5890
|
+
var crypto3 = __toESM(require("crypto"));
|
|
5035
5891
|
var fs8 = __toESM(require("fs"));
|
|
5036
5892
|
var path11 = __toESM(require("path"));
|
|
5037
5893
|
var import_node_child_process = require("child_process");
|
|
@@ -5119,9 +5975,9 @@ function resolveSourcePath(source, roots) {
|
|
|
5119
5975
|
const normalized = normalizeSourcePath(source);
|
|
5120
5976
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
5121
5977
|
if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
|
|
5122
|
-
const
|
|
5978
|
+
const relative8 = normalized.replace(/^\/+/, "");
|
|
5123
5979
|
for (const root of roots) {
|
|
5124
|
-
const candidate = path11.resolve(root,
|
|
5980
|
+
const candidate = path11.resolve(root, relative8);
|
|
5125
5981
|
if (isFile(candidate)) return candidate;
|
|
5126
5982
|
}
|
|
5127
5983
|
return null;
|
|
@@ -5156,7 +6012,7 @@ function relativeToRepo(file, repoRoot) {
|
|
|
5156
6012
|
return path11.relative(repoRoot, file).split(path11.sep).join("/");
|
|
5157
6013
|
}
|
|
5158
6014
|
function scriptVersion(entry) {
|
|
5159
|
-
return entry.hash ??
|
|
6015
|
+
return entry.hash ?? crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
5160
6016
|
}
|
|
5161
6017
|
async function loadMap(entry, fetchMap) {
|
|
5162
6018
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -5199,7 +6055,7 @@ async function resolveScript(entry, roots, bases, fetchMap) {
|
|
|
5199
6055
|
if (text) scriptCache.set(entry.url, version, resolution);
|
|
5200
6056
|
return resolution;
|
|
5201
6057
|
}
|
|
5202
|
-
function
|
|
6058
|
+
function textOf3(entry, resolution) {
|
|
5203
6059
|
return resolution.text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
|
|
5204
6060
|
}
|
|
5205
6061
|
async function resolveCodeReach(entries, roots, fetchMap, pageUrl) {
|
|
@@ -5245,7 +6101,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5245
6101
|
};
|
|
5246
6102
|
if (resolution.kind === "file") {
|
|
5247
6103
|
if (!resolution.file) continue;
|
|
5248
|
-
const text =
|
|
6104
|
+
const text = textOf3(covered, resolution);
|
|
5249
6105
|
if (resolution.lookup) {
|
|
5250
6106
|
const reached = reachedFunctionLines(covered, resolution.lookup, text);
|
|
5251
6107
|
if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
|
|
@@ -5253,7 +6109,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5253
6109
|
add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
|
|
5254
6110
|
}
|
|
5255
6111
|
} else if (resolution.kind === "map") {
|
|
5256
|
-
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup,
|
|
6112
|
+
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf3(covered, resolution))) {
|
|
5257
6113
|
const file = mapFile(resolution, index);
|
|
5258
6114
|
if (file) add2(file, functions);
|
|
5259
6115
|
}
|
|
@@ -5263,7 +6119,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5263
6119
|
let listed = knownCache.get(covered.url, version);
|
|
5264
6120
|
if (!listed || listed.count < covered.functions.length) {
|
|
5265
6121
|
const byFile = /* @__PURE__ */ new Map();
|
|
5266
|
-
const text =
|
|
6122
|
+
const text = textOf3(covered, resolution);
|
|
5267
6123
|
if (resolution.kind === "file" && resolution.file) {
|
|
5268
6124
|
const lines = knownFunctionLines(covered, resolution.lookup, text);
|
|
5269
6125
|
const all = resolution.lookup ? [...lines.values()].flat() : lines.get(-1) ?? [];
|
|
@@ -5562,7 +6418,7 @@ async function stopPageCoverage(page) {
|
|
|
5562
6418
|
}
|
|
5563
6419
|
|
|
5564
6420
|
// src/internal/capture/css-coverage.ts
|
|
5565
|
-
var
|
|
6421
|
+
var crypto4 = __toESM(require("crypto"));
|
|
5566
6422
|
var fs9 = __toESM(require("fs"));
|
|
5567
6423
|
|
|
5568
6424
|
// ../core/src/css-rules.ts
|
|
@@ -5777,7 +6633,7 @@ async function resolveSheet(sheet, text, dirs, fetchMap) {
|
|
|
5777
6633
|
return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
|
|
5778
6634
|
}
|
|
5779
6635
|
function cachedResolution(sheet, text, dirs, fetchMap) {
|
|
5780
|
-
const version =
|
|
6636
|
+
const version = crypto4.createHash("sha1").update(text).digest("hex");
|
|
5781
6637
|
const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
|
|
5782
6638
|
" "
|
|
5783
6639
|
);
|
|
@@ -5823,7 +6679,9 @@ async function resolveCssCoverage(sheets, roots, fetchMap) {
|
|
|
5823
6679
|
}
|
|
5824
6680
|
|
|
5825
6681
|
// src/internal/capture/resource-ledger.ts
|
|
6682
|
+
var childProcess = __toESM(require("child_process"));
|
|
5826
6683
|
var fs11 = __toESM(require("fs"));
|
|
6684
|
+
var net3 = __toESM(require("net"));
|
|
5827
6685
|
var path13 = __toESM(require("path"));
|
|
5828
6686
|
var import_node_module2 = require("module");
|
|
5829
6687
|
|
|
@@ -6003,6 +6861,8 @@ var WorkerMetrics = class {
|
|
|
6003
6861
|
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
6004
6862
|
this.proc = proc;
|
|
6005
6863
|
this.pid = pid;
|
|
6864
|
+
/** The lowest heap read right after a full garbage collection in the current window. */
|
|
6865
|
+
this.heapAfterGc = null;
|
|
6006
6866
|
this.loop = null;
|
|
6007
6867
|
this.usage = null;
|
|
6008
6868
|
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
@@ -6017,6 +6877,28 @@ var WorkerMetrics = class {
|
|
|
6017
6877
|
histogram = null;
|
|
6018
6878
|
}
|
|
6019
6879
|
this.histogram = histogram;
|
|
6880
|
+
this.gcObserver = this.observeFullCollections();
|
|
6881
|
+
}
|
|
6882
|
+
/**
|
|
6883
|
+
* Read the heap after each full (major) garbage collection V8 runs on its
|
|
6884
|
+
* own: nothing is forced, so a test that triggers none reads null.
|
|
6885
|
+
*/
|
|
6886
|
+
observeFullCollections() {
|
|
6887
|
+
try {
|
|
6888
|
+
const observer = new import_node_perf_hooks.PerformanceObserver((list) => {
|
|
6889
|
+
for (const entry of list.getEntries()) {
|
|
6890
|
+
const detail = entry.detail;
|
|
6891
|
+
const kind = detail?.kind ?? entry.kind;
|
|
6892
|
+
if (kind !== import_node_perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR) continue;
|
|
6893
|
+
const heap = process.memoryUsage().heapUsed;
|
|
6894
|
+
this.heapAfterGc = Math.min(this.heapAfterGc ?? Infinity, heap);
|
|
6895
|
+
}
|
|
6896
|
+
});
|
|
6897
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
6898
|
+
return observer;
|
|
6899
|
+
} catch {
|
|
6900
|
+
return null;
|
|
6901
|
+
}
|
|
6020
6902
|
}
|
|
6021
6903
|
/** The worker's processes under it, keyed by pid and start time. */
|
|
6022
6904
|
tree() {
|
|
@@ -6050,6 +6932,7 @@ var WorkerMetrics = class {
|
|
|
6050
6932
|
start(processes = true) {
|
|
6051
6933
|
try {
|
|
6052
6934
|
this.histogram?.reset();
|
|
6935
|
+
this.heapAfterGc = null;
|
|
6053
6936
|
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
6054
6937
|
this.usage = process.resourceUsage();
|
|
6055
6938
|
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
@@ -6072,6 +6955,7 @@ var WorkerMetrics = class {
|
|
|
6072
6955
|
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
6073
6956
|
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
6074
6957
|
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
6958
|
+
heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
|
|
6075
6959
|
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
6076
6960
|
}
|
|
6077
6961
|
};
|
|
@@ -6106,6 +6990,7 @@ var WorkerMetrics = class {
|
|
|
6106
6990
|
}
|
|
6107
6991
|
dispose() {
|
|
6108
6992
|
this.histogram?.disable();
|
|
6993
|
+
this.gcObserver?.disconnect();
|
|
6109
6994
|
}
|
|
6110
6995
|
};
|
|
6111
6996
|
var PageReader = class {
|
|
@@ -6127,12 +7012,36 @@ var PageReader = class {
|
|
|
6127
7012
|
try {
|
|
6128
7013
|
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
6129
7014
|
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
6130
|
-
|
|
7015
|
+
const main = {
|
|
6131
7016
|
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
6132
7017
|
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
6133
7018
|
nodes: values.get("Nodes") ?? 0,
|
|
6134
7019
|
listeners: values.get("JSEventListeners") ?? 0
|
|
6135
7020
|
};
|
|
7021
|
+
const elements = await this.countElements(page, opened);
|
|
7022
|
+
if (elements !== null) main.elements = elements;
|
|
7023
|
+
return main;
|
|
7024
|
+
} catch {
|
|
7025
|
+
return null;
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
/**
|
|
7029
|
+
* The elements attached to the page's main document, through the page's own
|
|
7030
|
+
* DevTools session: no Playwright call, so no step and no trace action. The
|
|
7031
|
+
* count runs in the page's main world and changes nothing in it.
|
|
7032
|
+
*/
|
|
7033
|
+
async countElements(page, session) {
|
|
7034
|
+
try {
|
|
7035
|
+
const result = await internalCall(
|
|
7036
|
+
page,
|
|
7037
|
+
() => session.send("Runtime.evaluate", {
|
|
7038
|
+
expression: "document.getElementsByTagName('*').length",
|
|
7039
|
+
returnByValue: true,
|
|
7040
|
+
silent: true
|
|
7041
|
+
})
|
|
7042
|
+
);
|
|
7043
|
+
const value = result.result?.value;
|
|
7044
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6136
7045
|
} catch {
|
|
6137
7046
|
return null;
|
|
6138
7047
|
}
|
|
@@ -6141,6 +7050,9 @@ var PageReader = class {
|
|
|
6141
7050
|
|
|
6142
7051
|
// src/internal/capture/resource-ledger.ts
|
|
6143
7052
|
var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
|
|
7053
|
+
var COUNTED_WITH_RUNNING = ["FSEventWrap", "StatWatcher"];
|
|
7054
|
+
var PLAYWRIGHT_ROLES = /* @__PURE__ */ new Set(["worker", "runner", "browser", "renderer", "gpu", "utility", "ffmpeg"]);
|
|
7055
|
+
var HANDLE_STACK_LIMIT = 40;
|
|
6144
7056
|
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
6145
7057
|
var CLOSE_TIMEOUT_MS = 5e3;
|
|
6146
7058
|
var PAGE_READ_CAP = 50;
|
|
@@ -6216,20 +7128,41 @@ function playwrightTestInfoSource() {
|
|
|
6216
7128
|
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
6217
7129
|
return value === "fail" || value === "close" ? value : "report";
|
|
6218
7130
|
}
|
|
6219
|
-
function countHandles() {
|
|
7131
|
+
function countHandles(types = TRACKED_HANDLE_TYPES) {
|
|
6220
7132
|
const counts = {};
|
|
6221
7133
|
const read = process.getActiveResourcesInfo;
|
|
6222
7134
|
if (typeof read !== "function") return counts;
|
|
6223
7135
|
for (const type of read.call(process)) {
|
|
6224
|
-
if (
|
|
7136
|
+
if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
6225
7137
|
}
|
|
6226
7138
|
return counts;
|
|
6227
7139
|
}
|
|
6228
|
-
async function settledHandles(start) {
|
|
6229
|
-
const end = countHandles();
|
|
7140
|
+
async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
|
|
7141
|
+
const end = countHandles(types);
|
|
6230
7142
|
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
6231
7143
|
await new Promise((resolve5) => setTimeout(resolve5, 0));
|
|
6232
|
-
return countHandles();
|
|
7144
|
+
return countHandles(types);
|
|
7145
|
+
}
|
|
7146
|
+
function deepUserSite() {
|
|
7147
|
+
const limit = Error.stackTraceLimit;
|
|
7148
|
+
Error.stackTraceLimit = HANDLE_STACK_LIMIT;
|
|
7149
|
+
try {
|
|
7150
|
+
return userSite(new Error().stack);
|
|
7151
|
+
} finally {
|
|
7152
|
+
Error.stackTraceLimit = limit;
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
var SHELLS = /^(?:sh|bash|dash|zsh|cmd|cmd\.exe|powershell|powershell\.exe|pwsh)$/i;
|
|
7156
|
+
function commandOf(spawnfile, spawnargs) {
|
|
7157
|
+
const name = (file) => path13.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
|
|
7158
|
+
const program = typeof spawnfile === "string" ? name(spawnfile) : "";
|
|
7159
|
+
const args = Array.isArray(spawnargs) ? spawnargs.map(String) : [];
|
|
7160
|
+
const script = args.findIndex((arg) => arg === "-c" || /^\/c$/i.test(arg));
|
|
7161
|
+
if (SHELLS.test(program) && script >= 0 && args[script + 1]) {
|
|
7162
|
+
const first = args[script + 1].trim().split(/\s+/)[0];
|
|
7163
|
+
if (first) return name(first);
|
|
7164
|
+
}
|
|
7165
|
+
return program || "a child process";
|
|
6233
7166
|
}
|
|
6234
7167
|
function settleWithin(promise, ms) {
|
|
6235
7168
|
let timer;
|
|
@@ -6241,7 +7174,7 @@ function settleWithin(promise, ms) {
|
|
|
6241
7174
|
]).finally(() => clearTimeout(timer));
|
|
6242
7175
|
}
|
|
6243
7176
|
var ResourceLedger = class {
|
|
6244
|
-
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
|
|
7177
|
+
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}, nodeHandles = false) {
|
|
6245
7178
|
this.playwright = playwright;
|
|
6246
7179
|
this.workerIndex = workerIndex;
|
|
6247
7180
|
this.leakCheck = leakCheck;
|
|
@@ -6249,6 +7182,7 @@ var ResourceLedger = class {
|
|
|
6249
7182
|
this.testInfoSource = testInfoSource;
|
|
6250
7183
|
this.now = now;
|
|
6251
7184
|
this.readers = readers;
|
|
7185
|
+
this.nodeHandles = nodeHandles;
|
|
6252
7186
|
this.nextId = 1;
|
|
6253
7187
|
this.entries = /* @__PURE__ */ new Map();
|
|
6254
7188
|
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
@@ -6265,6 +7199,11 @@ var ResourceLedger = class {
|
|
|
6265
7199
|
this.listener = null;
|
|
6266
7200
|
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
6267
7201
|
this.sawBrowser = false;
|
|
7202
|
+
/** The servers and child processes still running, while the ledger tracks them. */
|
|
7203
|
+
this.handles = /* @__PURE__ */ new Set();
|
|
7204
|
+
this.byHandle = /* @__PURE__ */ new WeakMap();
|
|
7205
|
+
this.trackingHandles = false;
|
|
7206
|
+
this.restores = [];
|
|
6268
7207
|
}
|
|
6269
7208
|
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
6270
7209
|
install() {
|
|
@@ -6297,13 +7236,107 @@ var ResourceLedger = class {
|
|
|
6297
7236
|
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
6298
7237
|
if (type) this.wrapBrowserType(type);
|
|
6299
7238
|
}
|
|
7239
|
+
if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
|
|
6300
7240
|
return true;
|
|
6301
7241
|
}
|
|
6302
|
-
/** Stop listening; the objects already tracked stay in the ledger. */
|
|
7242
|
+
/** Stop listening, and put Node's `listen` and `spawn` back; the objects already tracked stay in the ledger. */
|
|
6303
7243
|
uninstall() {
|
|
6304
7244
|
const remove = this.playwright?._instrumentation?.removeListener;
|
|
6305
7245
|
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
6306
7246
|
this.listener = null;
|
|
7247
|
+
for (const restore of this.restores.splice(0)) restore();
|
|
7248
|
+
this.trackingHandles = false;
|
|
7249
|
+
}
|
|
7250
|
+
/**
|
|
7251
|
+
* Wrap `net.Server.prototype.listen` (every HTTP, HTTPS and TCP server) and
|
|
7252
|
+
* `ChildProcess.prototype.spawn` (`spawn`, `exec`, `execFile` and `fork`), so
|
|
7253
|
+
* each server and child process the worker starts carries the line and the
|
|
7254
|
+
* runnable that started it, as a context does. A worker-scoped fixture's
|
|
7255
|
+
* server is then the worker's, never the first test's. Each wrapper calls the
|
|
7256
|
+
* original on its receiver and only records; it never throws.
|
|
7257
|
+
*/
|
|
7258
|
+
wrapNodeHandles() {
|
|
7259
|
+
const onServer = (server2, site) => this.guard(() => this.onServer(server2, site));
|
|
7260
|
+
const onChild = (child2, site) => this.guard(() => this.onChild(child2, site));
|
|
7261
|
+
const server = net3.Server.prototype;
|
|
7262
|
+
const listen = server.listen;
|
|
7263
|
+
if (typeof listen === "function") {
|
|
7264
|
+
const wrapped = function(...args) {
|
|
7265
|
+
onServer(this, deepUserSite());
|
|
7266
|
+
return listen.apply(this, args);
|
|
7267
|
+
};
|
|
7268
|
+
server.listen = wrapped;
|
|
7269
|
+
this.restores.push(() => {
|
|
7270
|
+
if (server.listen === wrapped) server.listen = listen;
|
|
7271
|
+
});
|
|
7272
|
+
}
|
|
7273
|
+
const child = childProcess.ChildProcess?.prototype;
|
|
7274
|
+
const spawn = child?.spawn;
|
|
7275
|
+
if (child && typeof spawn === "function") {
|
|
7276
|
+
const wrapped = function(...args) {
|
|
7277
|
+
const site = deepUserSite();
|
|
7278
|
+
const result = spawn.apply(this, args);
|
|
7279
|
+
onChild(this, site);
|
|
7280
|
+
return result;
|
|
7281
|
+
};
|
|
7282
|
+
child.spawn = wrapped;
|
|
7283
|
+
this.restores.push(() => {
|
|
7284
|
+
if (child.spawn === wrapped) child.spawn = spawn;
|
|
7285
|
+
});
|
|
7286
|
+
}
|
|
7287
|
+
this.trackingHandles = true;
|
|
7288
|
+
}
|
|
7289
|
+
trackHandle(object, entry) {
|
|
7290
|
+
const info = this.testInfoSource();
|
|
7291
|
+
const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
7292
|
+
const tracked = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
|
|
7293
|
+
this.handles.add(tracked);
|
|
7294
|
+
this.byHandle.set(object, tracked);
|
|
7295
|
+
return tracked;
|
|
7296
|
+
}
|
|
7297
|
+
onServer(server, site) {
|
|
7298
|
+
const known = this.byHandle.get(server);
|
|
7299
|
+
if (known && this.handles.has(known)) return;
|
|
7300
|
+
let bound = false;
|
|
7301
|
+
let closed = false;
|
|
7302
|
+
const entry = this.trackHandle(server, {
|
|
7303
|
+
type: "TCPServerWrap",
|
|
7304
|
+
site,
|
|
7305
|
+
// `close()` unbinds at once, while its 'close' event waits for open connections.
|
|
7306
|
+
running: () => server.listening,
|
|
7307
|
+
pending: () => !bound && !closed && !server.listening
|
|
7308
|
+
});
|
|
7309
|
+
server.once("listening", () => {
|
|
7310
|
+
bound = true;
|
|
7311
|
+
});
|
|
7312
|
+
server.once("close", () => {
|
|
7313
|
+
closed = true;
|
|
7314
|
+
this.handles.delete(entry);
|
|
7315
|
+
});
|
|
7316
|
+
}
|
|
7317
|
+
onChild(child, site) {
|
|
7318
|
+
const command = commandOf(child.spawnfile, child.spawnargs);
|
|
7319
|
+
const role = roleOf(command, (child.spawnargs ?? []).join(" "));
|
|
7320
|
+
if (role && PLAYWRIGHT_ROLES.has(role) || child.pid === void 0) return;
|
|
7321
|
+
const entry = this.trackHandle(child, {
|
|
7322
|
+
type: "ChildProcess",
|
|
7323
|
+
site,
|
|
7324
|
+
command,
|
|
7325
|
+
running: () => child.exitCode === null && child.signalCode === null
|
|
7326
|
+
});
|
|
7327
|
+
child.once("exit", () => this.handles.delete(entry));
|
|
7328
|
+
}
|
|
7329
|
+
/**
|
|
7330
|
+
* The servers and child processes a test started (its body, its each-hooks
|
|
7331
|
+
* or a test-scoped fixture) that still run. A server whose `listen` has not
|
|
7332
|
+
* bound yet is read again one tick later.
|
|
7333
|
+
*/
|
|
7334
|
+
async runningOf(testId) {
|
|
7335
|
+
const own = () => [...this.handles].filter(
|
|
7336
|
+
(entry) => entry.test?.id === testId && !entry.fixture?.worker && (entry.phase === "test" || entry.phase === "beforeEach" || entry.phase === "afterEach")
|
|
7337
|
+
);
|
|
7338
|
+
if (own().some((entry) => entry.pending?.())) await new Promise((resolve5) => setTimeout(resolve5, 0));
|
|
7339
|
+
return own().filter((entry) => entry.running()).map((entry) => ({ type: entry.type, site: entry.site, ...entry.command ? { command: entry.command } : {} }));
|
|
6307
7340
|
}
|
|
6308
7341
|
/** Mark a page used: a locator was built on it. */
|
|
6309
7342
|
notePageUse(page) {
|
|
@@ -6313,7 +7346,7 @@ var ResourceLedger = class {
|
|
|
6313
7346
|
/** Open the census window of a test. */
|
|
6314
7347
|
testStarted(info) {
|
|
6315
7348
|
this.window = testRef2(info);
|
|
6316
|
-
this.handlesAtStart = countHandles();
|
|
7349
|
+
this.handlesAtStart = countHandles(this.countedHandleTypes());
|
|
6317
7350
|
this.openAtStart = { contexts: 0, pages: 0 };
|
|
6318
7351
|
for (const entry of this.openEntries()) {
|
|
6319
7352
|
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
@@ -6333,9 +7366,14 @@ var ResourceLedger = class {
|
|
|
6333
7366
|
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
6334
7367
|
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
6335
7368
|
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
6336
|
-
const handles = {
|
|
7369
|
+
const handles = {
|
|
7370
|
+
start: this.handlesAtStart,
|
|
7371
|
+
end: await settledHandles(this.handlesAtStart, this.countedHandleTypes())
|
|
7372
|
+
};
|
|
7373
|
+
const running = this.trackingHandles && test ? await this.runningOf(test.id) : null;
|
|
6337
7374
|
const census = this.census(ref, handles, await this.readPages());
|
|
6338
7375
|
if (metrics) census.metrics = metrics;
|
|
7376
|
+
if (running) census.running = running;
|
|
6339
7377
|
if (test) {
|
|
6340
7378
|
census.openAtStart = this.openAtStart;
|
|
6341
7379
|
if (!failed) census.leftOpen = leaks.length;
|
|
@@ -6374,6 +7412,10 @@ var ResourceLedger = class {
|
|
|
6374
7412
|
this.uninstall();
|
|
6375
7413
|
return census;
|
|
6376
7414
|
}
|
|
7415
|
+
/** The handle types counted at each boundary: servers leave the count once they are tracked one by one. */
|
|
7416
|
+
countedHandleTypes() {
|
|
7417
|
+
return this.trackingHandles ? COUNTED_WITH_RUNNING : TRACKED_HANDLE_TYPES;
|
|
7418
|
+
}
|
|
6377
7419
|
guard(run) {
|
|
6378
7420
|
try {
|
|
6379
7421
|
run();
|
|
@@ -6596,6 +7638,7 @@ var ResourceLedger = class {
|
|
|
6596
7638
|
open
|
|
6597
7639
|
};
|
|
6598
7640
|
if (handles) census.handles = handles;
|
|
7641
|
+
for (const close of this.closed) this.entries.delete(close.id);
|
|
6599
7642
|
this.born = [];
|
|
6600
7643
|
this.closed = [];
|
|
6601
7644
|
return census;
|
|
@@ -6634,7 +7677,8 @@ function startResourceLedger(playwright, workerIndex) {
|
|
|
6634
7677
|
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
6635
7678
|
playwrightTestInfoSource(),
|
|
6636
7679
|
Date.now,
|
|
6637
|
-
{ metrics, pages: new PageReader() }
|
|
7680
|
+
{ metrics, pages: new PageReader() },
|
|
7681
|
+
true
|
|
6638
7682
|
);
|
|
6639
7683
|
if (!ledger.install()) {
|
|
6640
7684
|
metrics.dispose();
|
|
@@ -6985,10 +8029,15 @@ function createSink() {
|
|
|
6985
8029
|
locatorCalls: /* @__PURE__ */ new Map(),
|
|
6986
8030
|
stops: [],
|
|
6987
8031
|
shownStops: /* @__PURE__ */ new Map(),
|
|
8032
|
+
failureHeld: false,
|
|
8033
|
+
frozenAt: null,
|
|
8034
|
+
stopEnds: 0,
|
|
8035
|
+
ending: false,
|
|
6988
8036
|
released: [],
|
|
6989
8037
|
releasedRequests: /* @__PURE__ */ new Set(),
|
|
6990
8038
|
onReleasedRequestsEnded: null,
|
|
6991
8039
|
releasedQuietSince: 0,
|
|
8040
|
+
releasedUntil: null,
|
|
6992
8041
|
locatorPages: new LocatorPageLog(),
|
|
6993
8042
|
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
|
|
6994
8043
|
serverCoverage: /* @__PURE__ */ new Map(),
|
|
@@ -7092,6 +8141,25 @@ async function drainPendingProbes(capMs) {
|
|
|
7092
8141
|
]);
|
|
7093
8142
|
clearTimeout(cap);
|
|
7094
8143
|
}
|
|
8144
|
+
async function settleBeforeClose(closing) {
|
|
8145
|
+
const drained = drainPendingProbes(1e3);
|
|
8146
|
+
const sink = currentSink;
|
|
8147
|
+
if (sink) {
|
|
8148
|
+
await endShownStops(sink, "closed", closing);
|
|
8149
|
+
await awaitReleasedCalls(sink);
|
|
8150
|
+
await stashPageState(sink, closing);
|
|
8151
|
+
}
|
|
8152
|
+
await drained;
|
|
8153
|
+
if (sink) {
|
|
8154
|
+
await maybeOpenPicker(sink, closing);
|
|
8155
|
+
await stopSinkCodeReach(sink, closing);
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
async function settleContextClose(context) {
|
|
8159
|
+
CLOSING_CONTEXTS.add(context);
|
|
8160
|
+
await settleBeforeClose({ context });
|
|
8161
|
+
if (contextDebugger(context)?.pausedDetails()) await steerDebugger(context, "resume");
|
|
8162
|
+
}
|
|
7095
8163
|
function isPageClosed(page) {
|
|
7096
8164
|
try {
|
|
7097
8165
|
return typeof page.isClosed === "function" ? page.isClosed() : false;
|
|
@@ -7214,7 +8282,7 @@ function buildPageState(raw, cookies, interaction) {
|
|
|
7214
8282
|
}
|
|
7215
8283
|
async function readPageState(page) {
|
|
7216
8284
|
try {
|
|
7217
|
-
const
|
|
8285
|
+
const readRaw = page.evaluate(() => {
|
|
7218
8286
|
const listStorage = (s) => {
|
|
7219
8287
|
const out = [];
|
|
7220
8288
|
try {
|
|
@@ -7240,14 +8308,16 @@ async function readPageState(page) {
|
|
|
7240
8308
|
sessionStorage: listStorage(globalThis.sessionStorage)
|
|
7241
8309
|
};
|
|
7242
8310
|
});
|
|
8311
|
+
const readCookies = (async () => {
|
|
8312
|
+
try {
|
|
8313
|
+
return await pageContext(page)?.cookies() ?? null;
|
|
8314
|
+
} catch {
|
|
8315
|
+
return null;
|
|
8316
|
+
}
|
|
8317
|
+
})();
|
|
8318
|
+
const readInteraction = page.evaluate(readPageInteraction).catch(() => null);
|
|
8319
|
+
const [raw, cookies, interaction] = await Promise.all([readRaw, readCookies, readInteraction]);
|
|
7243
8320
|
if (!raw) return null;
|
|
7244
|
-
let cookies = null;
|
|
7245
|
-
try {
|
|
7246
|
-
cookies = await pageContext(page)?.cookies() ?? null;
|
|
7247
|
-
} catch {
|
|
7248
|
-
cookies = null;
|
|
7249
|
-
}
|
|
7250
|
-
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
7251
8321
|
return buildPageState(raw, cookies, interaction);
|
|
7252
8322
|
} catch {
|
|
7253
8323
|
return null;
|
|
@@ -7311,7 +8381,7 @@ async function recordPageWindow(sink, page) {
|
|
|
7311
8381
|
}
|
|
7312
8382
|
async function stashPageState(sink, closing) {
|
|
7313
8383
|
const page = sink.lastActivePage;
|
|
7314
|
-
if (!page || isPageClosed(page)) return;
|
|
8384
|
+
if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
|
|
7315
8385
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
7316
8386
|
if (!belongsToClosing) return;
|
|
7317
8387
|
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
@@ -7323,13 +8393,12 @@ async function readBlankPage(page) {
|
|
|
7323
8393
|
return aria != null && aria.trim() === "";
|
|
7324
8394
|
}
|
|
7325
8395
|
async function readPageBeforeClose(sink, page) {
|
|
7326
|
-
const
|
|
8396
|
+
const readBlank = sink.probeItem?.level === "server" ? readBlankPage(page).catch(() => null) : null;
|
|
8397
|
+
const readState = process.env.PIWI_CAPTURE_PAGE_STATE !== "false" ? Promise.resolve(readBlank).then(() => readPageState(page)) : null;
|
|
8398
|
+
const [vitals, blankPage, pageState] = await Promise.all([readWebVitals(page), readBlank, readState]);
|
|
7327
8399
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
7328
|
-
if (
|
|
7329
|
-
if (
|
|
7330
|
-
const pageState = await readPageState(page);
|
|
7331
|
-
if (pageState) sink.stashedPageState = pageState;
|
|
7332
|
-
}
|
|
8400
|
+
if (blankPage !== null) sink.probeBlankPage = blankPage;
|
|
8401
|
+
if (pageState) sink.stashedPageState = pageState;
|
|
7333
8402
|
const status = sink.testInfo?.status;
|
|
7334
8403
|
const sampleAria = async () => {
|
|
7335
8404
|
const root = page.locator(":root");
|
|
@@ -7523,16 +8592,35 @@ function locatorSource(target, method, args) {
|
|
|
7523
8592
|
return renderFailing({ method, args });
|
|
7524
8593
|
}
|
|
7525
8594
|
var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
8595
|
+
var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
8596
|
+
var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
|
|
7526
8597
|
var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
7527
|
-
|
|
8598
|
+
function stopPageOf(context, call, paused) {
|
|
8599
|
+
const named = [call?.page, paused].find((page) => !!page && pageContext(page) === context);
|
|
8600
|
+
if (named) return { page: named };
|
|
8601
|
+
const open = openPages(context);
|
|
8602
|
+
return open.length === 1 ? { page: open[0] } : { among: open };
|
|
8603
|
+
}
|
|
8604
|
+
function openPages(context) {
|
|
8605
|
+
return context.pages().filter((page) => !isPageClosed(page));
|
|
8606
|
+
}
|
|
8607
|
+
function stopPages(of) {
|
|
8608
|
+
return "page" in of ? [of.page] : of.among;
|
|
8609
|
+
}
|
|
8610
|
+
function stopCanShow(context, of) {
|
|
8611
|
+
if (CLOSING_CONTEXTS.has(context)) return false;
|
|
8612
|
+
return !stopPages(of).some((page) => isPageClosed(page) || CLOSING_PAGES.has(page));
|
|
8613
|
+
}
|
|
8614
|
+
async function armStops(contexts) {
|
|
7528
8615
|
const state = currentSink?.pause;
|
|
7529
|
-
if (!state || state
|
|
8616
|
+
if (!state || !wantsStops(state)) return;
|
|
7530
8617
|
await Promise.all(
|
|
7531
8618
|
[...contexts].map(
|
|
7532
8619
|
(context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
|
|
7533
8620
|
)
|
|
7534
8621
|
);
|
|
7535
8622
|
}
|
|
8623
|
+
var stopTurn = Promise.resolve();
|
|
7536
8624
|
function onDebuggerStop(context) {
|
|
7537
8625
|
const details = contextDebugger(context)?.pausedDetails();
|
|
7538
8626
|
if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
|
|
@@ -7543,13 +8631,38 @@ function onDebuggerStop(context) {
|
|
|
7543
8631
|
try {
|
|
7544
8632
|
if (!sink || !state) return;
|
|
7545
8633
|
const callSite = stopSite(sink, details.location);
|
|
8634
|
+
const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
|
|
7546
8635
|
const pagePause = PAGE_PAUSES.get(context);
|
|
8636
|
+
const asked = askedOnce(state);
|
|
7547
8637
|
const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
|
|
7548
8638
|
if ("go" in decided) {
|
|
7549
8639
|
how = decided.go;
|
|
7550
8640
|
return;
|
|
7551
8641
|
}
|
|
7552
|
-
|
|
8642
|
+
if (pagePause) stopShown(state);
|
|
8643
|
+
if (sink.ending || CLOSING_CONTEXTS.has(context)) {
|
|
8644
|
+
askAgain(state, asked);
|
|
8645
|
+
how = goOn(state, sink.ending ? endedWaiting(sink, call) : void 0);
|
|
8646
|
+
return;
|
|
8647
|
+
}
|
|
8648
|
+
const of = stopPageOf(context, call, pagePause?.page);
|
|
8649
|
+
const ends = sink.stopEnds;
|
|
8650
|
+
const shown2 = stopTurn.then(async () => {
|
|
8651
|
+
const ended = currentSink !== sink || sink.ending || sink.stopEnds !== ends;
|
|
8652
|
+
const gone = decided.show === "breakpoint" && !atBreakpoint(state, callSite);
|
|
8653
|
+
if (!ended && !gone && stopCanShow(context, of)) {
|
|
8654
|
+
const location = callSite ?? pagePause.location;
|
|
8655
|
+
const answer = await showStop(sink, context, details.title, location, decided.show, call, of);
|
|
8656
|
+
if (answer !== void 0) return answer;
|
|
8657
|
+
}
|
|
8658
|
+
askAgain(state, asked);
|
|
8659
|
+
if (ended) return endedWaiting(sink, call);
|
|
8660
|
+
const closing = stopPages(of).flatMap((page) => CLOSING_PAGES.get(page) ?? []);
|
|
8661
|
+
await settledWithin(closing, END_STOP_MS);
|
|
8662
|
+
return void 0;
|
|
8663
|
+
});
|
|
8664
|
+
stopTurn = shown2.catch(() => void 0);
|
|
8665
|
+
how = goOn(state, await shown2);
|
|
7553
8666
|
} catch {
|
|
7554
8667
|
if (state) how = goOn(state);
|
|
7555
8668
|
} finally {
|
|
@@ -7557,22 +8670,30 @@ function onDebuggerStop(context) {
|
|
|
7557
8670
|
}
|
|
7558
8671
|
})();
|
|
7559
8672
|
}
|
|
8673
|
+
function endedWaiting(sink, call) {
|
|
8674
|
+
releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
8675
|
+
return "finish";
|
|
8676
|
+
}
|
|
7560
8677
|
function stopSite(sink, location) {
|
|
7561
8678
|
if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
|
|
7562
8679
|
return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
|
|
7563
8680
|
}
|
|
7564
|
-
function
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
const open = context
|
|
8681
|
+
function stopBarPage(sink, context, of) {
|
|
8682
|
+
if ("page" in of) return of.page;
|
|
8683
|
+
const last = sink.lastActivePage;
|
|
8684
|
+
if (last && !isPageClosed(last) && pageContext(last) === context) return last;
|
|
8685
|
+
const open = openPages(context);
|
|
7569
8686
|
return open[open.length - 1] ?? null;
|
|
7570
8687
|
}
|
|
7571
|
-
async function showStop(sink, context, title, location, reason) {
|
|
8688
|
+
async function showStop(sink, context, title, location, reason, call, of, failure = null) {
|
|
7572
8689
|
const testInfo = sink.testInfo;
|
|
7573
|
-
const
|
|
7574
|
-
const
|
|
7575
|
-
if (!testInfo
|
|
8690
|
+
const page = stopBarPage(sink, context, of);
|
|
8691
|
+
const guessed = !("page" in of);
|
|
8692
|
+
if (!testInfo) return void 0;
|
|
8693
|
+
const bar = !!page && testInfo.project?.use?.headless === false;
|
|
8694
|
+
const place = runChannelOpen() ? callSitePlace(location) : null;
|
|
8695
|
+
if (!bar && !place) return void 0;
|
|
8696
|
+
const failing = failure?.failed ?? null;
|
|
7576
8697
|
const onPick = async (picked) => {
|
|
7577
8698
|
if (call) {
|
|
7578
8699
|
call.picked = true;
|
|
@@ -7583,16 +8704,57 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
7583
8704
|
...picked
|
|
7584
8705
|
});
|
|
7585
8706
|
sink.probedLocations.add(call.location);
|
|
8707
|
+
} else if (failing) {
|
|
8708
|
+
const pick = {
|
|
8709
|
+
failing: { method: failing.method, args: failing.args, rendered: renderFailing(failing), location },
|
|
8710
|
+
...picked
|
|
8711
|
+
};
|
|
8712
|
+
sink.userPick = pick;
|
|
8713
|
+
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
7586
8714
|
}
|
|
7587
8715
|
console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
|
|
7588
|
-
await postPickToEditor(
|
|
8716
|
+
await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
|
|
7589
8717
|
};
|
|
8718
|
+
const handoffReason = reason === "page-pause" ? "pause" : "breakpoint";
|
|
8719
|
+
const onRecord = bar && page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
|
|
8720
|
+
const intents = bar && page ? new BarIntents() : null;
|
|
7590
8721
|
const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
|
|
7591
8722
|
sink.stops.push(record);
|
|
7592
8723
|
const end = new AbortController();
|
|
7593
|
-
const
|
|
7594
|
-
|
|
7595
|
-
|
|
8724
|
+
const over = new AbortController();
|
|
8725
|
+
end.signal.addEventListener("abort", () => over.abort(), { once: true });
|
|
8726
|
+
let fromEditor;
|
|
8727
|
+
const held2 = place ? holdStop(
|
|
8728
|
+
{
|
|
8729
|
+
reason,
|
|
8730
|
+
title,
|
|
8731
|
+
place,
|
|
8732
|
+
locator: call ? locatorSource(call.target, call.origin.method, call.origin.args) : failing ? renderFailing(failing) : null,
|
|
8733
|
+
bar,
|
|
8734
|
+
page: guessed ? null : page,
|
|
8735
|
+
...failure?.error ? { error: failure.error } : {},
|
|
8736
|
+
intent: intents ? (what) => intentAt(intents, what, {
|
|
8737
|
+
record: !!onRecord,
|
|
8738
|
+
guessed,
|
|
8739
|
+
// A failure's stop is held in the teardown it ends with.
|
|
8740
|
+
holds: () => !over.signal.aborted && (!!failure || !sink.ending && stopCanShow(context, of))
|
|
8741
|
+
}) : void 0
|
|
8742
|
+
},
|
|
8743
|
+
(answer2) => {
|
|
8744
|
+
fromEditor = answer2;
|
|
8745
|
+
over.abort();
|
|
8746
|
+
}
|
|
8747
|
+
) : null;
|
|
8748
|
+
const onIntent = bar && page && held2 ? (intent, ended) => intent === "assert" ? checkAt(page, testInfo, location, handoffReason, held2.stop, ended) : regionAt(page, location, held2.stop, ended) : void 0;
|
|
8749
|
+
const shown2 = withTestTimeoutHeld(testInfo, async () => {
|
|
8750
|
+
if (!bar || !page) {
|
|
8751
|
+
console.log(
|
|
8752
|
+
failure ? `[piwi] The test failed at ${callSiteLine(location)}: Resume in the editor lets it end as it failed.` : `[piwi] Paused at ${callSiteLine(location)} before ${title}: Resume, Step or Finish in the editor.`
|
|
8753
|
+
);
|
|
8754
|
+
await abortion(over.signal);
|
|
8755
|
+
return "resume";
|
|
8756
|
+
}
|
|
8757
|
+
return internalCall(
|
|
7596
8758
|
page,
|
|
7597
8759
|
() => pauseHere(
|
|
7598
8760
|
page,
|
|
@@ -7600,45 +8762,221 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
7600
8762
|
location,
|
|
7601
8763
|
call: title,
|
|
7602
8764
|
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
7603
|
-
target: call?.page === page ? call.target : null
|
|
8765
|
+
target: call?.page === page ? call.target : null,
|
|
8766
|
+
...failure ? { failure: true } : {}
|
|
7604
8767
|
},
|
|
7605
8768
|
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
7606
8769
|
onPick,
|
|
7607
|
-
|
|
7608
|
-
|
|
8770
|
+
onRecord,
|
|
8771
|
+
over.signal,
|
|
8772
|
+
intents ?? void 0,
|
|
8773
|
+
onIntent
|
|
7609
8774
|
)
|
|
7610
|
-
)
|
|
7611
|
-
).finally(() => {
|
|
8775
|
+
);
|
|
8776
|
+
}).finally(async () => {
|
|
7612
8777
|
if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
|
|
7613
8778
|
else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
7614
8779
|
sink.shownStops.delete(end);
|
|
8780
|
+
await held2?.release();
|
|
7615
8781
|
});
|
|
7616
8782
|
const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
8783
|
+
const stop = { page, guessed, context, record, done: shown2.catch(() => void 0), heldAt, after: null };
|
|
8784
|
+
if (!failure) sink.shownStops.set(end, stop);
|
|
8785
|
+
const answer = await shown2;
|
|
8786
|
+
if (!end.signal.aborted) return fromEditor ?? answer;
|
|
8787
|
+
if (end.signal.reason === "ended") return "finish";
|
|
8788
|
+
await settledWithin(stop.after ? [stop.after] : [], END_STOP_MS);
|
|
8789
|
+
return "resume";
|
|
8790
|
+
}
|
|
8791
|
+
function intentAt(intents, what, stop) {
|
|
8792
|
+
if (!stop.holds()) return { ok: false, message: "The test went on." };
|
|
8793
|
+
if (stop.guessed) {
|
|
8794
|
+
return {
|
|
8795
|
+
ok: false,
|
|
8796
|
+
message: `${RUN_INTENT_NAMES[what]} needs the page the test stopped in, which this call does not name among its context's pages: use the pause bar in the test's window.`
|
|
8797
|
+
};
|
|
8798
|
+
}
|
|
8799
|
+
if ((what === "record" || what === "assert") && !stop.record) {
|
|
8800
|
+
return {
|
|
8801
|
+
ok: false,
|
|
8802
|
+
message: `${RUN_INTENT_NAMES[what]} needs the editor that started the run: this run names no record channel.`
|
|
8803
|
+
};
|
|
8804
|
+
}
|
|
8805
|
+
if (!intents.offer(what)) {
|
|
8806
|
+
return {
|
|
8807
|
+
ok: false,
|
|
8808
|
+
message: `${RUN_INTENT_NAMES[what]} waits: a pick, a check or a recording runs in the test's window.`
|
|
8809
|
+
};
|
|
8810
|
+
}
|
|
8811
|
+
const started = {
|
|
8812
|
+
pick: "Pick an element in the test's window: the locator goes to the editor.",
|
|
8813
|
+
record: "Recording in the test's window: the editor writes the steps above the stopped line.",
|
|
8814
|
+
assert: "Check an element in the test's window: the editor writes the check above the stopped line.",
|
|
8815
|
+
"assert-region": "Pick the region in the test's window: the editor writes its ARIA snapshot above the stopped line."
|
|
8816
|
+
};
|
|
8817
|
+
return { ok: true, message: started[what] };
|
|
8818
|
+
}
|
|
8819
|
+
async function checkAt(page, testInfo, location, reason, stop, ended) {
|
|
8820
|
+
const ref = process.env[PIWI_ORIGIN_ENV.ref] ?? "";
|
|
8821
|
+
console.log(`[piwi] Checking an element at ${callSiteLine(location)}: the editor writes the check.`);
|
|
8822
|
+
const why = await recordFromHere({
|
|
8823
|
+
page,
|
|
8824
|
+
testInfo,
|
|
8825
|
+
location,
|
|
8826
|
+
reason,
|
|
8827
|
+
mode: "check",
|
|
8828
|
+
stop: { ref, worker: testInfo.workerIndex, stop },
|
|
8829
|
+
ended
|
|
8830
|
+
});
|
|
8831
|
+
if (ended?.aborted) console.log("[piwi] The check ended with the stop.");
|
|
8832
|
+
else console.log(why ? `[piwi] Nothing was checked: ${why}` : "[piwi] The check ended: the test is paused again.");
|
|
8833
|
+
}
|
|
8834
|
+
async function regionAt(page, location, stop, ended) {
|
|
8835
|
+
const picked = await pickLocator(page, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG }, null, ended).catch(
|
|
8836
|
+
() => null
|
|
8837
|
+
);
|
|
8838
|
+
if (!picked || ended?.aborted) return;
|
|
8839
|
+
const source = picked.picked.locator;
|
|
8840
|
+
const say = (message) => {
|
|
8841
|
+
console.log(`[piwi] ${message}`);
|
|
8842
|
+
tellHub({ type: "region", stop, refused: message });
|
|
8843
|
+
};
|
|
8844
|
+
const region = locatorOn(page, source);
|
|
8845
|
+
if (!region) return say(`The region's locator, ${source}, is not one Piwi can read.`);
|
|
8846
|
+
const count = await region.count().catch(() => 0);
|
|
8847
|
+
if (count !== 1) return say(`${source} finds ${count} elements: pick a region it finds alone.`);
|
|
8848
|
+
const read = await region.ariaSnapshot({ timeout: 5e3 }).catch(() => null);
|
|
8849
|
+
if (!read?.trim()) return say(`The ARIA snapshot of ${source} could not be read.`);
|
|
8850
|
+
const secrets = await regionSecrets(region).catch(() => null);
|
|
8851
|
+
if (secrets === null) return say(`The fields of ${source} that hold a secret could not be read.`);
|
|
8852
|
+
const snapshot = regionSnapshotWithoutSecrets(read, secrets);
|
|
8853
|
+
if (snapshot.length > MAX_REGION_SNAPSHOT) {
|
|
8854
|
+
return say(
|
|
8855
|
+
`The ARIA snapshot of ${source} is longer than ${MAX_REGION_SNAPSHOT} characters: pick a smaller region.`
|
|
8856
|
+
);
|
|
8857
|
+
}
|
|
8858
|
+
console.log(`[piwi] Region picked at ${callSiteLine(location)}: ${source}, its ARIA snapshot sent to the editor.`);
|
|
8859
|
+
tellHub({ type: "region", stop, locator: source, snapshot });
|
|
8860
|
+
}
|
|
8861
|
+
function regionSecrets(region) {
|
|
8862
|
+
return region.locator(":scope:is(input, textarea), input, textarea").evaluateAll(
|
|
8863
|
+
(elements, secret) => {
|
|
8864
|
+
return elements.flatMap((field) => {
|
|
8865
|
+
const tokens = (field.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
|
|
8866
|
+
const password = field.localName === "input" && String(field.type).toLowerCase() === "password";
|
|
8867
|
+
return password || tokens.some((token) => secret.includes(token)) ? [String(field.value ?? "")] : [];
|
|
8868
|
+
});
|
|
8869
|
+
},
|
|
8870
|
+
[...SECRET_AUTOCOMPLETE]
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8873
|
+
async function holdFailure(sink, fallback) {
|
|
8874
|
+
const testInfo = sink.testInfo;
|
|
8875
|
+
if (sink.failureHeld || !sink.pause || !testInfo || !editorShows("failure")) return;
|
|
8876
|
+
const { status, expectedStatus } = testInfo;
|
|
8877
|
+
if (status !== "failed" && status !== "timedOut" || status === expectedStatus) return;
|
|
8878
|
+
const page = [sink.lastActivePage, fallback].find((open) => !!open && !isPageClosed(open));
|
|
8879
|
+
const context = page ? pageContext(page) : null;
|
|
8880
|
+
if (!page || !context) return;
|
|
8881
|
+
sink.failureHeld = true;
|
|
8882
|
+
const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
|
|
8883
|
+
const errors = testInfo.errors.length ? testInfo.errors : testInfo.error ? [testInfo.error] : [];
|
|
8884
|
+
const declared = (place2) => place2.file === testInfo.file && place2.line === testInfo.line;
|
|
8885
|
+
const placed = errors.flatMap((error2) => {
|
|
8886
|
+
const place2 = errorPlace(error2);
|
|
8887
|
+
return place2 && !declared(place2) ? [{ error: error2, place: place2 }] : [];
|
|
8888
|
+
})[0];
|
|
8889
|
+
const error = placed?.error ?? errors[0];
|
|
8890
|
+
const place = placed?.place ?? (failed?.location ? null : stepInProgress(testInfo));
|
|
8891
|
+
const location = (place ? stopCallSite(place) : null) ?? failed?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
|
|
8892
|
+
if (!location) return;
|
|
8893
|
+
const said = stripAnsi(error?.message ?? "").split("\n").find((line) => line.trim()) ?? status;
|
|
8894
|
+
if (page === sink.lastActivePage)
|
|
8895
|
+
await internalCall(page, () => readPageBeforeClose(sink, page)).catch(() => void 0);
|
|
8896
|
+
sink.frozenAt = Date.now();
|
|
8897
|
+
await stopSinkCodeReach(sink, { context });
|
|
8898
|
+
const title = hideSecretsInText(`Failed: ${said.trim().slice(0, 300)}`).slice(0, 1e3);
|
|
8899
|
+
const errorText = failureError(error?.message ?? error?.value);
|
|
8900
|
+
const named = errorText === null ? null : hideSecretsInText(errorText).slice(0, MAX_STOP_ERROR);
|
|
8901
|
+
await showStop(sink, context, title, location, "failure", void 0, { page }, { failed, error: named });
|
|
8902
|
+
}
|
|
8903
|
+
var FAILURE_ERROR_LINES = 4;
|
|
8904
|
+
function failureError(message) {
|
|
8905
|
+
const lines = [];
|
|
8906
|
+
for (const line of stripAnsi(message ?? "").trimStart().replace(/^Error: /, "").split("\n")) {
|
|
8907
|
+
if (!line.trim() || /^\s*Call log:/.test(line)) break;
|
|
8908
|
+
lines.push(line.trimEnd());
|
|
8909
|
+
if (lines.length === FAILURE_ERROR_LINES) break;
|
|
8910
|
+
}
|
|
8911
|
+
const text = lines.join("\n").slice(0, MAX_STOP_ERROR);
|
|
8912
|
+
return text.trim() ? text : null;
|
|
8913
|
+
}
|
|
8914
|
+
function errorPlace(error) {
|
|
8915
|
+
const given = error?.location;
|
|
8916
|
+
if (typeof given?.file === "string" && typeof given.line === "number") {
|
|
8917
|
+
return { file: given.file, line: given.line, column: given.column ?? 0 };
|
|
8918
|
+
}
|
|
8919
|
+
const stack = error?.stack;
|
|
8920
|
+
for (const line of typeof stack === "string" ? stack.split("\n") : []) {
|
|
8921
|
+
const frame = /^\s*at\s+(?:.*?\s+\()?(?:file:\/\/)?(\/[^():]+|[A-Za-z]:[\\/][^():]+):(\d+):(\d+)\)?$/.exec(line);
|
|
8922
|
+
if (!frame) continue;
|
|
8923
|
+
const file = frame[1];
|
|
8924
|
+
if (/[\\/]node_modules[\\/]/.test(file) || path14.dirname(path14.resolve(file)) === __dirname) continue;
|
|
8925
|
+
return { file, line: Number(frame[2]), column: Number(frame[3]) };
|
|
8926
|
+
}
|
|
8927
|
+
return null;
|
|
8928
|
+
}
|
|
8929
|
+
function stepInProgress(testInfo) {
|
|
8930
|
+
let found = null;
|
|
8931
|
+
const visit = (steps) => {
|
|
8932
|
+
if (!Array.isArray(steps)) return;
|
|
8933
|
+
for (const step of steps) {
|
|
8934
|
+
if (step.endWallTime) continue;
|
|
8935
|
+
const at = step.stack?.[0];
|
|
8936
|
+
const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
|
|
8937
|
+
if (called && typeof at?.file === "string" && typeof at.line === "number") {
|
|
8938
|
+
found = { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 };
|
|
8939
|
+
}
|
|
8940
|
+
visit(step.steps);
|
|
8941
|
+
}
|
|
8942
|
+
};
|
|
8943
|
+
visit(testInfo._steps);
|
|
8944
|
+
return found;
|
|
8945
|
+
}
|
|
8946
|
+
function abortion(signal) {
|
|
8947
|
+
return new Promise((resolve5) => {
|
|
8948
|
+
if (signal.aborted) resolve5();
|
|
8949
|
+
else signal.addEventListener("abort", () => resolve5(), { once: true });
|
|
8950
|
+
});
|
|
7621
8951
|
}
|
|
7622
8952
|
var END_STOP_MS = 2e3;
|
|
7623
8953
|
var RELEASED_CALL_MS = 2e3;
|
|
7624
8954
|
var RELEASED_QUIET_MS = 200;
|
|
7625
8955
|
async function endShownStops(sink, why, closing) {
|
|
7626
8956
|
const ending = [];
|
|
8957
|
+
if (why === "ended") sink.stopEnds++;
|
|
7627
8958
|
let heldAt = Infinity;
|
|
7628
|
-
for (const [end,
|
|
7629
|
-
if (closing &&
|
|
8959
|
+
for (const [end, stop] of sink.shownStops) {
|
|
8960
|
+
if (closing && !closesStop(closing, stop)) continue;
|
|
7630
8961
|
if (!end.signal.aborted) {
|
|
7631
|
-
record.duration = Date.now() - record.startedAt;
|
|
8962
|
+
stop.record.duration = Date.now() - stop.record.startedAt;
|
|
8963
|
+
if (closing?.page) stop.after = CLOSING_PAGES.get(closing.page) ?? null;
|
|
7632
8964
|
end.abort(why);
|
|
7633
|
-
if (why === "ended" &&
|
|
8965
|
+
if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
|
|
7634
8966
|
}
|
|
7635
|
-
ending.push(done);
|
|
8967
|
+
ending.push(stop.done);
|
|
7636
8968
|
}
|
|
7637
8969
|
if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
|
|
7638
8970
|
await settledWithin(ending, END_STOP_MS);
|
|
7639
8971
|
}
|
|
8972
|
+
function closesStop(closing, stop) {
|
|
8973
|
+
if (closing.context && stop.context === closing.context) return true;
|
|
8974
|
+
if (!closing.page) return false;
|
|
8975
|
+
return stop.page === closing.page || stop.guessed && pageContext(closing.page) === stop.context;
|
|
8976
|
+
}
|
|
7640
8977
|
function releaseCall(sink, settled) {
|
|
7641
8978
|
sink.releasedQuietSince = Date.now();
|
|
8979
|
+
sink.releasedUntil = null;
|
|
7642
8980
|
sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
|
|
7643
8981
|
}
|
|
7644
8982
|
function releasedRequestsEnded(sink) {
|
|
@@ -7647,8 +8985,8 @@ function releasedRequestsEnded(sink) {
|
|
|
7647
8985
|
}
|
|
7648
8986
|
async function awaitReleasedCalls(sink) {
|
|
7649
8987
|
if (sink.released.length === 0) return;
|
|
7650
|
-
const deadline = Date.now() + RELEASED_CALL_MS;
|
|
7651
|
-
await settledWithin(sink.released,
|
|
8988
|
+
const deadline = sink.releasedUntil ??= Date.now() + RELEASED_CALL_MS;
|
|
8989
|
+
await settledWithin(sink.released, Math.max(0, deadline - Date.now()));
|
|
7652
8990
|
for (let now = Date.now(); now < deadline; now = Date.now()) {
|
|
7653
8991
|
if (sink.releasedRequests.size > 0) {
|
|
7654
8992
|
await settledWithin([releasedRequestsEnded(sink)], deadline - now);
|
|
@@ -7678,7 +9016,7 @@ function recorderAt(page, testInfo, location, reason) {
|
|
|
7678
9016
|
return async (ended) => {
|
|
7679
9017
|
console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
|
|
7680
9018
|
const why = await recordFromHere({ page, testInfo, location, reason, ended });
|
|
7681
|
-
if (ended?.aborted) console.log("[piwi] The recording ended with the
|
|
9019
|
+
if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
|
|
7682
9020
|
else
|
|
7683
9021
|
console.log(
|
|
7684
9022
|
why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
|
|
@@ -7695,10 +9033,10 @@ async function pauseInPlace(page, playwrightPause, location) {
|
|
|
7695
9033
|
if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
|
|
7696
9034
|
return leaveToPlaywright(context, playwrightPause);
|
|
7697
9035
|
}
|
|
7698
|
-
sink.pause ??=
|
|
9036
|
+
sink.pause ??= pauseState(NO_BREAKPOINTS);
|
|
7699
9037
|
watchStops(context, onDebuggerStop);
|
|
7700
9038
|
await steerDebugger(context, "pause");
|
|
7701
|
-
PAGE_PAUSES.set(context, location);
|
|
9039
|
+
PAGE_PAUSES.set(context, { location, page });
|
|
7702
9040
|
try {
|
|
7703
9041
|
await playwrightPause({ __testHookKeepTestTimeout: true });
|
|
7704
9042
|
} finally {
|
|
@@ -7881,15 +9219,15 @@ function instrumentPage(page) {
|
|
|
7881
9219
|
if (typeof page.close === "function") {
|
|
7882
9220
|
const originalClose = page.close.bind(page);
|
|
7883
9221
|
page.close = async (...args) => {
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
await
|
|
7889
|
-
await
|
|
7890
|
-
|
|
9222
|
+
let closed = () => {
|
|
9223
|
+
};
|
|
9224
|
+
CLOSING_PAGES.set(page, new Promise((resolve5) => closed = resolve5));
|
|
9225
|
+
try {
|
|
9226
|
+
await settleBeforeClose({ page });
|
|
9227
|
+
return await originalClose(...args);
|
|
9228
|
+
} finally {
|
|
9229
|
+
closed();
|
|
7891
9230
|
}
|
|
7892
|
-
return originalClose(...args);
|
|
7893
9231
|
};
|
|
7894
9232
|
}
|
|
7895
9233
|
const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
|
|
@@ -7934,7 +9272,7 @@ function instrumentPage(page) {
|
|
|
7934
9272
|
});
|
|
7935
9273
|
page.on("console", (msg) => {
|
|
7936
9274
|
const sink = currentSink;
|
|
7937
|
-
if (!sink) return;
|
|
9275
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
7938
9276
|
const type = msg.type();
|
|
7939
9277
|
if (["warning", "error", "assert"].includes(type)) {
|
|
7940
9278
|
const location = msg.location();
|
|
@@ -7950,7 +9288,7 @@ function instrumentPage(page) {
|
|
|
7950
9288
|
try {
|
|
7951
9289
|
page.on("dialogclosed", (dialog) => {
|
|
7952
9290
|
const sink = currentSink;
|
|
7953
|
-
if (!sink) return;
|
|
9291
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
7954
9292
|
try {
|
|
7955
9293
|
sink.dialogs.push({
|
|
7956
9294
|
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
@@ -7969,12 +9307,14 @@ function instrumentPage(page) {
|
|
|
7969
9307
|
if (!sink) return;
|
|
7970
9308
|
try {
|
|
7971
9309
|
const request = response.request();
|
|
9310
|
+
if (HELD_REQUESTS.has(request)) return;
|
|
7972
9311
|
if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
|
|
7973
9312
|
} catch {
|
|
7974
9313
|
}
|
|
7975
9314
|
});
|
|
7976
9315
|
page.on("request", (request) => {
|
|
7977
9316
|
const sink = currentSink;
|
|
9317
|
+
if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request);
|
|
7978
9318
|
if (!sink || sink.released.length === 0) return;
|
|
7979
9319
|
sink.releasedRequests.add(request);
|
|
7980
9320
|
sink.releasedQuietSince = Date.now();
|
|
@@ -7985,16 +9325,18 @@ function instrumentPage(page) {
|
|
|
7985
9325
|
sink.releasedQuietSince = Date.now();
|
|
7986
9326
|
if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
|
|
7987
9327
|
}
|
|
7988
|
-
if (!sink || RECORDED_OPEN.has(request)) return;
|
|
9328
|
+
if (!sink || RECORDED_OPEN.has(request) || HELD_REQUESTS.has(request)) return;
|
|
7989
9329
|
sink.lastActivePage = page;
|
|
7990
9330
|
const response = sink.openResponses.get(request) ?? null;
|
|
7991
9331
|
sink.openResponses.delete(request);
|
|
7992
9332
|
recordRequest(sink, request, failed ? "failed" : "finished", response);
|
|
9333
|
+
if (!failed) void keepResponse(request, response);
|
|
7993
9334
|
};
|
|
7994
9335
|
page.on("requestfinished", (request) => onRequestEnd(request, false));
|
|
7995
9336
|
page.on("requestfailed", (request) => onRequestEnd(request, true));
|
|
7996
9337
|
}
|
|
7997
9338
|
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
9339
|
+
var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
7998
9340
|
function isTrackedRequest(url, resourceType) {
|
|
7999
9341
|
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
8000
9342
|
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
@@ -8056,6 +9398,7 @@ function readResponseHeaders(sink, entry, url, headers) {
|
|
|
8056
9398
|
}
|
|
8057
9399
|
}
|
|
8058
9400
|
function recordApiCall(sink, call) {
|
|
9401
|
+
if (sink.frozenAt !== null) return;
|
|
8059
9402
|
try {
|
|
8060
9403
|
const entry = apiCallEntry(call, sink.baseURL);
|
|
8061
9404
|
if (!entry) return;
|
|
@@ -8092,7 +9435,10 @@ function instrumentContext(context) {
|
|
|
8092
9435
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
8093
9436
|
if (typeof context.once === "function") {
|
|
8094
9437
|
OPEN_CONTEXTS.add(context);
|
|
8095
|
-
context.once("close", () =>
|
|
9438
|
+
context.once("close", () => {
|
|
9439
|
+
OPEN_CONTEXTS.delete(context);
|
|
9440
|
+
CLOSING_CONTEXTS.add(context);
|
|
9441
|
+
});
|
|
8096
9442
|
}
|
|
8097
9443
|
observeOwnApiRequests(context);
|
|
8098
9444
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
@@ -8110,14 +9456,7 @@ function instrumentContext(context) {
|
|
|
8110
9456
|
if (typeof context.close === "function") {
|
|
8111
9457
|
const originalClose = context.close.bind(context);
|
|
8112
9458
|
context.close = async (...args) => {
|
|
8113
|
-
await
|
|
8114
|
-
const sink = currentSink;
|
|
8115
|
-
if (sink) {
|
|
8116
|
-
await endShownStops(sink, "closed", { context });
|
|
8117
|
-
await stashPageState(sink, { context });
|
|
8118
|
-
await maybeOpenPicker(sink, { context });
|
|
8119
|
-
await stopSinkCodeReach(sink, { context });
|
|
8120
|
-
}
|
|
9459
|
+
await settleContextClose(context);
|
|
8121
9460
|
return originalClose(...args);
|
|
8122
9461
|
};
|
|
8123
9462
|
}
|
|
@@ -8130,7 +9469,7 @@ function patchBrowser(browser) {
|
|
|
8130
9469
|
browser.newPage = async (...args) => {
|
|
8131
9470
|
const page = await originalNewPage(...args);
|
|
8132
9471
|
instrumentPage(page);
|
|
8133
|
-
await
|
|
9472
|
+
await armStops([pageContext(page)]);
|
|
8134
9473
|
await awaitFlakeConditions();
|
|
8135
9474
|
await maybeStartCodeReach(page);
|
|
8136
9475
|
return page;
|
|
@@ -8139,13 +9478,16 @@ function patchBrowser(browser) {
|
|
|
8139
9478
|
browser.newContext = async (...args) => {
|
|
8140
9479
|
const context = await originalNewContext(...args);
|
|
8141
9480
|
instrumentContext(context);
|
|
8142
|
-
await
|
|
9481
|
+
await armStops([context]);
|
|
8143
9482
|
return context;
|
|
8144
9483
|
};
|
|
8145
9484
|
if (typeof browser.close === "function") {
|
|
8146
9485
|
const originalClose = browser.close.bind(browser);
|
|
8147
9486
|
browser.close = async (...args) => {
|
|
8148
9487
|
await drainPendingProbes(1e3);
|
|
9488
|
+
for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
|
|
9489
|
+
await settleContextClose(context);
|
|
9490
|
+
}
|
|
8149
9491
|
return originalClose(...args);
|
|
8150
9492
|
};
|
|
8151
9493
|
}
|
|
@@ -8198,7 +9540,11 @@ async function flushSink(sink, testInfo) {
|
|
|
8198
9540
|
clearTimeout(drainDeadline);
|
|
8199
9541
|
const page = sink.lastActivePage;
|
|
8200
9542
|
const pageReadable = page !== null && !isPageClosed(page);
|
|
8201
|
-
if (pageReadable)
|
|
9543
|
+
if (pageReadable) {
|
|
9544
|
+
await holdFailure(sink);
|
|
9545
|
+
await maybeOpenPicker(sink);
|
|
9546
|
+
}
|
|
9547
|
+
const live = pageReadable && sink.frozenAt === null;
|
|
8202
9548
|
if (sink.stops.length > 0) {
|
|
8203
9549
|
await testInfo.attach(ATTACHMENT_NAMES.pauses, {
|
|
8204
9550
|
contentType: "application/json",
|
|
@@ -8266,13 +9612,13 @@ async function flushSink(sink, testInfo) {
|
|
|
8266
9612
|
}
|
|
8267
9613
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
8268
9614
|
try {
|
|
8269
|
-
const snapshot = (
|
|
9615
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8270
9616
|
if (snapshot) {
|
|
8271
9617
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8272
|
-
contentType:
|
|
9618
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
8273
9619
|
body: snapshot
|
|
8274
9620
|
});
|
|
8275
|
-
const snapshotJson = (
|
|
9621
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8276
9622
|
if (snapshotJson) {
|
|
8277
9623
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8278
9624
|
contentType: "application/json",
|
|
@@ -8297,7 +9643,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8297
9643
|
}
|
|
8298
9644
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
8299
9645
|
const option = sink.failureScreenshot;
|
|
8300
|
-
const shot = (
|
|
9646
|
+
const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
8301
9647
|
if (shot) {
|
|
8302
9648
|
try {
|
|
8303
9649
|
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
@@ -8307,13 +9653,13 @@ async function flushSink(sink, testInfo) {
|
|
|
8307
9653
|
}
|
|
8308
9654
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
8309
9655
|
try {
|
|
8310
|
-
const snapshot = (
|
|
9656
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
8311
9657
|
if (snapshot) {
|
|
8312
9658
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
8313
|
-
contentType:
|
|
9659
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
8314
9660
|
body: snapshot
|
|
8315
9661
|
});
|
|
8316
|
-
const snapshotJson = (
|
|
9662
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
8317
9663
|
if (snapshotJson) {
|
|
8318
9664
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
8319
9665
|
contentType: "application/json",
|
|
@@ -8359,7 +9705,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8359
9705
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
8360
9706
|
});
|
|
8361
9707
|
}
|
|
8362
|
-
const webVitals = (
|
|
9708
|
+
const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
8363
9709
|
if (webVitals) {
|
|
8364
9710
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
8365
9711
|
contentType: "application/json",
|
|
@@ -8367,7 +9713,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8367
9713
|
});
|
|
8368
9714
|
}
|
|
8369
9715
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
8370
|
-
const pageState = (
|
|
9716
|
+
const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
8371
9717
|
if (pageState) {
|
|
8372
9718
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
8373
9719
|
contentType: "application/json",
|
|
@@ -8376,7 +9722,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8376
9722
|
}
|
|
8377
9723
|
}
|
|
8378
9724
|
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
8379
|
-
if (
|
|
9725
|
+
if (live) await recordPageWindow(sink, page);
|
|
8380
9726
|
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
8381
9727
|
if (windows.length > 0) {
|
|
8382
9728
|
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
@@ -8386,21 +9732,79 @@ async function flushSink(sink, testInfo) {
|
|
|
8386
9732
|
}
|
|
8387
9733
|
}
|
|
8388
9734
|
}
|
|
9735
|
+
var pauseNextTest = false;
|
|
9736
|
+
var runControl = {
|
|
9737
|
+
pause() {
|
|
9738
|
+
const sink = currentSink;
|
|
9739
|
+
if (!sink) {
|
|
9740
|
+
pauseNextTest = true;
|
|
9741
|
+
return;
|
|
9742
|
+
}
|
|
9743
|
+
if (!sink.pause) {
|
|
9744
|
+
const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo), channel: true }) : null;
|
|
9745
|
+
tellHub({ type: "notice", message: `This test does not stop${why ? `: ${why}` : ""}.` });
|
|
9746
|
+
return;
|
|
9747
|
+
}
|
|
9748
|
+
sink.pause.pause = true;
|
|
9749
|
+
void armStops(OPEN_CONTEXTS);
|
|
9750
|
+
},
|
|
9751
|
+
breakpoints(places) {
|
|
9752
|
+
const set = replacePauseSet(places);
|
|
9753
|
+
const state = currentSink?.pause;
|
|
9754
|
+
if (!state) return;
|
|
9755
|
+
state.set = set;
|
|
9756
|
+
void armStops(OPEN_CONTEXTS);
|
|
9757
|
+
},
|
|
9758
|
+
runTo(place) {
|
|
9759
|
+
const state = currentSink?.pause;
|
|
9760
|
+
if (!state) return;
|
|
9761
|
+
state.runTo = placeLine(place);
|
|
9762
|
+
void armStops(OPEN_CONTEXTS);
|
|
9763
|
+
}
|
|
9764
|
+
};
|
|
9765
|
+
function runFile(file) {
|
|
9766
|
+
return path14.relative(process.cwd(), file).split(path14.sep).join("/") || file;
|
|
9767
|
+
}
|
|
9768
|
+
function testBegin(testInfo, cannotPause) {
|
|
9769
|
+
const title = testInfo.titlePath.slice(1);
|
|
9770
|
+
return {
|
|
9771
|
+
type: "test-begin",
|
|
9772
|
+
testId: testInfo.testId,
|
|
9773
|
+
title: title.length ? title : [testInfo.title],
|
|
9774
|
+
file: runFile(testInfo.file),
|
|
9775
|
+
line: Math.max(1, testInfo.line),
|
|
9776
|
+
project: testInfo.project.name,
|
|
9777
|
+
retry: testInfo.retry,
|
|
9778
|
+
cannotPause
|
|
9779
|
+
};
|
|
9780
|
+
}
|
|
9781
|
+
function testEnd(sink, testInfo) {
|
|
9782
|
+
return {
|
|
9783
|
+
type: "test-end",
|
|
9784
|
+
testId: testInfo.testId,
|
|
9785
|
+
status: testInfo.status ?? "passed",
|
|
9786
|
+
pausedMs: sink.stops.reduce((sum, stop) => sum + stop.duration, 0)
|
|
9787
|
+
};
|
|
9788
|
+
}
|
|
8389
9789
|
var piwiFixtures = {
|
|
8390
9790
|
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
8391
9791
|
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
8392
9792
|
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
8393
9793
|
// ledger and records the calls the tests make through it, and torn down after
|
|
8394
9794
|
// the browser closed, so its census sees what only the worker's shutdown
|
|
8395
|
-
// closed. Depends on `playwright` alone, which launches nothing.
|
|
9795
|
+
// closed. Depends on `playwright` alone, which launches nothing. The worker's
|
|
9796
|
+
// run connection to the editor (`PIWI_RUN_CHANNEL`) opens here, before the
|
|
9797
|
+
// first test, and closes as the worker ends.
|
|
8396
9798
|
piwiResources: [
|
|
8397
9799
|
async ({ playwright }, use, workerInfo) => {
|
|
8398
9800
|
boxCaptureFrames();
|
|
8399
9801
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
8400
9802
|
observeApiRequests(playwright, apiCallRecorder);
|
|
9803
|
+
await openRunChannel(workerInfo, runControl);
|
|
8401
9804
|
try {
|
|
8402
9805
|
await use();
|
|
8403
9806
|
} finally {
|
|
9807
|
+
closeRunChannel();
|
|
8404
9808
|
stopResourceLedger();
|
|
8405
9809
|
}
|
|
8406
9810
|
},
|
|
@@ -8420,10 +9824,12 @@ var piwiFixtures = {
|
|
|
8420
9824
|
// never double-wraps a page the browser patch already instrumented.
|
|
8421
9825
|
page: async ({ page }, use) => {
|
|
8422
9826
|
instrumentPage(page);
|
|
8423
|
-
await
|
|
9827
|
+
await armStops([pageContext(page)]);
|
|
8424
9828
|
await awaitFlakeConditions();
|
|
8425
9829
|
await maybeStartCodeReach(page);
|
|
8426
9830
|
await use(page);
|
|
9831
|
+
const sink = currentSink;
|
|
9832
|
+
if (sink) await holdFailure(sink, page);
|
|
8427
9833
|
},
|
|
8428
9834
|
// Auto, test-scoped: open a capture sink for the running test and flush it
|
|
8429
9835
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
@@ -8438,12 +9844,16 @@ var piwiFixtures = {
|
|
|
8438
9844
|
sink.baseURL = baseURL ?? null;
|
|
8439
9845
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
8440
9846
|
const pauseSet = currentPauseSet();
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
9847
|
+
const channel = runChannelOpen();
|
|
9848
|
+
const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo), channel });
|
|
9849
|
+
if (pauseSet.size || channel) {
|
|
9850
|
+
if (!cannotPause) sink.pause = pauseState(pauseSet, pauseNextTest);
|
|
9851
|
+
else if (pauseSet.size) logPauseSkipOnce(cannotPause);
|
|
8445
9852
|
}
|
|
9853
|
+
pauseNextTest = false;
|
|
8446
9854
|
if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
|
|
9855
|
+
if (channel) tellHub(testBegin(testInfo, cannotPause));
|
|
9856
|
+
if (channel && sink.pause) startKeeping();
|
|
8447
9857
|
if (isFlakeMode()) {
|
|
8448
9858
|
const plan = loadFlakePlan();
|
|
8449
9859
|
const role = flakeRoleForTest(plan, {
|
|
@@ -8467,14 +9877,18 @@ var piwiFixtures = {
|
|
|
8467
9877
|
});
|
|
8468
9878
|
activeResourceLedger()?.testStarted(testInfo);
|
|
8469
9879
|
currentSink = sink;
|
|
9880
|
+
const showRunChannel = hideRunChannel();
|
|
8470
9881
|
try {
|
|
8471
|
-
await
|
|
9882
|
+
await armStops(OPEN_CONTEXTS);
|
|
8472
9883
|
await startCodeReachOnOpenPages();
|
|
8473
9884
|
await use();
|
|
8474
9885
|
} finally {
|
|
9886
|
+
sink.ending = true;
|
|
8475
9887
|
await endShownStops(sink, "ended");
|
|
8476
9888
|
await awaitReleasedCalls(sink);
|
|
9889
|
+
showRunChannel();
|
|
8477
9890
|
currentSink = null;
|
|
9891
|
+
if (sink.pause?.pause) pauseNextTest = true;
|
|
8478
9892
|
if (sink.probeItem) {
|
|
8479
9893
|
const applied = sink.probeInterception?.applied() ?? false;
|
|
8480
9894
|
const level = sink.probeItem.level ?? "client";
|
|
@@ -8497,7 +9911,12 @@ var piwiFixtures = {
|
|
|
8497
9911
|
});
|
|
8498
9912
|
}
|
|
8499
9913
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
8500
|
-
|
|
9914
|
+
try {
|
|
9915
|
+
await flushSink(sink, testInfo);
|
|
9916
|
+
} finally {
|
|
9917
|
+
stopKeeping();
|
|
9918
|
+
if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
|
|
9919
|
+
}
|
|
8501
9920
|
await recordResourceCensus(testInfo);
|
|
8502
9921
|
}
|
|
8503
9922
|
},
|
|
@@ -8543,6 +9962,8 @@ function extendPiwiFixtures(test) {
|
|
|
8543
9962
|
computeCoreVitals,
|
|
8544
9963
|
configuredCodeReachRoots,
|
|
8545
9964
|
extendPiwiFixtures,
|
|
9965
|
+
failureError,
|
|
9966
|
+
intentAt,
|
|
8546
9967
|
internalCall,
|
|
8547
9968
|
noteLocatorCall,
|
|
8548
9969
|
noteNavigation,
|