@piwitests/reporter 0.60.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 +437 -229
- package/dist/global-setup-module.js +66 -9
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2534 -371
- package/dist/internal/capture/attachments.d.ts +26 -1
- package/dist/internal/capture/attachments.js +23 -2
- package/dist/internal/capture/capture-fixtures.d.ts +114 -6
- package/dist/internal/capture/capture-fixtures.js +2028 -236
- 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");
|
|
@@ -1678,7 +1680,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
|
|
|
1678
1680
|
function generateAlternatives(attrs, options = {}) {
|
|
1679
1681
|
const alts = [];
|
|
1680
1682
|
const seen = /* @__PURE__ */ new Set();
|
|
1681
|
-
const
|
|
1683
|
+
const add2 = (loc) => {
|
|
1682
1684
|
if (!seen.has(loc.locator)) {
|
|
1683
1685
|
seen.add(loc.locator);
|
|
1684
1686
|
alts.push(loc);
|
|
@@ -1695,7 +1697,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1695
1697
|
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
1696
1698
|
const testId = attr(attrs, testIdAttribute);
|
|
1697
1699
|
if (testId && isUnique(counts?.testId)) {
|
|
1698
|
-
|
|
1700
|
+
add2({
|
|
1699
1701
|
locator: `getByTestId(${quoteJsString(testId)})`,
|
|
1700
1702
|
method: "getByTestId",
|
|
1701
1703
|
args: { testId },
|
|
@@ -1706,7 +1708,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1706
1708
|
const levelPart = level != null ? `, level: ${level}` : "";
|
|
1707
1709
|
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
1708
1710
|
if (role && accessibleName) {
|
|
1709
|
-
|
|
1711
|
+
add2({
|
|
1710
1712
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
|
|
1711
1713
|
method: "getByRole",
|
|
1712
1714
|
args: withLevel({ role, name: accessibleName }),
|
|
@@ -1715,7 +1717,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1715
1717
|
}
|
|
1716
1718
|
const ariaLabel = attr(attrs, "aria-label");
|
|
1717
1719
|
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
1718
|
-
|
|
1720
|
+
add2({
|
|
1719
1721
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
|
|
1720
1722
|
method: "getByRole",
|
|
1721
1723
|
args: withLevel({ role, name: ariaLabel }),
|
|
@@ -1726,7 +1728,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1726
1728
|
const label = attr(attrs, "aria-label");
|
|
1727
1729
|
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
1728
1730
|
if (labelBacked) {
|
|
1729
|
-
|
|
1731
|
+
add2({
|
|
1730
1732
|
locator: `getByLabel(${quoteJsString(accessibleName)})`,
|
|
1731
1733
|
method: "getByLabel",
|
|
1732
1734
|
args: { label: accessibleName },
|
|
@@ -1736,7 +1738,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1736
1738
|
}
|
|
1737
1739
|
const placeholder = attr(attrs, "placeholder");
|
|
1738
1740
|
if (placeholder) {
|
|
1739
|
-
|
|
1741
|
+
add2({
|
|
1740
1742
|
locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
|
|
1741
1743
|
method: "getByPlaceholder",
|
|
1742
1744
|
args: { placeholder },
|
|
@@ -1744,7 +1746,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1744
1746
|
});
|
|
1745
1747
|
}
|
|
1746
1748
|
if (text && text.length < 80) {
|
|
1747
|
-
|
|
1749
|
+
add2({
|
|
1748
1750
|
locator: `getByText(${quoteJsString(text)})`,
|
|
1749
1751
|
method: "getByText",
|
|
1750
1752
|
args: { text },
|
|
@@ -1754,7 +1756,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1754
1756
|
const id = attr(attrs, "id");
|
|
1755
1757
|
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
1756
1758
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
1757
|
-
|
|
1759
|
+
add2({
|
|
1758
1760
|
locator: `locator(${quoteJsString(selector)})`,
|
|
1759
1761
|
method: "locator",
|
|
1760
1762
|
args: { selector },
|
|
@@ -1764,7 +1766,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1764
1766
|
const name = attr(attrs, "name");
|
|
1765
1767
|
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
1766
1768
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
1767
|
-
|
|
1769
|
+
add2({
|
|
1768
1770
|
locator: `locator(${quoteJsString(selector)})`,
|
|
1769
1771
|
method: "locator",
|
|
1770
1772
|
args: { selector },
|
|
@@ -1773,7 +1775,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1773
1775
|
}
|
|
1774
1776
|
const alt = attr(attrs, "alt");
|
|
1775
1777
|
if (alt) {
|
|
1776
|
-
|
|
1778
|
+
add2({
|
|
1777
1779
|
locator: `getByAltText(${quoteJsString(alt)})`,
|
|
1778
1780
|
method: "getByAltText",
|
|
1779
1781
|
args: { text: alt },
|
|
@@ -1782,7 +1784,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1782
1784
|
}
|
|
1783
1785
|
const title = attr(attrs, "title");
|
|
1784
1786
|
if (title) {
|
|
1785
|
-
|
|
1787
|
+
add2({
|
|
1786
1788
|
locator: `getByTitle(${quoteJsString(title)})`,
|
|
1787
1789
|
method: "getByTitle",
|
|
1788
1790
|
args: { title },
|
|
@@ -1800,7 +1802,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1800
1802
|
if (scopedCount(anc) !== 1) continue;
|
|
1801
1803
|
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
1802
1804
|
testIdAnchorDone = true;
|
|
1803
|
-
|
|
1805
|
+
add2({
|
|
1804
1806
|
locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
|
|
1805
1807
|
method: leaf.method,
|
|
1806
1808
|
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
@@ -1810,7 +1812,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1810
1812
|
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
1811
1813
|
idAnchorDone = true;
|
|
1812
1814
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
1813
|
-
|
|
1815
|
+
add2({
|
|
1814
1816
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
1815
1817
|
method: leaf.method,
|
|
1816
1818
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -1821,7 +1823,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1821
1823
|
dataAnchorDone = true;
|
|
1822
1824
|
const { name: name2, value } = anc.dataAttr;
|
|
1823
1825
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
1824
|
-
|
|
1826
|
+
add2({
|
|
1825
1827
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
1826
1828
|
method: leaf.method,
|
|
1827
1829
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -1831,7 +1833,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1831
1833
|
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
1832
1834
|
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
1833
1835
|
roleAnchorDone = true;
|
|
1834
|
-
|
|
1836
|
+
add2({
|
|
1835
1837
|
locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
|
|
1836
1838
|
method: leaf.method,
|
|
1837
1839
|
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
@@ -1840,7 +1842,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1840
1842
|
}
|
|
1841
1843
|
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
1842
1844
|
filterAnchorDone = true;
|
|
1843
|
-
|
|
1845
|
+
add2({
|
|
1844
1846
|
locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
|
|
1845
1847
|
method: leaf.method,
|
|
1846
1848
|
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
@@ -1859,7 +1861,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1859
1861
|
);
|
|
1860
1862
|
const pos = attrs.rolePosition;
|
|
1861
1863
|
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
1862
|
-
|
|
1864
|
+
add2({
|
|
1863
1865
|
locator: `getByRole(${rolePart})`,
|
|
1864
1866
|
method: "getByRole",
|
|
1865
1867
|
args: leafArgs,
|
|
@@ -1878,7 +1880,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
1878
1880
|
const countsClasses = counts?.classes;
|
|
1879
1881
|
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
1880
1882
|
for (const { cls, score } of classes) {
|
|
1881
|
-
|
|
1883
|
+
add2({
|
|
1882
1884
|
locator: `locator(${quoteJsString(`.${cls}`)})`,
|
|
1883
1885
|
method: "locator",
|
|
1884
1886
|
args: { selector: `.${cls}` },
|
|
@@ -1935,7 +1937,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
1935
1937
|
const seen = /* @__PURE__ */ new Set();
|
|
1936
1938
|
const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
|
|
1937
1939
|
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
1938
|
-
const
|
|
1940
|
+
const add2 = (l) => {
|
|
1939
1941
|
if (!seen.has(l.locator)) {
|
|
1940
1942
|
seen.add(l.locator);
|
|
1941
1943
|
out.push(l);
|
|
@@ -1945,7 +1947,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
1945
1947
|
if (anchor.scopedLeafCount !== 1) continue;
|
|
1946
1948
|
const seg = anchorSegment(anchor);
|
|
1947
1949
|
if (!seg) continue;
|
|
1948
|
-
|
|
1950
|
+
add2({
|
|
1949
1951
|
locator: `${seg.code}.getByRole(${rolePart})`,
|
|
1950
1952
|
method: "getByRole",
|
|
1951
1953
|
args: { ...leafArgs, ...seg.flatArgs },
|
|
@@ -1958,7 +1960,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
1958
1960
|
if (segments.every((s) => s !== null)) {
|
|
1959
1961
|
const innermost = segments[segments.length - 1];
|
|
1960
1962
|
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
1961
|
-
|
|
1963
|
+
add2({
|
|
1962
1964
|
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
1963
1965
|
method: "getByRole",
|
|
1964
1966
|
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
@@ -2401,6 +2403,8 @@ var ATTACHMENT_NAMES = {
|
|
|
2401
2403
|
codeReach: "piwi-code-reach",
|
|
2402
2404
|
browserCoverage: "piwi-browser-coverage",
|
|
2403
2405
|
browserFunctions: "piwi-browser-functions",
|
|
2406
|
+
cssCoverage: "piwi-css-coverage",
|
|
2407
|
+
cssRules: "piwi-css-rules",
|
|
2404
2408
|
serverCoverageRequests: "piwi-server-coverage-requests",
|
|
2405
2409
|
webVitals: "piwi-web-vitals",
|
|
2406
2410
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -2412,6 +2416,7 @@ var ATTACHMENT_NAMES = {
|
|
|
2412
2416
|
pauses: "piwi-pauses"
|
|
2413
2417
|
};
|
|
2414
2418
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
2419
|
+
var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
2415
2420
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
2416
2421
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
2417
2422
|
|
|
@@ -2718,6 +2723,7 @@ var PIWI_ENV_KEYS = {
|
|
|
2718
2723
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2719
2724
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
2720
2725
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
2726
|
+
coverageCss: "PIWI_COVERAGE_CSS",
|
|
2721
2727
|
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
2722
2728
|
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
2723
2729
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
@@ -2743,6 +2749,10 @@ var PIWI_LOCAL_DEBUG_ENV = {
|
|
|
2743
2749
|
pauseAt: "PIWI_PAUSE_AT",
|
|
2744
2750
|
editorSend: "PIWI_EDITOR_SEND"
|
|
2745
2751
|
};
|
|
2752
|
+
var PIWI_ORIGIN_ENV = {
|
|
2753
|
+
kind: "PIWI_ORIGIN",
|
|
2754
|
+
ref: "PIWI_ORIGIN_REF"
|
|
2755
|
+
};
|
|
2746
2756
|
var PIWI_PROBE_ENV = {
|
|
2747
2757
|
flag: "PIWI_PROBE",
|
|
2748
2758
|
plan: "PIWI_PROBE_PLAN",
|
|
@@ -2872,9 +2882,9 @@ function loadPlan() {
|
|
|
2872
2882
|
function probeItemForTest(test) {
|
|
2873
2883
|
const plan = loadPlan();
|
|
2874
2884
|
if (!plan) return null;
|
|
2875
|
-
const
|
|
2885
|
+
const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
|
|
2876
2886
|
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
2877
|
-
return matchProbeItem(plan, { title: test.title, filePath, suitePath });
|
|
2887
|
+
return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
|
|
2878
2888
|
}
|
|
2879
2889
|
function classifyProbeHandled(signals) {
|
|
2880
2890
|
const blank = signals.blankPage === true;
|
|
@@ -3807,10 +3817,10 @@ function joinRoot(root, source) {
|
|
|
3807
3817
|
function decodeLines(raw) {
|
|
3808
3818
|
const sources = [];
|
|
3809
3819
|
const decoded = [];
|
|
3810
|
-
const
|
|
3820
|
+
const add2 = (map, lineOffset, columnOffset) => {
|
|
3811
3821
|
if (map.sections) {
|
|
3812
3822
|
for (const section of map.sections) {
|
|
3813
|
-
if (!
|
|
3823
|
+
if (!add2(section.map, section.offset.line, section.offset.column)) return false;
|
|
3814
3824
|
}
|
|
3815
3825
|
return true;
|
|
3816
3826
|
}
|
|
@@ -3831,7 +3841,7 @@ function decodeLines(raw) {
|
|
|
3831
3841
|
});
|
|
3832
3842
|
return true;
|
|
3833
3843
|
};
|
|
3834
|
-
return
|
|
3844
|
+
return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
|
|
3835
3845
|
}
|
|
3836
3846
|
function compactSegments(lines) {
|
|
3837
3847
|
let total = 0;
|
|
@@ -4007,7 +4017,7 @@ function functionLines(entry, map, text, all) {
|
|
|
4007
4017
|
const read = textOf(entry, text);
|
|
4008
4018
|
const ranges = executedFunctionRanges(entry, read?.length, all);
|
|
4009
4019
|
if (!ranges.length || !read) return out;
|
|
4010
|
-
const
|
|
4020
|
+
const add2 = (source, range) => {
|
|
4011
4021
|
const list = out.get(source);
|
|
4012
4022
|
if (list) list.push(range);
|
|
4013
4023
|
else out.set(source, [range]);
|
|
@@ -4017,7 +4027,7 @@ function functionLines(entry, map, text, all) {
|
|
|
4017
4027
|
const start = offsetToPosition(starts, range.start);
|
|
4018
4028
|
if (!map) {
|
|
4019
4029
|
const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
|
|
4020
|
-
|
|
4030
|
+
add2(-1, [start.line + 1, last.line + 1]);
|
|
4021
4031
|
continue;
|
|
4022
4032
|
}
|
|
4023
4033
|
const at = map.originalAt(start.line, start.column);
|
|
@@ -4027,7 +4037,7 @@ function functionLines(entry, map, text, all) {
|
|
|
4027
4037
|
if (first === null) continue;
|
|
4028
4038
|
const header = first === at.line || first === at.line + 1 ? at.line : first;
|
|
4029
4039
|
const lines = map.originalLinesBetween(at.source, start, end, header);
|
|
4030
|
-
if (lines)
|
|
4040
|
+
if (lines) add2(at.source, [header + 1, lines[1] + 1]);
|
|
4031
4041
|
}
|
|
4032
4042
|
return out;
|
|
4033
4043
|
}
|
|
@@ -4063,6 +4073,258 @@ function maskUrlFragment(hash) {
|
|
|
4063
4073
|
const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
|
|
4064
4074
|
return hash.startsWith("#") ? `#${out}` : out;
|
|
4065
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
|
+
}
|
|
4066
4328
|
|
|
4067
4329
|
// src/internal/capture/inspect-on-failure.ts
|
|
4068
4330
|
function isCi(ci) {
|
|
@@ -4318,6 +4580,7 @@ var path7 = __toESM(require("path"));
|
|
|
4318
4580
|
// src/internal/support/reporter-version.ts
|
|
4319
4581
|
var fs5 = __toESM(require("fs"));
|
|
4320
4582
|
var path6 = __toESM(require("path"));
|
|
4583
|
+
var cachedVersion = null;
|
|
4321
4584
|
var CANDIDATE_OFFSETS = ["..", "../../.."];
|
|
4322
4585
|
function findOwnPackageJson(fromDir) {
|
|
4323
4586
|
for (const offset of CANDIDATE_OFFSETS) {
|
|
@@ -4332,6 +4595,11 @@ function findOwnPackageJson(fromDir) {
|
|
|
4332
4595
|
}
|
|
4333
4596
|
return void 0;
|
|
4334
4597
|
}
|
|
4598
|
+
function getReporterVersion() {
|
|
4599
|
+
if (cachedVersion) return cachedVersion;
|
|
4600
|
+
cachedVersion = findOwnPackageJson(__dirname)?.version ?? "unknown";
|
|
4601
|
+
return cachedVersion;
|
|
4602
|
+
}
|
|
4335
4603
|
|
|
4336
4604
|
// src/internal/capture/quiet-capture.ts
|
|
4337
4605
|
var BOXED_PREFIX_EXPORTS = [
|
|
@@ -4457,11 +4725,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
|
|
|
4457
4725
|
return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
4458
4726
|
}
|
|
4459
4727
|
var parsed = null;
|
|
4728
|
+
var sent = null;
|
|
4460
4729
|
function currentPauseSet() {
|
|
4730
|
+
if (sent) return sent;
|
|
4461
4731
|
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
4462
4732
|
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
4463
4733
|
return parsed.set;
|
|
4464
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
|
+
}
|
|
4465
4753
|
function pauseGateFromTestInfo(testInfo) {
|
|
4466
4754
|
const use = testInfo.project?.use ?? {};
|
|
4467
4755
|
const debug = testInfo._configInternal?.configCLIOverrides?.debug;
|
|
@@ -4469,7 +4757,7 @@ function pauseGateFromTestInfo(testInfo) {
|
|
|
4469
4757
|
}
|
|
4470
4758
|
function pauseSkipReason(gate) {
|
|
4471
4759
|
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
4472
|
-
if (gate.headless !== false)
|
|
4760
|
+
if (gate.headless !== false && !gate.channel)
|
|
4473
4761
|
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
4474
4762
|
if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
|
|
4475
4763
|
return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
|
|
@@ -4477,20 +4765,42 @@ function pauseSkipReason(gate) {
|
|
|
4477
4765
|
return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
|
|
4478
4766
|
return null;
|
|
4479
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
|
+
}
|
|
4480
4777
|
function atStop(state, callSite) {
|
|
4481
|
-
if (callSite
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
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 };
|
|
4485
4783
|
}
|
|
4486
|
-
if (state.set.matches(callSite)) return { show: "breakpoint" };
|
|
4487
4784
|
}
|
|
4488
4785
|
return { go: goOn(state) };
|
|
4489
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
|
+
}
|
|
4490
4800
|
function goOn(state, answer) {
|
|
4491
4801
|
if (answer === "step") state.step = true;
|
|
4492
4802
|
else if (answer === "finish") state.finished = true;
|
|
4493
|
-
return
|
|
4803
|
+
return wantsStops(state) ? "next" : "resume";
|
|
4494
4804
|
}
|
|
4495
4805
|
function contextDebugger(context) {
|
|
4496
4806
|
const dbg = context.debugger;
|
|
@@ -4626,38 +4936,83 @@ function logPauseSkipOnce(reason) {
|
|
|
4626
4936
|
var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
|
|
4627
4937
|
var MAX_NAVIGATED = 10;
|
|
4628
4938
|
var HIGHLIGHT_MS = 2e3;
|
|
4629
|
-
|
|
4630
|
-
|
|
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;
|
|
4631
4984
|
try {
|
|
4632
4985
|
const highlight = point.target?.highlight;
|
|
4633
4986
|
if (typeof highlight === "function") {
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
4637
|
-
new Promise((resolve5) => {
|
|
4638
|
-
timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
|
|
4639
|
-
})
|
|
4640
|
-
]);
|
|
4641
|
-
clearTimeout(timer);
|
|
4987
|
+
const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
|
|
4988
|
+
shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
|
|
4642
4989
|
}
|
|
4643
4990
|
const arg = {
|
|
4644
4991
|
place: pausePlace(point.location),
|
|
4645
4992
|
attempt: point.attempt,
|
|
4646
4993
|
action: point.call,
|
|
4647
4994
|
locator: null,
|
|
4648
|
-
record: !!onRecord
|
|
4995
|
+
record: !!onRecord,
|
|
4996
|
+
...point.failure ? { failure: true } : {}
|
|
4649
4997
|
};
|
|
4650
4998
|
console.log(
|
|
4651
|
-
`[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).`
|
|
4652
5000
|
);
|
|
4653
5001
|
let navigated = 0;
|
|
4654
5002
|
for (; ; ) {
|
|
4655
5003
|
try {
|
|
4656
5004
|
if (ended?.aborted) return "resume";
|
|
4657
|
-
await page.evaluate(showPauseBar, arg);
|
|
4658
|
-
|
|
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);
|
|
4659
5015
|
if (ended?.aborted) return "resume";
|
|
4660
|
-
const choice = await page.evaluate(takePauseState);
|
|
4661
5016
|
navigated = 0;
|
|
4662
5017
|
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
4663
5018
|
if (choice === "record" && onRecord) {
|
|
@@ -4667,26 +5022,285 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
|
|
|
4667
5022
|
);
|
|
4668
5023
|
continue;
|
|
4669
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
|
+
}
|
|
4670
5032
|
if (choice !== "pick") continue;
|
|
4671
5033
|
const picked = await pickLocator(page, probe, null, ended).catch(() => null);
|
|
4672
5034
|
if (picked && !ended?.aborted) await onPick(picked);
|
|
4673
5035
|
} catch (error) {
|
|
4674
5036
|
if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
|
|
4675
|
-
await
|
|
5037
|
+
await unlessEnded(
|
|
5038
|
+
page.waitForLoadState("domcontentloaded").catch(() => void 0),
|
|
5039
|
+
ended
|
|
5040
|
+
);
|
|
4676
5041
|
}
|
|
4677
5042
|
}
|
|
4678
5043
|
} catch {
|
|
4679
5044
|
return "resume";
|
|
4680
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('"')) {
|
|
4681
5133
|
try {
|
|
4682
|
-
|
|
4683
|
-
const dispose = shown?.dispose;
|
|
4684
|
-
const hide = page.hideHighlight;
|
|
4685
|
-
if (typeof dispose === "function") await dispose.call(shown);
|
|
4686
|
-
else if (typeof hide === "function") await hide.call(page);
|
|
5134
|
+
return String(JSON.parse(value));
|
|
4687
5135
|
} catch {
|
|
5136
|
+
return value;
|
|
4688
5137
|
}
|
|
4689
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 };
|
|
4690
5304
|
}
|
|
4691
5305
|
|
|
4692
5306
|
// src/internal/capture/record-handoff.ts
|
|
@@ -4707,7 +5321,7 @@ function parseRecordChannel(value) {
|
|
|
4707
5321
|
const path15 = text.slice(0, hash);
|
|
4708
5322
|
return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
|
|
4709
5323
|
}
|
|
4710
|
-
function
|
|
5324
|
+
function isObject3(value) {
|
|
4711
5325
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4712
5326
|
}
|
|
4713
5327
|
function parseHandoffToTest(line) {
|
|
@@ -4717,7 +5331,7 @@ function parseHandoffToTest(line) {
|
|
|
4717
5331
|
} catch {
|
|
4718
5332
|
return null;
|
|
4719
5333
|
}
|
|
4720
|
-
if (!
|
|
5334
|
+
if (!isObject3(value)) return null;
|
|
4721
5335
|
switch (value.type) {
|
|
4722
5336
|
case "refused":
|
|
4723
5337
|
return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
|
|
@@ -4780,8 +5394,8 @@ async function recordFromHere(options) {
|
|
|
4780
5394
|
const use = testInfo.project?.use ?? {};
|
|
4781
5395
|
const context = page.context();
|
|
4782
5396
|
return new Promise((resolve5) => {
|
|
4783
|
-
const
|
|
4784
|
-
|
|
5397
|
+
const socket2 = net.connect(channel.path);
|
|
5398
|
+
socket2.setEncoding("utf8");
|
|
4785
5399
|
const answers = /* @__PURE__ */ new Map();
|
|
4786
5400
|
let calls = 0;
|
|
4787
5401
|
let attached = false;
|
|
@@ -4789,7 +5403,7 @@ async function recordFromHere(options) {
|
|
|
4789
5403
|
let settled = false;
|
|
4790
5404
|
let initScript;
|
|
4791
5405
|
const send = (message) => {
|
|
4792
|
-
if (!
|
|
5406
|
+
if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
|
|
4793
5407
|
`);
|
|
4794
5408
|
};
|
|
4795
5409
|
const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
|
|
@@ -4839,7 +5453,7 @@ async function recordFromHere(options) {
|
|
|
4839
5453
|
page.off("close", onGone);
|
|
4840
5454
|
page.off("crash", onGone);
|
|
4841
5455
|
ended?.removeEventListener("abort", onEnded);
|
|
4842
|
-
|
|
5456
|
+
socket2.end();
|
|
4843
5457
|
resolve5(recording ? null : said);
|
|
4844
5458
|
};
|
|
4845
5459
|
const relayed = (source, payload) => new Promise((answer) => {
|
|
@@ -4879,7 +5493,7 @@ async function recordFromHere(options) {
|
|
|
4879
5493
|
context.on("page", onPage);
|
|
4880
5494
|
send({ type: "loaded" });
|
|
4881
5495
|
};
|
|
4882
|
-
|
|
5496
|
+
socket2.on("connect", () => {
|
|
4883
5497
|
attached = true;
|
|
4884
5498
|
send({
|
|
4885
5499
|
type: "attach",
|
|
@@ -4888,10 +5502,11 @@ async function recordFromHere(options) {
|
|
|
4888
5502
|
line: place.line,
|
|
4889
5503
|
reason: options.reason,
|
|
4890
5504
|
testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
|
|
4891
|
-
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 } : {}
|
|
4892
5507
|
});
|
|
4893
5508
|
});
|
|
4894
|
-
|
|
5509
|
+
socket2.on(
|
|
4895
5510
|
"data",
|
|
4896
5511
|
lineReader(
|
|
4897
5512
|
(line) => {
|
|
@@ -4924,16 +5539,263 @@ async function recordFromHere(options) {
|
|
|
4924
5539
|
() => finish("the editor sent a message too long to read.")
|
|
4925
5540
|
)
|
|
4926
5541
|
);
|
|
4927
|
-
|
|
4928
|
-
|
|
5542
|
+
socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
|
|
5543
|
+
socket2.on("close", () => finish("the editor closed the recording."));
|
|
4929
5544
|
});
|
|
4930
5545
|
}
|
|
4931
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
|
+
|
|
4932
5791
|
// src/internal/collect/coverage-settings.ts
|
|
4933
5792
|
var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
|
|
4934
5793
|
function isServerGranularity(value) {
|
|
4935
5794
|
return typeof value === "string" && SERVER_VALUES.has(value);
|
|
4936
5795
|
}
|
|
5796
|
+
function cssCoverageInWorker() {
|
|
5797
|
+
return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
|
|
5798
|
+
}
|
|
4937
5799
|
function browserCoverageInWorker() {
|
|
4938
5800
|
const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
|
|
4939
5801
|
return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || browser === "files" || browser === "functions";
|
|
@@ -4947,7 +5809,7 @@ function browserFunctionCoverage(files) {
|
|
|
4947
5809
|
files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
|
|
4948
5810
|
});
|
|
4949
5811
|
}
|
|
4950
|
-
function newKnownFunctions(known,
|
|
5812
|
+
function newKnownFunctions(known, sent2) {
|
|
4951
5813
|
const parts = [];
|
|
4952
5814
|
let files = [];
|
|
4953
5815
|
let budget = MAX_COVERAGE_RANGES;
|
|
@@ -4959,10 +5821,10 @@ function newKnownFunctions(known, sent) {
|
|
|
4959
5821
|
for (const [raw, ranges] of known) {
|
|
4960
5822
|
const path15 = normalizeCoveragePath(raw);
|
|
4961
5823
|
if (path15 === null) continue;
|
|
4962
|
-
let named =
|
|
5824
|
+
let named = sent2.get(path15);
|
|
4963
5825
|
let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
|
|
4964
5826
|
if (fresh.length === 0) continue;
|
|
4965
|
-
if (!named)
|
|
5827
|
+
if (!named) sent2.set(path15, named = /* @__PURE__ */ new Set());
|
|
4966
5828
|
for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
|
|
4967
5829
|
while (fresh.length > 0) {
|
|
4968
5830
|
if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
|
|
@@ -4985,15 +5847,15 @@ var os3 = __toESM(require("os"));
|
|
|
4985
5847
|
var fs7 = __toESM(require("fs"));
|
|
4986
5848
|
|
|
4987
5849
|
// src/internal/support/instance-id.ts
|
|
4988
|
-
var
|
|
5850
|
+
var crypto2 = __toESM(require("crypto"));
|
|
4989
5851
|
var os2 = __toESM(require("os"));
|
|
4990
5852
|
function hashForProject(projectName) {
|
|
4991
|
-
return
|
|
5853
|
+
return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
4992
5854
|
}
|
|
4993
5855
|
|
|
4994
5856
|
// src/internal/support/aria-sampling.ts
|
|
4995
|
-
function ariaSampleIdentity(
|
|
4996
|
-
return `${
|
|
5857
|
+
function ariaSampleIdentity(filePath2, title) {
|
|
5858
|
+
return `${filePath2}\0${title}`;
|
|
4997
5859
|
}
|
|
4998
5860
|
function getAriaSampleFilePath(projectName) {
|
|
4999
5861
|
return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
@@ -5025,7 +5887,7 @@ function isDueForAriaSample(testInfo) {
|
|
|
5025
5887
|
}
|
|
5026
5888
|
|
|
5027
5889
|
// src/internal/capture/code-reach.ts
|
|
5028
|
-
var
|
|
5890
|
+
var crypto3 = __toESM(require("crypto"));
|
|
5029
5891
|
var fs8 = __toESM(require("fs"));
|
|
5030
5892
|
var path11 = __toESM(require("path"));
|
|
5031
5893
|
var import_node_child_process = require("child_process");
|
|
@@ -5113,9 +5975,9 @@ function resolveSourcePath(source, roots) {
|
|
|
5113
5975
|
const normalized = normalizeSourcePath(source);
|
|
5114
5976
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
5115
5977
|
if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
|
|
5116
|
-
const
|
|
5978
|
+
const relative8 = normalized.replace(/^\/+/, "");
|
|
5117
5979
|
for (const root of roots) {
|
|
5118
|
-
const candidate = path11.resolve(root,
|
|
5980
|
+
const candidate = path11.resolve(root, relative8);
|
|
5119
5981
|
if (isFile(candidate)) return candidate;
|
|
5120
5982
|
}
|
|
5121
5983
|
return null;
|
|
@@ -5150,7 +6012,7 @@ function relativeToRepo(file, repoRoot) {
|
|
|
5150
6012
|
return path11.relative(repoRoot, file).split(path11.sep).join("/");
|
|
5151
6013
|
}
|
|
5152
6014
|
function scriptVersion(entry) {
|
|
5153
|
-
return entry.hash ??
|
|
6015
|
+
return entry.hash ?? crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
5154
6016
|
}
|
|
5155
6017
|
async function loadMap(entry, fetchMap) {
|
|
5156
6018
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -5193,7 +6055,7 @@ async function resolveScript(entry, roots, bases, fetchMap) {
|
|
|
5193
6055
|
if (text) scriptCache.set(entry.url, version, resolution);
|
|
5194
6056
|
return resolution;
|
|
5195
6057
|
}
|
|
5196
|
-
function
|
|
6058
|
+
function textOf3(entry, resolution) {
|
|
5197
6059
|
return resolution.text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
|
|
5198
6060
|
}
|
|
5199
6061
|
async function resolveCodeReach(entries, roots, fetchMap, pageUrl) {
|
|
@@ -5215,7 +6077,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5215
6077
|
if (listed) listed.push(...functions);
|
|
5216
6078
|
else into.set(path15, [...functions]);
|
|
5217
6079
|
};
|
|
5218
|
-
const
|
|
6080
|
+
const add2 = (file, functions) => addTo(files, file, functions);
|
|
5219
6081
|
const bases = viteBases(entries);
|
|
5220
6082
|
const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
|
|
5221
6083
|
const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
|
|
@@ -5239,17 +6101,17 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5239
6101
|
};
|
|
5240
6102
|
if (resolution.kind === "file") {
|
|
5241
6103
|
if (!resolution.file) continue;
|
|
5242
|
-
const text =
|
|
6104
|
+
const text = textOf3(covered, resolution);
|
|
5243
6105
|
if (resolution.lookup) {
|
|
5244
6106
|
const reached = reachedFunctionLines(covered, resolution.lookup, text);
|
|
5245
|
-
if (reached.size > 0)
|
|
6107
|
+
if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
|
|
5246
6108
|
} else if (scriptRan(covered, text)) {
|
|
5247
|
-
|
|
6109
|
+
add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
|
|
5248
6110
|
}
|
|
5249
6111
|
} else if (resolution.kind === "map") {
|
|
5250
|
-
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup,
|
|
6112
|
+
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf3(covered, resolution))) {
|
|
5251
6113
|
const file = mapFile(resolution, index);
|
|
5252
|
-
if (file)
|
|
6114
|
+
if (file) add2(file, functions);
|
|
5253
6115
|
}
|
|
5254
6116
|
}
|
|
5255
6117
|
if (!withKnown || resolution.kind === "none") continue;
|
|
@@ -5257,7 +6119,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
5257
6119
|
let listed = knownCache.get(covered.url, version);
|
|
5258
6120
|
if (!listed || listed.count < covered.functions.length) {
|
|
5259
6121
|
const byFile = /* @__PURE__ */ new Map();
|
|
5260
|
-
const text =
|
|
6122
|
+
const text = textOf3(covered, resolution);
|
|
5261
6123
|
if (resolution.kind === "file" && resolution.file) {
|
|
5262
6124
|
const lines = knownFunctionLines(covered, resolution.lookup, text);
|
|
5263
6125
|
const all = resolution.lookup ? [...lines.values()].flat() : lines.get(-1) ?? [];
|
|
@@ -5328,10 +6190,13 @@ async function markCurrentDocument(page, cdp) {
|
|
|
5328
6190
|
}
|
|
5329
6191
|
function onPaused(page, capture, event) {
|
|
5330
6192
|
const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
|
|
5331
|
-
const read = leaving ?
|
|
6193
|
+
const read = leaving ? Promise.all([
|
|
6194
|
+
capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
|
|
6195
|
+
capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
|
|
6196
|
+
]) : null;
|
|
5332
6197
|
if (read) {
|
|
5333
6198
|
const merging = read.then(
|
|
5334
|
-
(coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
6199
|
+
([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
5335
6200
|
);
|
|
5336
6201
|
capture.reading.add(merging);
|
|
5337
6202
|
void merging.finally(() => capture.reading.delete(merging));
|
|
@@ -5345,7 +6210,7 @@ function versionKey(url, hash) {
|
|
|
5345
6210
|
return `${hash} ${url}`;
|
|
5346
6211
|
}
|
|
5347
6212
|
function onScriptParsed(page, capture, event) {
|
|
5348
|
-
if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
6213
|
+
if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
5349
6214
|
capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
|
|
5350
6215
|
if (NEVER_REACHED.test(event.url)) return;
|
|
5351
6216
|
const key = versionKey(event.url, event.hash);
|
|
@@ -5374,6 +6239,53 @@ function readCoverage(page, cdp) {
|
|
|
5374
6239
|
);
|
|
5375
6240
|
return Promise.race([taken, late]).finally(() => clearTimeout(timer));
|
|
5376
6241
|
}
|
|
6242
|
+
function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
|
|
6243
|
+
if (header.isConstructed) return;
|
|
6244
|
+
const sheet = {
|
|
6245
|
+
sourceURL: header.sourceURL,
|
|
6246
|
+
...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
|
|
6247
|
+
isInline: header.isInline,
|
|
6248
|
+
startLine: header.startLine,
|
|
6249
|
+
pageUrl,
|
|
6250
|
+
used: /* @__PURE__ */ new Map()
|
|
6251
|
+
};
|
|
6252
|
+
css.sheets.set(header.styleSheetId, sheet);
|
|
6253
|
+
const reading = (async () => {
|
|
6254
|
+
try {
|
|
6255
|
+
if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
|
|
6256
|
+
const { node } = await internalCall(
|
|
6257
|
+
page,
|
|
6258
|
+
() => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
|
|
6259
|
+
);
|
|
6260
|
+
const attributes = node.attributes ?? [];
|
|
6261
|
+
const at = attributes.indexOf("data-vite-dev-id");
|
|
6262
|
+
if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
|
|
6263
|
+
}
|
|
6264
|
+
if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
|
|
6265
|
+
const { text } = await internalCall(
|
|
6266
|
+
page,
|
|
6267
|
+
() => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
|
|
6268
|
+
);
|
|
6269
|
+
sheet.text = text;
|
|
6270
|
+
} catch {
|
|
6271
|
+
}
|
|
6272
|
+
})();
|
|
6273
|
+
css.reading.add(reading);
|
|
6274
|
+
void reading.finally(() => css.reading.delete(reading));
|
|
6275
|
+
}
|
|
6276
|
+
async function readCssUsage(page, cdp, css) {
|
|
6277
|
+
try {
|
|
6278
|
+
const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
|
|
6279
|
+
for (const rule of coverage) {
|
|
6280
|
+
if (!rule.used) continue;
|
|
6281
|
+
css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
|
|
6282
|
+
startOffset: rule.startOffset,
|
|
6283
|
+
endOffset: rule.endOffset
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
} catch {
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
5377
6289
|
function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
|
|
5378
6290
|
for (const covered of read) {
|
|
5379
6291
|
const script = scripts.get(covered.scriptId);
|
|
@@ -5408,11 +6320,15 @@ async function detach(page, cdp) {
|
|
|
5408
6320
|
}
|
|
5409
6321
|
async function startCodeReach(page) {
|
|
5410
6322
|
if (!isChromiumPage(page) || captures.has(page)) return false;
|
|
6323
|
+
const withCss = cssCoverageInWorker();
|
|
6324
|
+
const js = browserCoverageInWorker() || !withCss;
|
|
5411
6325
|
let cdp = null;
|
|
5412
6326
|
try {
|
|
5413
6327
|
cdp = await internalCall(page, () => page.context().newCDPSession(page));
|
|
5414
6328
|
const capture = {
|
|
5415
6329
|
cdp,
|
|
6330
|
+
js,
|
|
6331
|
+
css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
|
|
5416
6332
|
scripts: /* @__PURE__ */ new Map(),
|
|
5417
6333
|
sources: /* @__PURE__ */ new Map(),
|
|
5418
6334
|
ran: /* @__PURE__ */ new Map(),
|
|
@@ -5422,6 +6338,9 @@ async function startCodeReach(page) {
|
|
|
5422
6338
|
};
|
|
5423
6339
|
const marks = marksOf(page);
|
|
5424
6340
|
cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
|
|
6341
|
+
const css = capture.css;
|
|
6342
|
+
if (css)
|
|
6343
|
+
cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
|
|
5425
6344
|
cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
|
|
5426
6345
|
cdp.on("Page.frameNavigated", (event) => {
|
|
5427
6346
|
if (event.frame.parentId) return;
|
|
@@ -5432,8 +6351,17 @@ async function startCodeReach(page) {
|
|
|
5432
6351
|
page,
|
|
5433
6352
|
() => Promise.all([
|
|
5434
6353
|
capture.cdp.send("Debugger.enable"),
|
|
5435
|
-
|
|
5436
|
-
|
|
6354
|
+
...js ? [
|
|
6355
|
+
capture.cdp.send("Profiler.enable"),
|
|
6356
|
+
capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
|
|
6357
|
+
] : [],
|
|
6358
|
+
// The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
|
|
6359
|
+
// scripts' coverage running.
|
|
6360
|
+
...css ? [
|
|
6361
|
+
capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
|
|
6362
|
+
capture.css = null;
|
|
6363
|
+
})
|
|
6364
|
+
] : [],
|
|
5437
6365
|
capture.cdp.send("Page.enable"),
|
|
5438
6366
|
capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
|
|
5439
6367
|
capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
|
|
@@ -5448,17 +6376,28 @@ async function startCodeReach(page) {
|
|
|
5448
6376
|
}
|
|
5449
6377
|
}
|
|
5450
6378
|
async function stopCodeReach(page) {
|
|
6379
|
+
return (await stopPageCoverage(page)).scripts;
|
|
6380
|
+
}
|
|
6381
|
+
async function stopPageCoverage(page) {
|
|
5451
6382
|
const capture = captures.get(page);
|
|
5452
|
-
if (!capture) return null;
|
|
6383
|
+
if (!capture) return { scripts: null, sheets: [] };
|
|
5453
6384
|
captures.delete(page);
|
|
5454
6385
|
const { cdp } = capture;
|
|
5455
|
-
|
|
6386
|
+
const sheets = async () => {
|
|
6387
|
+
if (!capture.css) return [];
|
|
6388
|
+
await readCssUsage(page, cdp, capture.css);
|
|
6389
|
+
await Promise.all(capture.css.reading);
|
|
6390
|
+
await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
|
|
6391
|
+
});
|
|
6392
|
+
return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
|
|
6393
|
+
};
|
|
6394
|
+
const scripts = async () => {
|
|
6395
|
+
if (!capture.js) return null;
|
|
5456
6396
|
const last = await internalCall(page, async () => {
|
|
5457
6397
|
const taken = await cdp.send("Profiler.takePreciseCoverage");
|
|
5458
6398
|
await cdp.send("Profiler.stopPreciseCoverage");
|
|
5459
6399
|
return taken.result;
|
|
5460
6400
|
});
|
|
5461
|
-
await Promise.all(capture.reading);
|
|
5462
6401
|
mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
|
|
5463
6402
|
const entries = [];
|
|
5464
6403
|
for (const [key, { script, functions }] of capture.ran) {
|
|
@@ -5468,15 +6407,281 @@ async function stopCodeReach(page) {
|
|
|
5468
6407
|
entries.push(entry);
|
|
5469
6408
|
}
|
|
5470
6409
|
return entries;
|
|
5471
|
-
}
|
|
5472
|
-
|
|
6410
|
+
};
|
|
6411
|
+
try {
|
|
6412
|
+
await Promise.all(capture.reading);
|
|
6413
|
+
const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
|
|
6414
|
+
return { scripts: read, sheets: styles };
|
|
5473
6415
|
} finally {
|
|
5474
6416
|
await detach(page, cdp);
|
|
5475
6417
|
}
|
|
5476
6418
|
}
|
|
5477
6419
|
|
|
6420
|
+
// src/internal/capture/css-coverage.ts
|
|
6421
|
+
var crypto4 = __toESM(require("crypto"));
|
|
6422
|
+
var fs9 = __toESM(require("fs"));
|
|
6423
|
+
|
|
6424
|
+
// ../core/src/css-rules.ts
|
|
6425
|
+
var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
|
|
6426
|
+
function parseCssRules(text) {
|
|
6427
|
+
const rules = [];
|
|
6428
|
+
const open = [];
|
|
6429
|
+
let preludeStart = -1;
|
|
6430
|
+
for (let i = 0; i < text.length; i++) {
|
|
6431
|
+
const c = text[i];
|
|
6432
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
6433
|
+
const end = text.indexOf("*/", i + 2);
|
|
6434
|
+
i = end < 0 ? text.length : end + 1;
|
|
6435
|
+
continue;
|
|
6436
|
+
}
|
|
6437
|
+
if (c === '"' || c === "'") {
|
|
6438
|
+
if (preludeStart < 0) preludeStart = i;
|
|
6439
|
+
for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
|
|
6440
|
+
continue;
|
|
6441
|
+
}
|
|
6442
|
+
if (c === "\\") {
|
|
6443
|
+
if (preludeStart < 0) preludeStart = i;
|
|
6444
|
+
i++;
|
|
6445
|
+
continue;
|
|
6446
|
+
}
|
|
6447
|
+
if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
|
|
6448
|
+
let j = i + 4;
|
|
6449
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
6450
|
+
if (text[j] !== '"' && text[j] !== "'") {
|
|
6451
|
+
if (preludeStart < 0) preludeStart = i;
|
|
6452
|
+
for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
|
|
6453
|
+
i = j;
|
|
6454
|
+
continue;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
|
|
6458
|
+
i += text.startsWith("<!--", i) ? 3 : 2;
|
|
6459
|
+
continue;
|
|
6460
|
+
}
|
|
6461
|
+
if (c === "{") {
|
|
6462
|
+
const start = preludeStart < 0 ? i : preludeStart;
|
|
6463
|
+
const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
|
|
6464
|
+
const parent = open[open.length - 1];
|
|
6465
|
+
const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
|
|
6466
|
+
open.push({ kind, start, open: i });
|
|
6467
|
+
preludeStart = -1;
|
|
6468
|
+
continue;
|
|
6469
|
+
}
|
|
6470
|
+
if (c === "}") {
|
|
6471
|
+
const block = open.pop();
|
|
6472
|
+
if (block && block.kind !== "other")
|
|
6473
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
|
|
6474
|
+
preludeStart = -1;
|
|
6475
|
+
continue;
|
|
6476
|
+
}
|
|
6477
|
+
if (c === ";") {
|
|
6478
|
+
preludeStart = -1;
|
|
6479
|
+
continue;
|
|
6480
|
+
}
|
|
6481
|
+
if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
|
|
6482
|
+
}
|
|
6483
|
+
for (const block of open) {
|
|
6484
|
+
if (block.kind !== "other")
|
|
6485
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
|
|
6486
|
+
}
|
|
6487
|
+
return rules.sort((a, b) => a.start - b.start);
|
|
6488
|
+
}
|
|
6489
|
+
function usedCssRules(rules, used) {
|
|
6490
|
+
const styleAt = /* @__PURE__ */ new Map();
|
|
6491
|
+
const groups = [];
|
|
6492
|
+
for (const rule of rules) {
|
|
6493
|
+
if (rule.kind === "style") styleAt.set(rule.start, rule);
|
|
6494
|
+
else groups.push(rule);
|
|
6495
|
+
}
|
|
6496
|
+
const out = /* @__PURE__ */ new Map();
|
|
6497
|
+
for (const range of used) {
|
|
6498
|
+
const style = styleAt.get(range.startOffset);
|
|
6499
|
+
if (style) {
|
|
6500
|
+
out.set(`${style.start}`, [style.start, style.end]);
|
|
6501
|
+
continue;
|
|
6502
|
+
}
|
|
6503
|
+
const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
|
|
6504
|
+
if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
|
|
6505
|
+
}
|
|
6506
|
+
return [...out.values()].sort((a, b) => a[0] - b[0]);
|
|
6507
|
+
}
|
|
6508
|
+
function allCssRules(rules) {
|
|
6509
|
+
return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
|
|
6510
|
+
}
|
|
6511
|
+
function cssRuleLines(text, offsets, options = {}) {
|
|
6512
|
+
const starts = lineStarts(text);
|
|
6513
|
+
const lineOffset = options.lineOffset ?? 0;
|
|
6514
|
+
const out = /* @__PURE__ */ new Map();
|
|
6515
|
+
const add2 = (source, range) => {
|
|
6516
|
+
const ranges = out.get(source);
|
|
6517
|
+
if (ranges) ranges.push(range);
|
|
6518
|
+
else out.set(source, [range]);
|
|
6519
|
+
};
|
|
6520
|
+
for (const [startOffset, endOffset] of offsets) {
|
|
6521
|
+
const start = offsetToPosition(starts, startOffset);
|
|
6522
|
+
const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
|
|
6523
|
+
if (!options.lookup) {
|
|
6524
|
+
add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
|
|
6525
|
+
continue;
|
|
6526
|
+
}
|
|
6527
|
+
const first = options.lookup.originalAt(start.line, start.column);
|
|
6528
|
+
if (!first) continue;
|
|
6529
|
+
const last = options.lookup.originalAt(end.line, end.column);
|
|
6530
|
+
const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
|
|
6531
|
+
add2(first.source, [first.line + 1, lastLine + 1]);
|
|
6532
|
+
}
|
|
6533
|
+
return out;
|
|
6534
|
+
}
|
|
6535
|
+
|
|
6536
|
+
// src/internal/capture/css-coverage.ts
|
|
6537
|
+
var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
|
|
6538
|
+
var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
|
|
6539
|
+
var resolutions = /* @__PURE__ */ new Map();
|
|
6540
|
+
var MAX_CACHED_SHEETS = 2e3;
|
|
6541
|
+
function fileForStyleUrl(url, roots) {
|
|
6542
|
+
let pathname;
|
|
6543
|
+
try {
|
|
6544
|
+
const parsed2 = new URL(url);
|
|
6545
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
|
|
6546
|
+
pathname = parsed2.pathname;
|
|
6547
|
+
} catch {
|
|
6548
|
+
return null;
|
|
6549
|
+
}
|
|
6550
|
+
return sourceFile(decodeURIComponent(pathname), roots);
|
|
6551
|
+
}
|
|
6552
|
+
function sourceFile(source, roots) {
|
|
6553
|
+
const bare = source.replace(/[?#].*$/, "");
|
|
6554
|
+
if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
|
|
6555
|
+
const file = resolveSourcePath(bare, roots);
|
|
6556
|
+
if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
|
|
6557
|
+
return file;
|
|
6558
|
+
}
|
|
6559
|
+
function styleBlockLine(file, index) {
|
|
6560
|
+
let text;
|
|
6561
|
+
try {
|
|
6562
|
+
text = fs9.readFileSync(file, "utf-8");
|
|
6563
|
+
} catch {
|
|
6564
|
+
return 0;
|
|
6565
|
+
}
|
|
6566
|
+
const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
|
|
6567
|
+
const block = blocks[index];
|
|
6568
|
+
if (!block) return 0;
|
|
6569
|
+
return text.slice(0, block.index + block[0].length).split("\n").length - 1;
|
|
6570
|
+
}
|
|
6571
|
+
async function sheetMap(sheet, text, fetchMap) {
|
|
6572
|
+
const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
|
|
6573
|
+
if (!ref) return null;
|
|
6574
|
+
let raw;
|
|
6575
|
+
if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
|
|
6576
|
+
else {
|
|
6577
|
+
try {
|
|
6578
|
+
raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
|
|
6579
|
+
} catch {
|
|
6580
|
+
raw = null;
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
if (!raw) return null;
|
|
6584
|
+
try {
|
|
6585
|
+
return decodeSourceMap(JSON.parse(raw));
|
|
6586
|
+
} catch {
|
|
6587
|
+
return null;
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
function sameLines(text, rules, file, lineOffset) {
|
|
6591
|
+
let source;
|
|
6592
|
+
try {
|
|
6593
|
+
source = fs9.readFileSync(file, "utf-8");
|
|
6594
|
+
} catch {
|
|
6595
|
+
return false;
|
|
6596
|
+
}
|
|
6597
|
+
const fileLines = source.split(/\r?\n/);
|
|
6598
|
+
const sheetLineStarts = [0];
|
|
6599
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
|
|
6600
|
+
let low = 0;
|
|
6601
|
+
for (const rule of rules) {
|
|
6602
|
+
while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
|
|
6603
|
+
const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
|
|
6604
|
+
if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
|
|
6605
|
+
}
|
|
6606
|
+
return true;
|
|
6607
|
+
}
|
|
6608
|
+
async function resolveSheet(sheet, text, dirs, fetchMap) {
|
|
6609
|
+
const lookup = await sheetMap(sheet, text, fetchMap);
|
|
6610
|
+
const rules = parseCssRules(text);
|
|
6611
|
+
if (lookup) {
|
|
6612
|
+
const sources = lookup.sources.map((source) => {
|
|
6613
|
+
const direct = sourceFile(source, dirs);
|
|
6614
|
+
if (direct) return direct;
|
|
6615
|
+
try {
|
|
6616
|
+
return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
|
|
6617
|
+
} catch {
|
|
6618
|
+
return null;
|
|
6619
|
+
}
|
|
6620
|
+
});
|
|
6621
|
+
return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
|
|
6622
|
+
}
|
|
6623
|
+
if (sheet.devId) {
|
|
6624
|
+
const file2 = sourceFile(sheet.devId, dirs);
|
|
6625
|
+
if (!file2) return null;
|
|
6626
|
+
const index = /[?&]index=(\d+)/.exec(sheet.devId);
|
|
6627
|
+
const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
|
|
6628
|
+
return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
|
|
6629
|
+
}
|
|
6630
|
+
const file = fileForStyleUrl(sheet.sourceURL, dirs);
|
|
6631
|
+
if (!file) return null;
|
|
6632
|
+
const lineOffset = sheet.isInline ? sheet.startLine : 0;
|
|
6633
|
+
return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
|
|
6634
|
+
}
|
|
6635
|
+
function cachedResolution(sheet, text, dirs, fetchMap) {
|
|
6636
|
+
const version = crypto4.createHash("sha1").update(text).digest("hex");
|
|
6637
|
+
const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
|
|
6638
|
+
" "
|
|
6639
|
+
);
|
|
6640
|
+
let pending = resolutions.get(key);
|
|
6641
|
+
if (!pending) {
|
|
6642
|
+
pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
|
|
6643
|
+
resolutions.set(key, pending);
|
|
6644
|
+
if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
|
|
6645
|
+
}
|
|
6646
|
+
return pending;
|
|
6647
|
+
}
|
|
6648
|
+
function add(into, file, ranges, repoRoot, fileLevel = false) {
|
|
6649
|
+
if (ranges.length === 0 && !fileLevel) return;
|
|
6650
|
+
const key = relativeToRepo(file, repoRoot);
|
|
6651
|
+
into.set(key, [...into.get(key) ?? [], ...ranges]);
|
|
6652
|
+
}
|
|
6653
|
+
async function resolveCssCoverage(sheets, roots, fetchMap) {
|
|
6654
|
+
const reached = /* @__PURE__ */ new Map();
|
|
6655
|
+
const known = /* @__PURE__ */ new Map();
|
|
6656
|
+
for (const sheet of sheets) {
|
|
6657
|
+
if (sheet.text === void 0) continue;
|
|
6658
|
+
const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
|
|
6659
|
+
const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
|
|
6660
|
+
if (!resolution) continue;
|
|
6661
|
+
const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
|
|
6662
|
+
if (!resolution.exact) {
|
|
6663
|
+
if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
|
|
6664
|
+
continue;
|
|
6665
|
+
}
|
|
6666
|
+
const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
|
|
6667
|
+
const fileOf = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
|
|
6668
|
+
const matched = cssRuleLines(sheet.text, used, options);
|
|
6669
|
+
for (const [source, ranges] of matched) {
|
|
6670
|
+
const file = fileOf(source);
|
|
6671
|
+
if (file) add(reached, file, ranges, roots.repoRoot);
|
|
6672
|
+
}
|
|
6673
|
+
for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
|
|
6674
|
+
const file = fileOf(source);
|
|
6675
|
+
if (file) add(known, file, ranges, roots.repoRoot);
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
return { reached, known };
|
|
6679
|
+
}
|
|
6680
|
+
|
|
5478
6681
|
// src/internal/capture/resource-ledger.ts
|
|
5479
|
-
var
|
|
6682
|
+
var childProcess = __toESM(require("child_process"));
|
|
6683
|
+
var fs11 = __toESM(require("fs"));
|
|
6684
|
+
var net3 = __toESM(require("net"));
|
|
5480
6685
|
var path13 = __toESM(require("path"));
|
|
5481
6686
|
var import_node_module2 = require("module");
|
|
5482
6687
|
|
|
@@ -5484,12 +6689,12 @@ var import_node_module2 = require("module");
|
|
|
5484
6689
|
var import_node_perf_hooks = require("perf_hooks");
|
|
5485
6690
|
|
|
5486
6691
|
// src/internal/support/system-readers.ts
|
|
5487
|
-
var
|
|
6692
|
+
var fs10 = __toESM(require("fs"));
|
|
5488
6693
|
var path12 = __toESM(require("path"));
|
|
5489
6694
|
var CLK_TCK = 100;
|
|
5490
6695
|
function readText(file) {
|
|
5491
6696
|
try {
|
|
5492
|
-
return
|
|
6697
|
+
return fs10.readFileSync(file, "utf8");
|
|
5493
6698
|
} catch {
|
|
5494
6699
|
return null;
|
|
5495
6700
|
}
|
|
@@ -5571,14 +6776,14 @@ var ProcFs = class {
|
|
|
5571
6776
|
}
|
|
5572
6777
|
/** Whether this looks like a Linux `/proc` at all. */
|
|
5573
6778
|
available() {
|
|
5574
|
-
return
|
|
6779
|
+
return fs10.existsSync(path12.join(this.root, "stat"));
|
|
5575
6780
|
}
|
|
5576
6781
|
/** Every process, keyed by pid. */
|
|
5577
6782
|
table() {
|
|
5578
6783
|
const out = /* @__PURE__ */ new Map();
|
|
5579
6784
|
let names;
|
|
5580
6785
|
try {
|
|
5581
|
-
names =
|
|
6786
|
+
names = fs10.readdirSync(this.root);
|
|
5582
6787
|
} catch {
|
|
5583
6788
|
return out;
|
|
5584
6789
|
}
|
|
@@ -5609,7 +6814,7 @@ var ProcFs = class {
|
|
|
5609
6814
|
/** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
|
|
5610
6815
|
async pssKb(pid) {
|
|
5611
6816
|
try {
|
|
5612
|
-
return parseKbField(await
|
|
6817
|
+
return parseKbField(await fs10.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
|
|
5613
6818
|
} catch {
|
|
5614
6819
|
return null;
|
|
5615
6820
|
}
|
|
@@ -5617,7 +6822,7 @@ var ProcFs = class {
|
|
|
5617
6822
|
/** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
|
|
5618
6823
|
resetPeakRss(pid) {
|
|
5619
6824
|
try {
|
|
5620
|
-
|
|
6825
|
+
fs10.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
|
|
5621
6826
|
return true;
|
|
5622
6827
|
} catch {
|
|
5623
6828
|
return false;
|
|
@@ -5639,7 +6844,7 @@ var ProcFs = class {
|
|
|
5639
6844
|
/** Open file descriptors of a process. */
|
|
5640
6845
|
fdCount(pid) {
|
|
5641
6846
|
try {
|
|
5642
|
-
return
|
|
6847
|
+
return fs10.readdirSync(path12.join(this.root, String(pid), "fd")).length;
|
|
5643
6848
|
} catch {
|
|
5644
6849
|
return null;
|
|
5645
6850
|
}
|
|
@@ -5656,6 +6861,8 @@ var WorkerMetrics = class {
|
|
|
5656
6861
|
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
5657
6862
|
this.proc = proc;
|
|
5658
6863
|
this.pid = pid;
|
|
6864
|
+
/** The lowest heap read right after a full garbage collection in the current window. */
|
|
6865
|
+
this.heapAfterGc = null;
|
|
5659
6866
|
this.loop = null;
|
|
5660
6867
|
this.usage = null;
|
|
5661
6868
|
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
@@ -5670,6 +6877,28 @@ var WorkerMetrics = class {
|
|
|
5670
6877
|
histogram = null;
|
|
5671
6878
|
}
|
|
5672
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
|
+
}
|
|
5673
6902
|
}
|
|
5674
6903
|
/** The worker's processes under it, keyed by pid and start time. */
|
|
5675
6904
|
tree() {
|
|
@@ -5703,6 +6932,7 @@ var WorkerMetrics = class {
|
|
|
5703
6932
|
start(processes = true) {
|
|
5704
6933
|
try {
|
|
5705
6934
|
this.histogram?.reset();
|
|
6935
|
+
this.heapAfterGc = null;
|
|
5706
6936
|
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
5707
6937
|
this.usage = process.resourceUsage();
|
|
5708
6938
|
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
@@ -5725,6 +6955,7 @@ var WorkerMetrics = class {
|
|
|
5725
6955
|
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
5726
6956
|
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
5727
6957
|
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
6958
|
+
heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
|
|
5728
6959
|
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
5729
6960
|
}
|
|
5730
6961
|
};
|
|
@@ -5759,6 +6990,7 @@ var WorkerMetrics = class {
|
|
|
5759
6990
|
}
|
|
5760
6991
|
dispose() {
|
|
5761
6992
|
this.histogram?.disable();
|
|
6993
|
+
this.gcObserver?.disconnect();
|
|
5762
6994
|
}
|
|
5763
6995
|
};
|
|
5764
6996
|
var PageReader = class {
|
|
@@ -5780,12 +7012,36 @@ var PageReader = class {
|
|
|
5780
7012
|
try {
|
|
5781
7013
|
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
5782
7014
|
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
5783
|
-
|
|
7015
|
+
const main = {
|
|
5784
7016
|
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
5785
7017
|
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
5786
7018
|
nodes: values.get("Nodes") ?? 0,
|
|
5787
7019
|
listeners: values.get("JSEventListeners") ?? 0
|
|
5788
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;
|
|
5789
7045
|
} catch {
|
|
5790
7046
|
return null;
|
|
5791
7047
|
}
|
|
@@ -5794,6 +7050,9 @@ var PageReader = class {
|
|
|
5794
7050
|
|
|
5795
7051
|
// src/internal/capture/resource-ledger.ts
|
|
5796
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;
|
|
5797
7056
|
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
5798
7057
|
var CLOSE_TIMEOUT_MS = 5e3;
|
|
5799
7058
|
var PAGE_READ_CAP = 50;
|
|
@@ -5869,20 +7128,41 @@ function playwrightTestInfoSource() {
|
|
|
5869
7128
|
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
5870
7129
|
return value === "fail" || value === "close" ? value : "report";
|
|
5871
7130
|
}
|
|
5872
|
-
function countHandles() {
|
|
7131
|
+
function countHandles(types = TRACKED_HANDLE_TYPES) {
|
|
5873
7132
|
const counts = {};
|
|
5874
7133
|
const read = process.getActiveResourcesInfo;
|
|
5875
7134
|
if (typeof read !== "function") return counts;
|
|
5876
7135
|
for (const type of read.call(process)) {
|
|
5877
|
-
if (
|
|
7136
|
+
if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
5878
7137
|
}
|
|
5879
7138
|
return counts;
|
|
5880
7139
|
}
|
|
5881
|
-
async function settledHandles(start) {
|
|
5882
|
-
const end = countHandles();
|
|
7140
|
+
async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
|
|
7141
|
+
const end = countHandles(types);
|
|
5883
7142
|
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
5884
7143
|
await new Promise((resolve5) => setTimeout(resolve5, 0));
|
|
5885
|
-
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";
|
|
5886
7166
|
}
|
|
5887
7167
|
function settleWithin(promise, ms) {
|
|
5888
7168
|
let timer;
|
|
@@ -5894,7 +7174,7 @@ function settleWithin(promise, ms) {
|
|
|
5894
7174
|
]).finally(() => clearTimeout(timer));
|
|
5895
7175
|
}
|
|
5896
7176
|
var ResourceLedger = class {
|
|
5897
|
-
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) {
|
|
5898
7178
|
this.playwright = playwright;
|
|
5899
7179
|
this.workerIndex = workerIndex;
|
|
5900
7180
|
this.leakCheck = leakCheck;
|
|
@@ -5902,6 +7182,7 @@ var ResourceLedger = class {
|
|
|
5902
7182
|
this.testInfoSource = testInfoSource;
|
|
5903
7183
|
this.now = now;
|
|
5904
7184
|
this.readers = readers;
|
|
7185
|
+
this.nodeHandles = nodeHandles;
|
|
5905
7186
|
this.nextId = 1;
|
|
5906
7187
|
this.entries = /* @__PURE__ */ new Map();
|
|
5907
7188
|
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
@@ -5918,6 +7199,11 @@ var ResourceLedger = class {
|
|
|
5918
7199
|
this.listener = null;
|
|
5919
7200
|
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
5920
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 = [];
|
|
5921
7207
|
}
|
|
5922
7208
|
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
5923
7209
|
install() {
|
|
@@ -5950,13 +7236,107 @@ var ResourceLedger = class {
|
|
|
5950
7236
|
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
5951
7237
|
if (type) this.wrapBrowserType(type);
|
|
5952
7238
|
}
|
|
7239
|
+
if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
|
|
5953
7240
|
return true;
|
|
5954
7241
|
}
|
|
5955
|
-
/** 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. */
|
|
5956
7243
|
uninstall() {
|
|
5957
7244
|
const remove = this.playwright?._instrumentation?.removeListener;
|
|
5958
7245
|
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
5959
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 } : {} }));
|
|
5960
7340
|
}
|
|
5961
7341
|
/** Mark a page used: a locator was built on it. */
|
|
5962
7342
|
notePageUse(page) {
|
|
@@ -5966,7 +7346,7 @@ var ResourceLedger = class {
|
|
|
5966
7346
|
/** Open the census window of a test. */
|
|
5967
7347
|
testStarted(info) {
|
|
5968
7348
|
this.window = testRef2(info);
|
|
5969
|
-
this.handlesAtStart = countHandles();
|
|
7349
|
+
this.handlesAtStart = countHandles(this.countedHandleTypes());
|
|
5970
7350
|
this.openAtStart = { contexts: 0, pages: 0 };
|
|
5971
7351
|
for (const entry of this.openEntries()) {
|
|
5972
7352
|
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
@@ -5986,9 +7366,14 @@ var ResourceLedger = class {
|
|
|
5986
7366
|
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
5987
7367
|
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
5988
7368
|
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
5989
|
-
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;
|
|
5990
7374
|
const census = this.census(ref, handles, await this.readPages());
|
|
5991
7375
|
if (metrics) census.metrics = metrics;
|
|
7376
|
+
if (running) census.running = running;
|
|
5992
7377
|
if (test) {
|
|
5993
7378
|
census.openAtStart = this.openAtStart;
|
|
5994
7379
|
if (!failed) census.leftOpen = leaks.length;
|
|
@@ -6018,7 +7403,7 @@ var ResourceLedger = class {
|
|
|
6018
7403
|
const census = this.census(null);
|
|
6019
7404
|
if (this.resultsFile) {
|
|
6020
7405
|
try {
|
|
6021
|
-
|
|
7406
|
+
fs11.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
|
|
6022
7407
|
`);
|
|
6023
7408
|
} catch {
|
|
6024
7409
|
}
|
|
@@ -6027,6 +7412,10 @@ var ResourceLedger = class {
|
|
|
6027
7412
|
this.uninstall();
|
|
6028
7413
|
return census;
|
|
6029
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
|
+
}
|
|
6030
7419
|
guard(run) {
|
|
6031
7420
|
try {
|
|
6032
7421
|
run();
|
|
@@ -6249,6 +7638,7 @@ var ResourceLedger = class {
|
|
|
6249
7638
|
open
|
|
6250
7639
|
};
|
|
6251
7640
|
if (handles) census.handles = handles;
|
|
7641
|
+
for (const close of this.closed) this.entries.delete(close.id);
|
|
6252
7642
|
this.born = [];
|
|
6253
7643
|
this.closed = [];
|
|
6254
7644
|
return census;
|
|
@@ -6287,7 +7677,8 @@ function startResourceLedger(playwright, workerIndex) {
|
|
|
6287
7677
|
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
6288
7678
|
playwrightTestInfoSource(),
|
|
6289
7679
|
Date.now,
|
|
6290
|
-
{ metrics, pages: new PageReader() }
|
|
7680
|
+
{ metrics, pages: new PageReader() },
|
|
7681
|
+
true
|
|
6291
7682
|
);
|
|
6292
7683
|
if (!ledger.install()) {
|
|
6293
7684
|
metrics.dispose();
|
|
@@ -6638,21 +8029,29 @@ function createSink() {
|
|
|
6638
8029
|
locatorCalls: /* @__PURE__ */ new Map(),
|
|
6639
8030
|
stops: [],
|
|
6640
8031
|
shownStops: /* @__PURE__ */ new Map(),
|
|
8032
|
+
failureHeld: false,
|
|
8033
|
+
frozenAt: null,
|
|
8034
|
+
stopEnds: 0,
|
|
8035
|
+
ending: false,
|
|
6641
8036
|
released: [],
|
|
6642
8037
|
releasedRequests: /* @__PURE__ */ new Set(),
|
|
6643
8038
|
onReleasedRequestsEnded: null,
|
|
6644
8039
|
releasedQuietSince: 0,
|
|
8040
|
+
releasedUntil: null,
|
|
6645
8041
|
locatorPages: new LocatorPageLog(),
|
|
6646
|
-
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
|
|
8042
|
+
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
|
|
6647
8043
|
serverCoverage: /* @__PURE__ */ new Map(),
|
|
6648
8044
|
baseURL: null
|
|
6649
8045
|
};
|
|
6650
8046
|
}
|
|
6651
8047
|
var MAX_CODE_REACH_PAGES = 16;
|
|
6652
8048
|
var CODE_REACH_START_WAIT_MS = 2e3;
|
|
8049
|
+
function pageCoverageInWorker() {
|
|
8050
|
+
return browserCoverageInWorker() || cssCoverageInWorker();
|
|
8051
|
+
}
|
|
6653
8052
|
function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
6654
8053
|
const sink = currentSink;
|
|
6655
|
-
if (!sink || !
|
|
8054
|
+
if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
|
|
6656
8055
|
const known = sink.codeReach.sessions.get(page);
|
|
6657
8056
|
if (known) return known.started;
|
|
6658
8057
|
if (alreadyOpen) {
|
|
@@ -6665,7 +8064,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
|
6665
8064
|
return started;
|
|
6666
8065
|
}
|
|
6667
8066
|
async function startCodeReachOnOpenPages() {
|
|
6668
|
-
if (!
|
|
8067
|
+
if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
|
|
6669
8068
|
const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
|
|
6670
8069
|
await Promise.race([started, new Promise((resolve5) => setTimeout(resolve5, CODE_REACH_START_WAIT_MS).unref())]);
|
|
6671
8070
|
}
|
|
@@ -6708,22 +8107,28 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
6708
8107
|
async function stopPageCodeReach(sink, page, session, roots) {
|
|
6709
8108
|
try {
|
|
6710
8109
|
if (!await session.started || isPageClosed(page)) return;
|
|
6711
|
-
const
|
|
6712
|
-
|
|
6713
|
-
const
|
|
8110
|
+
const { scripts, sheets } = await stopPageCoverage(page);
|
|
8111
|
+
const fetchMap = pageMapFetcher(page);
|
|
8112
|
+
const append = (into, from) => {
|
|
8113
|
+
for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
|
|
8114
|
+
};
|
|
8115
|
+
if (sheets.length > 0) {
|
|
8116
|
+
const rules = await resolveCssCoverage(sheets, roots, fetchMap);
|
|
8117
|
+
append(sink.codeReach.cssFiles, rules.reached);
|
|
8118
|
+
append(sink.codeReach.cssKnown, rules.known);
|
|
8119
|
+
}
|
|
8120
|
+
if (!scripts) return;
|
|
8121
|
+
const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
|
|
6714
8122
|
known: browserFunctionsInWorker()
|
|
6715
8123
|
});
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
}
|
|
6719
|
-
for (const [file, functions] of known) {
|
|
6720
|
-
sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
|
|
6721
|
-
}
|
|
8124
|
+
append(sink.codeReach.files, reached);
|
|
8125
|
+
append(sink.codeReach.known, known);
|
|
6722
8126
|
} catch {
|
|
6723
8127
|
}
|
|
6724
8128
|
}
|
|
6725
8129
|
var currentSink = null;
|
|
6726
8130
|
var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
|
|
8131
|
+
var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
|
|
6727
8132
|
var PENDING_PROBES = /* @__PURE__ */ new Set();
|
|
6728
8133
|
async function drainPendingProbes(capMs) {
|
|
6729
8134
|
if (PENDING_PROBES.size === 0) return;
|
|
@@ -6736,6 +8141,25 @@ async function drainPendingProbes(capMs) {
|
|
|
6736
8141
|
]);
|
|
6737
8142
|
clearTimeout(cap);
|
|
6738
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
|
+
}
|
|
6739
8163
|
function isPageClosed(page) {
|
|
6740
8164
|
try {
|
|
6741
8165
|
return typeof page.isClosed === "function" ? page.isClosed() : false;
|
|
@@ -6858,7 +8282,7 @@ function buildPageState(raw, cookies, interaction) {
|
|
|
6858
8282
|
}
|
|
6859
8283
|
async function readPageState(page) {
|
|
6860
8284
|
try {
|
|
6861
|
-
const
|
|
8285
|
+
const readRaw = page.evaluate(() => {
|
|
6862
8286
|
const listStorage = (s) => {
|
|
6863
8287
|
const out = [];
|
|
6864
8288
|
try {
|
|
@@ -6884,14 +8308,16 @@ async function readPageState(page) {
|
|
|
6884
8308
|
sessionStorage: listStorage(globalThis.sessionStorage)
|
|
6885
8309
|
};
|
|
6886
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]);
|
|
6887
8320
|
if (!raw) return null;
|
|
6888
|
-
let cookies = null;
|
|
6889
|
-
try {
|
|
6890
|
-
cookies = await pageContext(page)?.cookies() ?? null;
|
|
6891
|
-
} catch {
|
|
6892
|
-
cookies = null;
|
|
6893
|
-
}
|
|
6894
|
-
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
6895
8321
|
return buildPageState(raw, cookies, interaction);
|
|
6896
8322
|
} catch {
|
|
6897
8323
|
return null;
|
|
@@ -6955,7 +8381,7 @@ async function recordPageWindow(sink, page) {
|
|
|
6955
8381
|
}
|
|
6956
8382
|
async function stashPageState(sink, closing) {
|
|
6957
8383
|
const page = sink.lastActivePage;
|
|
6958
|
-
if (!page || isPageClosed(page)) return;
|
|
8384
|
+
if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
|
|
6959
8385
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
6960
8386
|
if (!belongsToClosing) return;
|
|
6961
8387
|
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
@@ -6967,13 +8393,12 @@ async function readBlankPage(page) {
|
|
|
6967
8393
|
return aria != null && aria.trim() === "";
|
|
6968
8394
|
}
|
|
6969
8395
|
async function readPageBeforeClose(sink, page) {
|
|
6970
|
-
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]);
|
|
6971
8399
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
6972
|
-
if (
|
|
6973
|
-
if (
|
|
6974
|
-
const pageState = await readPageState(page);
|
|
6975
|
-
if (pageState) sink.stashedPageState = pageState;
|
|
6976
|
-
}
|
|
8400
|
+
if (blankPage !== null) sink.probeBlankPage = blankPage;
|
|
8401
|
+
if (pageState) sink.stashedPageState = pageState;
|
|
6977
8402
|
const status = sink.testInfo?.status;
|
|
6978
8403
|
const sampleAria = async () => {
|
|
6979
8404
|
const root = page.locator(":root");
|
|
@@ -7167,16 +8592,35 @@ function locatorSource(target, method, args) {
|
|
|
7167
8592
|
return renderFailing({ method, args });
|
|
7168
8593
|
}
|
|
7169
8594
|
var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
8595
|
+
var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
8596
|
+
var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
|
|
7170
8597
|
var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
7171
|
-
|
|
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) {
|
|
7172
8615
|
const state = currentSink?.pause;
|
|
7173
|
-
if (!state || state
|
|
8616
|
+
if (!state || !wantsStops(state)) return;
|
|
7174
8617
|
await Promise.all(
|
|
7175
8618
|
[...contexts].map(
|
|
7176
8619
|
(context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
|
|
7177
8620
|
)
|
|
7178
8621
|
);
|
|
7179
8622
|
}
|
|
8623
|
+
var stopTurn = Promise.resolve();
|
|
7180
8624
|
function onDebuggerStop(context) {
|
|
7181
8625
|
const details = contextDebugger(context)?.pausedDetails();
|
|
7182
8626
|
if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
|
|
@@ -7187,13 +8631,38 @@ function onDebuggerStop(context) {
|
|
|
7187
8631
|
try {
|
|
7188
8632
|
if (!sink || !state) return;
|
|
7189
8633
|
const callSite = stopSite(sink, details.location);
|
|
8634
|
+
const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
|
|
7190
8635
|
const pagePause = PAGE_PAUSES.get(context);
|
|
8636
|
+
const asked = askedOnce(state);
|
|
7191
8637
|
const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
|
|
7192
8638
|
if ("go" in decided) {
|
|
7193
8639
|
how = decided.go;
|
|
7194
8640
|
return;
|
|
7195
8641
|
}
|
|
7196
|
-
|
|
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);
|
|
7197
8666
|
} catch {
|
|
7198
8667
|
if (state) how = goOn(state);
|
|
7199
8668
|
} finally {
|
|
@@ -7201,22 +8670,30 @@ function onDebuggerStop(context) {
|
|
|
7201
8670
|
}
|
|
7202
8671
|
})();
|
|
7203
8672
|
}
|
|
8673
|
+
function endedWaiting(sink, call) {
|
|
8674
|
+
releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
8675
|
+
return "finish";
|
|
8676
|
+
}
|
|
7204
8677
|
function stopSite(sink, location) {
|
|
7205
8678
|
if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
|
|
7206
8679
|
return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
|
|
7207
8680
|
}
|
|
7208
|
-
function
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
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);
|
|
7213
8686
|
return open[open.length - 1] ?? null;
|
|
7214
8687
|
}
|
|
7215
|
-
async function showStop(sink, context, title, location, reason) {
|
|
8688
|
+
async function showStop(sink, context, title, location, reason, call, of, failure = null) {
|
|
7216
8689
|
const testInfo = sink.testInfo;
|
|
7217
|
-
const
|
|
7218
|
-
const
|
|
7219
|
-
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;
|
|
7220
8697
|
const onPick = async (picked) => {
|
|
7221
8698
|
if (call) {
|
|
7222
8699
|
call.picked = true;
|
|
@@ -7227,16 +8704,57 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
7227
8704
|
...picked
|
|
7228
8705
|
});
|
|
7229
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);
|
|
7230
8714
|
}
|
|
7231
8715
|
console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
|
|
7232
|
-
await postPickToEditor(
|
|
8716
|
+
await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
|
|
7233
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;
|
|
7234
8721
|
const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
|
|
7235
8722
|
sink.stops.push(record);
|
|
7236
8723
|
const end = new AbortController();
|
|
7237
|
-
const
|
|
7238
|
-
|
|
7239
|
-
|
|
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(
|
|
7240
8758
|
page,
|
|
7241
8759
|
() => pauseHere(
|
|
7242
8760
|
page,
|
|
@@ -7244,45 +8762,221 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
7244
8762
|
location,
|
|
7245
8763
|
call: title,
|
|
7246
8764
|
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
7247
|
-
target: call?.page === page ? call.target : null
|
|
8765
|
+
target: call?.page === page ? call.target : null,
|
|
8766
|
+
...failure ? { failure: true } : {}
|
|
7248
8767
|
},
|
|
7249
8768
|
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
7250
8769
|
onPick,
|
|
7251
|
-
|
|
7252
|
-
|
|
8770
|
+
onRecord,
|
|
8771
|
+
over.signal,
|
|
8772
|
+
intents ?? void 0,
|
|
8773
|
+
onIntent
|
|
7253
8774
|
)
|
|
7254
|
-
)
|
|
7255
|
-
).finally(() => {
|
|
8775
|
+
);
|
|
8776
|
+
}).finally(async () => {
|
|
7256
8777
|
if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
|
|
7257
8778
|
else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
7258
8779
|
sink.shownStops.delete(end);
|
|
8780
|
+
await held2?.release();
|
|
7259
8781
|
});
|
|
7260
8782
|
const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
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
|
+
});
|
|
7265
8951
|
}
|
|
7266
8952
|
var END_STOP_MS = 2e3;
|
|
7267
8953
|
var RELEASED_CALL_MS = 2e3;
|
|
7268
8954
|
var RELEASED_QUIET_MS = 200;
|
|
7269
8955
|
async function endShownStops(sink, why, closing) {
|
|
7270
8956
|
const ending = [];
|
|
8957
|
+
if (why === "ended") sink.stopEnds++;
|
|
7271
8958
|
let heldAt = Infinity;
|
|
7272
|
-
for (const [end,
|
|
7273
|
-
if (closing &&
|
|
8959
|
+
for (const [end, stop] of sink.shownStops) {
|
|
8960
|
+
if (closing && !closesStop(closing, stop)) continue;
|
|
7274
8961
|
if (!end.signal.aborted) {
|
|
7275
|
-
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;
|
|
7276
8964
|
end.abort(why);
|
|
7277
|
-
if (why === "ended" &&
|
|
8965
|
+
if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
|
|
7278
8966
|
}
|
|
7279
|
-
ending.push(done);
|
|
8967
|
+
ending.push(stop.done);
|
|
7280
8968
|
}
|
|
7281
8969
|
if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
|
|
7282
8970
|
await settledWithin(ending, END_STOP_MS);
|
|
7283
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
|
+
}
|
|
7284
8977
|
function releaseCall(sink, settled) {
|
|
7285
8978
|
sink.releasedQuietSince = Date.now();
|
|
8979
|
+
sink.releasedUntil = null;
|
|
7286
8980
|
sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
|
|
7287
8981
|
}
|
|
7288
8982
|
function releasedRequestsEnded(sink) {
|
|
@@ -7291,8 +8985,8 @@ function releasedRequestsEnded(sink) {
|
|
|
7291
8985
|
}
|
|
7292
8986
|
async function awaitReleasedCalls(sink) {
|
|
7293
8987
|
if (sink.released.length === 0) return;
|
|
7294
|
-
const deadline = Date.now() + RELEASED_CALL_MS;
|
|
7295
|
-
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()));
|
|
7296
8990
|
for (let now = Date.now(); now < deadline; now = Date.now()) {
|
|
7297
8991
|
if (sink.releasedRequests.size > 0) {
|
|
7298
8992
|
await settledWithin([releasedRequestsEnded(sink)], deadline - now);
|
|
@@ -7322,7 +9016,7 @@ function recorderAt(page, testInfo, location, reason) {
|
|
|
7322
9016
|
return async (ended) => {
|
|
7323
9017
|
console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
|
|
7324
9018
|
const why = await recordFromHere({ page, testInfo, location, reason, ended });
|
|
7325
|
-
if (ended?.aborted) console.log("[piwi] The recording ended with the
|
|
9019
|
+
if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
|
|
7326
9020
|
else
|
|
7327
9021
|
console.log(
|
|
7328
9022
|
why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
|
|
@@ -7339,10 +9033,10 @@ async function pauseInPlace(page, playwrightPause, location) {
|
|
|
7339
9033
|
if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
|
|
7340
9034
|
return leaveToPlaywright(context, playwrightPause);
|
|
7341
9035
|
}
|
|
7342
|
-
sink.pause ??=
|
|
9036
|
+
sink.pause ??= pauseState(NO_BREAKPOINTS);
|
|
7343
9037
|
watchStops(context, onDebuggerStop);
|
|
7344
9038
|
await steerDebugger(context, "pause");
|
|
7345
|
-
PAGE_PAUSES.set(context, location);
|
|
9039
|
+
PAGE_PAUSES.set(context, { location, page });
|
|
7346
9040
|
try {
|
|
7347
9041
|
await playwrightPause({ __testHookKeepTestTimeout: true });
|
|
7348
9042
|
} finally {
|
|
@@ -7525,15 +9219,15 @@ function instrumentPage(page) {
|
|
|
7525
9219
|
if (typeof page.close === "function") {
|
|
7526
9220
|
const originalClose = page.close.bind(page);
|
|
7527
9221
|
page.close = async (...args) => {
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
await
|
|
7533
|
-
await
|
|
7534
|
-
|
|
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();
|
|
7535
9230
|
}
|
|
7536
|
-
return originalClose(...args);
|
|
7537
9231
|
};
|
|
7538
9232
|
}
|
|
7539
9233
|
const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
|
|
@@ -7578,7 +9272,7 @@ function instrumentPage(page) {
|
|
|
7578
9272
|
});
|
|
7579
9273
|
page.on("console", (msg) => {
|
|
7580
9274
|
const sink = currentSink;
|
|
7581
|
-
if (!sink) return;
|
|
9275
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
7582
9276
|
const type = msg.type();
|
|
7583
9277
|
if (["warning", "error", "assert"].includes(type)) {
|
|
7584
9278
|
const location = msg.location();
|
|
@@ -7594,7 +9288,7 @@ function instrumentPage(page) {
|
|
|
7594
9288
|
try {
|
|
7595
9289
|
page.on("dialogclosed", (dialog) => {
|
|
7596
9290
|
const sink = currentSink;
|
|
7597
|
-
if (!sink) return;
|
|
9291
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
7598
9292
|
try {
|
|
7599
9293
|
sink.dialogs.push({
|
|
7600
9294
|
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
@@ -7613,12 +9307,14 @@ function instrumentPage(page) {
|
|
|
7613
9307
|
if (!sink) return;
|
|
7614
9308
|
try {
|
|
7615
9309
|
const request = response.request();
|
|
9310
|
+
if (HELD_REQUESTS.has(request)) return;
|
|
7616
9311
|
if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
|
|
7617
9312
|
} catch {
|
|
7618
9313
|
}
|
|
7619
9314
|
});
|
|
7620
9315
|
page.on("request", (request) => {
|
|
7621
9316
|
const sink = currentSink;
|
|
9317
|
+
if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request);
|
|
7622
9318
|
if (!sink || sink.released.length === 0) return;
|
|
7623
9319
|
sink.releasedRequests.add(request);
|
|
7624
9320
|
sink.releasedQuietSince = Date.now();
|
|
@@ -7629,16 +9325,18 @@ function instrumentPage(page) {
|
|
|
7629
9325
|
sink.releasedQuietSince = Date.now();
|
|
7630
9326
|
if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
|
|
7631
9327
|
}
|
|
7632
|
-
if (!sink || RECORDED_OPEN.has(request)) return;
|
|
9328
|
+
if (!sink || RECORDED_OPEN.has(request) || HELD_REQUESTS.has(request)) return;
|
|
7633
9329
|
sink.lastActivePage = page;
|
|
7634
9330
|
const response = sink.openResponses.get(request) ?? null;
|
|
7635
9331
|
sink.openResponses.delete(request);
|
|
7636
9332
|
recordRequest(sink, request, failed ? "failed" : "finished", response);
|
|
9333
|
+
if (!failed) void keepResponse(request, response);
|
|
7637
9334
|
};
|
|
7638
9335
|
page.on("requestfinished", (request) => onRequestEnd(request, false));
|
|
7639
9336
|
page.on("requestfailed", (request) => onRequestEnd(request, true));
|
|
7640
9337
|
}
|
|
7641
9338
|
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
9339
|
+
var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
7642
9340
|
function isTrackedRequest(url, resourceType) {
|
|
7643
9341
|
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
7644
9342
|
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
@@ -7700,6 +9398,7 @@ function readResponseHeaders(sink, entry, url, headers) {
|
|
|
7700
9398
|
}
|
|
7701
9399
|
}
|
|
7702
9400
|
function recordApiCall(sink, call) {
|
|
9401
|
+
if (sink.frozenAt !== null) return;
|
|
7703
9402
|
try {
|
|
7704
9403
|
const entry = apiCallEntry(call, sink.baseURL);
|
|
7705
9404
|
if (!entry) return;
|
|
@@ -7736,7 +9435,10 @@ function instrumentContext(context) {
|
|
|
7736
9435
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
7737
9436
|
if (typeof context.once === "function") {
|
|
7738
9437
|
OPEN_CONTEXTS.add(context);
|
|
7739
|
-
context.once("close", () =>
|
|
9438
|
+
context.once("close", () => {
|
|
9439
|
+
OPEN_CONTEXTS.delete(context);
|
|
9440
|
+
CLOSING_CONTEXTS.add(context);
|
|
9441
|
+
});
|
|
7740
9442
|
}
|
|
7741
9443
|
observeOwnApiRequests(context);
|
|
7742
9444
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
@@ -7754,14 +9456,7 @@ function instrumentContext(context) {
|
|
|
7754
9456
|
if (typeof context.close === "function") {
|
|
7755
9457
|
const originalClose = context.close.bind(context);
|
|
7756
9458
|
context.close = async (...args) => {
|
|
7757
|
-
await
|
|
7758
|
-
const sink = currentSink;
|
|
7759
|
-
if (sink) {
|
|
7760
|
-
await endShownStops(sink, "closed", { context });
|
|
7761
|
-
await stashPageState(sink, { context });
|
|
7762
|
-
await maybeOpenPicker(sink, { context });
|
|
7763
|
-
await stopSinkCodeReach(sink, { context });
|
|
7764
|
-
}
|
|
9459
|
+
await settleContextClose(context);
|
|
7765
9460
|
return originalClose(...args);
|
|
7766
9461
|
};
|
|
7767
9462
|
}
|
|
@@ -7774,7 +9469,7 @@ function patchBrowser(browser) {
|
|
|
7774
9469
|
browser.newPage = async (...args) => {
|
|
7775
9470
|
const page = await originalNewPage(...args);
|
|
7776
9471
|
instrumentPage(page);
|
|
7777
|
-
await
|
|
9472
|
+
await armStops([pageContext(page)]);
|
|
7778
9473
|
await awaitFlakeConditions();
|
|
7779
9474
|
await maybeStartCodeReach(page);
|
|
7780
9475
|
return page;
|
|
@@ -7783,13 +9478,16 @@ function patchBrowser(browser) {
|
|
|
7783
9478
|
browser.newContext = async (...args) => {
|
|
7784
9479
|
const context = await originalNewContext(...args);
|
|
7785
9480
|
instrumentContext(context);
|
|
7786
|
-
await
|
|
9481
|
+
await armStops([context]);
|
|
7787
9482
|
return context;
|
|
7788
9483
|
};
|
|
7789
9484
|
if (typeof browser.close === "function") {
|
|
7790
9485
|
const originalClose = browser.close.bind(browser);
|
|
7791
9486
|
browser.close = async (...args) => {
|
|
7792
9487
|
await drainPendingProbes(1e3);
|
|
9488
|
+
for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
|
|
9489
|
+
await settleContextClose(context);
|
|
9490
|
+
}
|
|
7793
9491
|
return originalClose(...args);
|
|
7794
9492
|
};
|
|
7795
9493
|
}
|
|
@@ -7842,7 +9540,11 @@ async function flushSink(sink, testInfo) {
|
|
|
7842
9540
|
clearTimeout(drainDeadline);
|
|
7843
9541
|
const page = sink.lastActivePage;
|
|
7844
9542
|
const pageReadable = page !== null && !isPageClosed(page);
|
|
7845
|
-
if (pageReadable)
|
|
9543
|
+
if (pageReadable) {
|
|
9544
|
+
await holdFailure(sink);
|
|
9545
|
+
await maybeOpenPicker(sink);
|
|
9546
|
+
}
|
|
9547
|
+
const live = pageReadable && sink.frozenAt === null;
|
|
7846
9548
|
if (sink.stops.length > 0) {
|
|
7847
9549
|
await testInfo.attach(ATTACHMENT_NAMES.pauses, {
|
|
7848
9550
|
contentType: "application/json",
|
|
@@ -7879,6 +9581,21 @@ async function flushSink(sink, testInfo) {
|
|
|
7879
9581
|
body: Buffer.from(JSON.stringify(part))
|
|
7880
9582
|
});
|
|
7881
9583
|
}
|
|
9584
|
+
if (cssCoverageInWorker()) {
|
|
9585
|
+
const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
|
|
9586
|
+
if (rules) {
|
|
9587
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
|
|
9588
|
+
contentType: "application/json",
|
|
9589
|
+
body: Buffer.from(JSON.stringify(rules))
|
|
9590
|
+
});
|
|
9591
|
+
}
|
|
9592
|
+
for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
|
|
9593
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
|
|
9594
|
+
contentType: "application/json",
|
|
9595
|
+
body: Buffer.from(JSON.stringify(part))
|
|
9596
|
+
});
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
7882
9599
|
if (sink.serverCoverage.size > 0) {
|
|
7883
9600
|
const requests = {};
|
|
7884
9601
|
for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
|
|
@@ -7895,13 +9612,13 @@ async function flushSink(sink, testInfo) {
|
|
|
7895
9612
|
}
|
|
7896
9613
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
7897
9614
|
try {
|
|
7898
|
-
const snapshot = (
|
|
9615
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
7899
9616
|
if (snapshot) {
|
|
7900
9617
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
7901
|
-
contentType:
|
|
9618
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
7902
9619
|
body: snapshot
|
|
7903
9620
|
});
|
|
7904
|
-
const snapshotJson = (
|
|
9621
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
7905
9622
|
if (snapshotJson) {
|
|
7906
9623
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
7907
9624
|
contentType: "application/json",
|
|
@@ -7926,7 +9643,7 @@ async function flushSink(sink, testInfo) {
|
|
|
7926
9643
|
}
|
|
7927
9644
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
7928
9645
|
const option = sink.failureScreenshot;
|
|
7929
|
-
const shot = (
|
|
9646
|
+
const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
7930
9647
|
if (shot) {
|
|
7931
9648
|
try {
|
|
7932
9649
|
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
@@ -7936,13 +9653,13 @@ async function flushSink(sink, testInfo) {
|
|
|
7936
9653
|
}
|
|
7937
9654
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
7938
9655
|
try {
|
|
7939
|
-
const snapshot = (
|
|
9656
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
7940
9657
|
if (snapshot) {
|
|
7941
9658
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
7942
|
-
contentType:
|
|
9659
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
7943
9660
|
body: snapshot
|
|
7944
9661
|
});
|
|
7945
|
-
const snapshotJson = (
|
|
9662
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
7946
9663
|
if (snapshotJson) {
|
|
7947
9664
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
7948
9665
|
contentType: "application/json",
|
|
@@ -7988,7 +9705,7 @@ async function flushSink(sink, testInfo) {
|
|
|
7988
9705
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
7989
9706
|
});
|
|
7990
9707
|
}
|
|
7991
|
-
const webVitals = (
|
|
9708
|
+
const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
7992
9709
|
if (webVitals) {
|
|
7993
9710
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
7994
9711
|
contentType: "application/json",
|
|
@@ -7996,7 +9713,7 @@ async function flushSink(sink, testInfo) {
|
|
|
7996
9713
|
});
|
|
7997
9714
|
}
|
|
7998
9715
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
7999
|
-
const pageState = (
|
|
9716
|
+
const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
8000
9717
|
if (pageState) {
|
|
8001
9718
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
8002
9719
|
contentType: "application/json",
|
|
@@ -8005,7 +9722,7 @@ async function flushSink(sink, testInfo) {
|
|
|
8005
9722
|
}
|
|
8006
9723
|
}
|
|
8007
9724
|
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
8008
|
-
if (
|
|
9725
|
+
if (live) await recordPageWindow(sink, page);
|
|
8009
9726
|
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
8010
9727
|
if (windows.length > 0) {
|
|
8011
9728
|
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
@@ -8015,21 +9732,79 @@ async function flushSink(sink, testInfo) {
|
|
|
8015
9732
|
}
|
|
8016
9733
|
}
|
|
8017
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
|
+
}
|
|
8018
9789
|
var piwiFixtures = {
|
|
8019
9790
|
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
8020
9791
|
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
8021
9792
|
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
8022
9793
|
// ledger and records the calls the tests make through it, and torn down after
|
|
8023
9794
|
// the browser closed, so its census sees what only the worker's shutdown
|
|
8024
|
-
// 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.
|
|
8025
9798
|
piwiResources: [
|
|
8026
9799
|
async ({ playwright }, use, workerInfo) => {
|
|
8027
9800
|
boxCaptureFrames();
|
|
8028
9801
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
8029
9802
|
observeApiRequests(playwright, apiCallRecorder);
|
|
9803
|
+
await openRunChannel(workerInfo, runControl);
|
|
8030
9804
|
try {
|
|
8031
9805
|
await use();
|
|
8032
9806
|
} finally {
|
|
9807
|
+
closeRunChannel();
|
|
8033
9808
|
stopResourceLedger();
|
|
8034
9809
|
}
|
|
8035
9810
|
},
|
|
@@ -8049,10 +9824,12 @@ var piwiFixtures = {
|
|
|
8049
9824
|
// never double-wraps a page the browser patch already instrumented.
|
|
8050
9825
|
page: async ({ page }, use) => {
|
|
8051
9826
|
instrumentPage(page);
|
|
8052
|
-
await
|
|
9827
|
+
await armStops([pageContext(page)]);
|
|
8053
9828
|
await awaitFlakeConditions();
|
|
8054
9829
|
await maybeStartCodeReach(page);
|
|
8055
9830
|
await use(page);
|
|
9831
|
+
const sink = currentSink;
|
|
9832
|
+
if (sink) await holdFailure(sink, page);
|
|
8056
9833
|
},
|
|
8057
9834
|
// Auto, test-scoped: open a capture sink for the running test and flush it
|
|
8058
9835
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
@@ -8067,12 +9844,16 @@ var piwiFixtures = {
|
|
|
8067
9844
|
sink.baseURL = baseURL ?? null;
|
|
8068
9845
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
8069
9846
|
const pauseSet = currentPauseSet();
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
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);
|
|
8074
9852
|
}
|
|
9853
|
+
pauseNextTest = false;
|
|
8075
9854
|
if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
|
|
9855
|
+
if (channel) tellHub(testBegin(testInfo, cannotPause));
|
|
9856
|
+
if (channel && sink.pause) startKeeping();
|
|
8076
9857
|
if (isFlakeMode()) {
|
|
8077
9858
|
const plan = loadFlakePlan();
|
|
8078
9859
|
const role = flakeRoleForTest(plan, {
|
|
@@ -8096,14 +9877,18 @@ var piwiFixtures = {
|
|
|
8096
9877
|
});
|
|
8097
9878
|
activeResourceLedger()?.testStarted(testInfo);
|
|
8098
9879
|
currentSink = sink;
|
|
9880
|
+
const showRunChannel = hideRunChannel();
|
|
8099
9881
|
try {
|
|
8100
|
-
await
|
|
9882
|
+
await armStops(OPEN_CONTEXTS);
|
|
8101
9883
|
await startCodeReachOnOpenPages();
|
|
8102
9884
|
await use();
|
|
8103
9885
|
} finally {
|
|
9886
|
+
sink.ending = true;
|
|
8104
9887
|
await endShownStops(sink, "ended");
|
|
8105
9888
|
await awaitReleasedCalls(sink);
|
|
9889
|
+
showRunChannel();
|
|
8106
9890
|
currentSink = null;
|
|
9891
|
+
if (sink.pause?.pause) pauseNextTest = true;
|
|
8107
9892
|
if (sink.probeItem) {
|
|
8108
9893
|
const applied = sink.probeInterception?.applied() ?? false;
|
|
8109
9894
|
const level = sink.probeItem.level ?? "client";
|
|
@@ -8126,7 +9911,12 @@ var piwiFixtures = {
|
|
|
8126
9911
|
});
|
|
8127
9912
|
}
|
|
8128
9913
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
8129
|
-
|
|
9914
|
+
try {
|
|
9915
|
+
await flushSink(sink, testInfo);
|
|
9916
|
+
} finally {
|
|
9917
|
+
stopKeeping();
|
|
9918
|
+
if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
|
|
9919
|
+
}
|
|
8130
9920
|
await recordResourceCensus(testInfo);
|
|
8131
9921
|
}
|
|
8132
9922
|
},
|
|
@@ -8172,6 +9962,8 @@ function extendPiwiFixtures(test) {
|
|
|
8172
9962
|
computeCoreVitals,
|
|
8173
9963
|
configuredCodeReachRoots,
|
|
8174
9964
|
extendPiwiFixtures,
|
|
9965
|
+
failureError,
|
|
9966
|
+
intentAt,
|
|
8175
9967
|
internalCall,
|
|
8176
9968
|
noteLocatorCall,
|
|
8177
9969
|
noteNavigation,
|