@piwitests/reporter 0.35.0 → 0.38.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 +281 -8
- package/dist/global-setup-module.js +168 -15
- package/dist/index.d.ts +33 -4
- package/dist/index.js +1085 -151
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +1 -0
- package/dist/internal/capture/capture-fixtures.js +911 -85
- package/dist/internal/capture/pick-on-failure.d.ts +5 -1
- package/dist/internal/capture/pick-on-failure.js +284 -79
- package/package.json +2 -2
- package/templates/skills/write-the-missing-test/SKILL.md +69 -0
|
@@ -40,7 +40,7 @@ __export(capture_fixtures_exports, {
|
|
|
40
40
|
probeElementAttrs: () => probeElementAttrs
|
|
41
41
|
});
|
|
42
42
|
module.exports = __toCommonJS(capture_fixtures_exports);
|
|
43
|
-
var
|
|
43
|
+
var import_node_zlib2 = require("zlib");
|
|
44
44
|
|
|
45
45
|
// ../picker-dom/src/probe.ts
|
|
46
46
|
function probeElementAttrs(el, arg) {
|
|
@@ -1742,6 +1742,7 @@ var ATTACHMENT_NAMES = {
|
|
|
1742
1742
|
console: "piwi-console",
|
|
1743
1743
|
dialogs: "piwi-dialogs",
|
|
1744
1744
|
network: "piwi-network",
|
|
1745
|
+
pageInventory: "piwi-page-inventory",
|
|
1745
1746
|
webVitals: "piwi-web-vitals",
|
|
1746
1747
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
1747
1748
|
pageState: "piwi-page-state",
|
|
@@ -1753,6 +1754,512 @@ var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
|
1753
1754
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
1754
1755
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
1755
1756
|
|
|
1757
|
+
// src/internal/capture/page-inventory.ts
|
|
1758
|
+
var PAGE_INVENTORY_MAX_ENTRIES = 500;
|
|
1759
|
+
function collectPageInventoryInPage(maxEntries) {
|
|
1760
|
+
const g = globalThis;
|
|
1761
|
+
const doc = g.document;
|
|
1762
|
+
if (!doc || !g.location) return null;
|
|
1763
|
+
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1764
|
+
const roleOf = (el) => {
|
|
1765
|
+
const explicit = el.getAttribute("role");
|
|
1766
|
+
if (explicit) return explicit.toLowerCase();
|
|
1767
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
1768
|
+
if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
|
|
1769
|
+
if (tag === "button") return "button";
|
|
1770
|
+
if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
1771
|
+
if (tag === "textarea") return "textbox";
|
|
1772
|
+
if (tag === "input") {
|
|
1773
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
1774
|
+
const map = {
|
|
1775
|
+
checkbox: "checkbox",
|
|
1776
|
+
radio: "radio",
|
|
1777
|
+
button: "button",
|
|
1778
|
+
submit: "button",
|
|
1779
|
+
reset: "button",
|
|
1780
|
+
image: "button",
|
|
1781
|
+
range: "slider",
|
|
1782
|
+
search: "searchbox",
|
|
1783
|
+
email: "textbox",
|
|
1784
|
+
tel: "textbox",
|
|
1785
|
+
url: "textbox",
|
|
1786
|
+
number: "spinbutton"
|
|
1787
|
+
};
|
|
1788
|
+
return map[type] || "textbox";
|
|
1789
|
+
}
|
|
1790
|
+
return "generic";
|
|
1791
|
+
};
|
|
1792
|
+
const holdsValue = (el, role) => {
|
|
1793
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
1794
|
+
if (tag === "select" || tag === "textarea" || tag === "input") return true;
|
|
1795
|
+
if (el.isContentEditable === true) return true;
|
|
1796
|
+
const editable = el.getAttribute("contenteditable");
|
|
1797
|
+
if (editable === "" || editable === "true") return true;
|
|
1798
|
+
return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
|
|
1799
|
+
};
|
|
1800
|
+
const labelText = (el) => {
|
|
1801
|
+
const labels = el.labels;
|
|
1802
|
+
if (!labels || labels.length === 0) return "";
|
|
1803
|
+
let text = "";
|
|
1804
|
+
for (const label of labels) text += ` ${label.textContent || ""}`;
|
|
1805
|
+
return text;
|
|
1806
|
+
};
|
|
1807
|
+
const labelledByText = (el) => {
|
|
1808
|
+
const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
1809
|
+
if (ids.length === 0) return "";
|
|
1810
|
+
let text = "";
|
|
1811
|
+
for (const id of ids) {
|
|
1812
|
+
const ref = doc.getElementById(id);
|
|
1813
|
+
if (ref) text += ` ${ref.textContent || ""}`;
|
|
1814
|
+
}
|
|
1815
|
+
return text;
|
|
1816
|
+
};
|
|
1817
|
+
const nameOf = (el) => {
|
|
1818
|
+
const role = roleOf(el);
|
|
1819
|
+
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
1820
|
+
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
1821
|
+
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
1822
|
+
return clean(chain.find((s) => s && String(s).trim()) || "");
|
|
1823
|
+
};
|
|
1824
|
+
const cleanHref = (href) => {
|
|
1825
|
+
const cut = href.search(/[?#]/);
|
|
1826
|
+
return cut >= 0 ? href.slice(0, cut) : href;
|
|
1827
|
+
};
|
|
1828
|
+
const controls = [];
|
|
1829
|
+
const links = [];
|
|
1830
|
+
const seenC = /* @__PURE__ */ new Set();
|
|
1831
|
+
const seenL = /* @__PURE__ */ new Set();
|
|
1832
|
+
let budget = maxEntries;
|
|
1833
|
+
const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
|
|
1834
|
+
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
1835
|
+
if (budget <= 0) break;
|
|
1836
|
+
const role = roleOf(el);
|
|
1837
|
+
const name = nameOf(el);
|
|
1838
|
+
if (!name) continue;
|
|
1839
|
+
const key = `${role}\0${name}`;
|
|
1840
|
+
if (seenC.has(key)) continue;
|
|
1841
|
+
seenC.add(key);
|
|
1842
|
+
controls.push({ role, name });
|
|
1843
|
+
budget--;
|
|
1844
|
+
}
|
|
1845
|
+
for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
|
|
1846
|
+
if (budget <= 0) break;
|
|
1847
|
+
const name = nameOf(el);
|
|
1848
|
+
const href = cleanHref(el.getAttribute("href") || "");
|
|
1849
|
+
if (!name) continue;
|
|
1850
|
+
const key = `${name}\0${href}`;
|
|
1851
|
+
if (seenL.has(key)) continue;
|
|
1852
|
+
seenL.add(key);
|
|
1853
|
+
links.push({ name, href });
|
|
1854
|
+
budget--;
|
|
1855
|
+
}
|
|
1856
|
+
return { url: g.location.href, controls, links };
|
|
1857
|
+
}
|
|
1858
|
+
function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
|
|
1859
|
+
const controls = inventory.controls.slice(0, Math.max(0, max));
|
|
1860
|
+
const links = inventory.links.slice(0, Math.max(0, max - controls.length));
|
|
1861
|
+
return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
|
|
1862
|
+
}
|
|
1863
|
+
function inventoryPageKey(url) {
|
|
1864
|
+
try {
|
|
1865
|
+
const u = new URL(url);
|
|
1866
|
+
return `${u.origin}${u.pathname}`;
|
|
1867
|
+
} catch {
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/internal/probe/mode.ts
|
|
1873
|
+
var fs2 = __toESM(require("fs"));
|
|
1874
|
+
var path3 = __toESM(require("path"));
|
|
1875
|
+
|
|
1876
|
+
// src/internal/config/desktop.ts
|
|
1877
|
+
var fs = __toESM(require("fs"));
|
|
1878
|
+
var os = __toESM(require("os"));
|
|
1879
|
+
var path2 = __toESM(require("path"));
|
|
1880
|
+
|
|
1881
|
+
// src/internal/config/env.ts
|
|
1882
|
+
var DEFAULTS = {
|
|
1883
|
+
projectName: "default-project",
|
|
1884
|
+
uploadTraces: true,
|
|
1885
|
+
uploadReport: true,
|
|
1886
|
+
liveFileUploads: true,
|
|
1887
|
+
collectScmInfo: true,
|
|
1888
|
+
collectCiInfo: true,
|
|
1889
|
+
collectPerformanceMetrics: true,
|
|
1890
|
+
captureLocators: true,
|
|
1891
|
+
capturePageState: true,
|
|
1892
|
+
// Off by default: the page inventory reads control and link names from every
|
|
1893
|
+
// visited page, so it stays opt-in until a project turns it on.
|
|
1894
|
+
capturePageInventory: false,
|
|
1895
|
+
captureServerTraces: true,
|
|
1896
|
+
sampleAriaOnPass: true,
|
|
1897
|
+
uploadManifest: true,
|
|
1898
|
+
defaultCapture: true,
|
|
1899
|
+
streaming: true,
|
|
1900
|
+
streamingBatchSize: 5,
|
|
1901
|
+
streamingBatchDelay: 2e3,
|
|
1902
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
1903
|
+
failOnFlakyTests: false,
|
|
1904
|
+
username: null,
|
|
1905
|
+
password: null,
|
|
1906
|
+
apiKey: null,
|
|
1907
|
+
verbose: false
|
|
1908
|
+
};
|
|
1909
|
+
var PIWI_ENV_KEYS = {
|
|
1910
|
+
serverUrl: "PIWI_DASHBOARD_URL",
|
|
1911
|
+
projectName: "PIWI_PROJECT_NAME",
|
|
1912
|
+
verbose: "PIWI_VERBOSE",
|
|
1913
|
+
apiKey: "PIWI_API_KEY",
|
|
1914
|
+
username: "PIWI_USERNAME",
|
|
1915
|
+
password: "PIWI_PASSWORD",
|
|
1916
|
+
environment: "PIWI_ENVIRONMENT",
|
|
1917
|
+
label: "PIWI_LABEL",
|
|
1918
|
+
runLabel: "PIWI_RUN_LABEL",
|
|
1919
|
+
keep: "PIWI_KEEP",
|
|
1920
|
+
streaming: "PIWI_STREAMING",
|
|
1921
|
+
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
1922
|
+
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
1923
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
1924
|
+
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
1925
|
+
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
1926
|
+
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
1927
|
+
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
1928
|
+
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
1929
|
+
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
1930
|
+
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
1931
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
1932
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
1933
|
+
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
1934
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
1935
|
+
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
1936
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
1937
|
+
outputFile: "PIWI_OUTPUT_FILE",
|
|
1938
|
+
aiMode: "PIWI_AI",
|
|
1939
|
+
aiDir: "PIWI_AI_DIR",
|
|
1940
|
+
aiOnMiss: "PIWI_AI_ON_MISS",
|
|
1941
|
+
aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
|
|
1942
|
+
aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
|
|
1943
|
+
aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
|
|
1944
|
+
aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
|
|
1945
|
+
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
1946
|
+
};
|
|
1947
|
+
var PIWI_PROBE_ENV = {
|
|
1948
|
+
flag: "PIWI_PROBE",
|
|
1949
|
+
plan: "PIWI_PROBE_PLAN",
|
|
1950
|
+
results: "PIWI_PROBE_RESULTS",
|
|
1951
|
+
secret: "PIWI_PROBE_SECRET"
|
|
1952
|
+
};
|
|
1953
|
+
var ENV_FALLBACK_SPECS = [
|
|
1954
|
+
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
1955
|
+
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
1956
|
+
{ option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
|
|
1957
|
+
{ option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
|
|
1958
|
+
{ option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
|
|
1959
|
+
{ option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
|
|
1960
|
+
{ option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
|
|
1961
|
+
{ option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
|
|
1962
|
+
{ option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
|
|
1963
|
+
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
1964
|
+
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
1965
|
+
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
1966
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
1967
|
+
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
1968
|
+
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
1969
|
+
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
1970
|
+
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
1971
|
+
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
1972
|
+
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
1973
|
+
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
1974
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
1975
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
1976
|
+
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
1977
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
1978
|
+
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
1979
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
1980
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
1981
|
+
];
|
|
1982
|
+
|
|
1983
|
+
// src/internal/probe/plan.ts
|
|
1984
|
+
function parseRouteKey(routeKey) {
|
|
1985
|
+
const space = routeKey.indexOf(" ");
|
|
1986
|
+
if (space < 0) return { method: "", pattern: routeKey };
|
|
1987
|
+
return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
|
|
1988
|
+
}
|
|
1989
|
+
function patternToRegex(pattern) {
|
|
1990
|
+
const path6 = pattern.split("?")[0] ?? pattern;
|
|
1991
|
+
const escaped = path6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
1992
|
+
return new RegExp(`^${escaped}$`);
|
|
1993
|
+
}
|
|
1994
|
+
function suitePathEqual(a, b) {
|
|
1995
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
1996
|
+
return a.every((title, i) => title === b[i]);
|
|
1997
|
+
}
|
|
1998
|
+
function matchProbeItem(plan, test) {
|
|
1999
|
+
const byFileTitle = plan.items.filter(
|
|
2000
|
+
(item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
|
|
2001
|
+
);
|
|
2002
|
+
if (byFileTitle.length === 1) return byFileTitle[0];
|
|
2003
|
+
if (byFileTitle.length > 1) {
|
|
2004
|
+
return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
|
|
2005
|
+
}
|
|
2006
|
+
for (const item of plan.items) {
|
|
2007
|
+
if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
function requestMatchesRoute(method, url, routeKey) {
|
|
2012
|
+
const { method: rm, pattern } = parseRouteKey(routeKey);
|
|
2013
|
+
if (rm && rm !== method.toUpperCase()) return false;
|
|
2014
|
+
let path6;
|
|
2015
|
+
try {
|
|
2016
|
+
path6 = new URL(url).pathname;
|
|
2017
|
+
} catch {
|
|
2018
|
+
path6 = url.split("?")[0] ?? url;
|
|
2019
|
+
}
|
|
2020
|
+
return patternToRegex(pattern).test(path6);
|
|
2021
|
+
}
|
|
2022
|
+
function createProbeState() {
|
|
2023
|
+
return { navigated: false, matches: 0 };
|
|
2024
|
+
}
|
|
2025
|
+
function markNavigated(state) {
|
|
2026
|
+
state.navigated = true;
|
|
2027
|
+
}
|
|
2028
|
+
function shouldMutate(state, item) {
|
|
2029
|
+
if (!state.navigated) return false;
|
|
2030
|
+
state.matches += 1;
|
|
2031
|
+
return state.matches === Math.max(1, item.nth ?? 1);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/internal/probe/mode.ts
|
|
2035
|
+
var planCache;
|
|
2036
|
+
function isProbeMode() {
|
|
2037
|
+
return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
|
|
2038
|
+
}
|
|
2039
|
+
function loadPlan() {
|
|
2040
|
+
if (planCache !== void 0) return planCache;
|
|
2041
|
+
planCache = null;
|
|
2042
|
+
const path6 = process.env[PIWI_PROBE_ENV.plan];
|
|
2043
|
+
if (path6) {
|
|
2044
|
+
try {
|
|
2045
|
+
planCache = JSON.parse(fs2.readFileSync(path6, "utf8"));
|
|
2046
|
+
} catch {
|
|
2047
|
+
planCache = null;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return planCache;
|
|
2051
|
+
}
|
|
2052
|
+
function probeItemForTest(test) {
|
|
2053
|
+
const plan = loadPlan();
|
|
2054
|
+
if (!plan) return null;
|
|
2055
|
+
const filePath = test.file ? path3.relative(process.cwd(), test.file).split(path3.sep).join("/") : null;
|
|
2056
|
+
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
2057
|
+
return matchProbeItem(plan, { title: test.title, filePath, suitePath });
|
|
2058
|
+
}
|
|
2059
|
+
function classifyProbeHandled(signals) {
|
|
2060
|
+
return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
|
|
2061
|
+
}
|
|
2062
|
+
function outcomeFromStatus(status, applied) {
|
|
2063
|
+
if (!applied) return "inconclusive";
|
|
2064
|
+
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2065
|
+
}
|
|
2066
|
+
function recordProbeOutcome(line) {
|
|
2067
|
+
const path6 = process.env[PIWI_PROBE_ENV.results];
|
|
2068
|
+
if (!path6) return;
|
|
2069
|
+
try {
|
|
2070
|
+
fs2.appendFileSync(path6, `${JSON.stringify(line)}
|
|
2071
|
+
`);
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/internal/probe/interception.ts
|
|
2077
|
+
var import_node_zlib = require("zlib");
|
|
2078
|
+
|
|
2079
|
+
// src/internal/probe/faults.ts
|
|
2080
|
+
var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
|
|
2081
|
+
var SERVER_PROBE_FAULTS = [
|
|
2082
|
+
"throw",
|
|
2083
|
+
"status",
|
|
2084
|
+
"delay",
|
|
2085
|
+
"dependency",
|
|
2086
|
+
"data",
|
|
2087
|
+
"extreme",
|
|
2088
|
+
"replay",
|
|
2089
|
+
"auth",
|
|
2090
|
+
"slow-first"
|
|
2091
|
+
];
|
|
2092
|
+
function isFaultAllowed(level, fault) {
|
|
2093
|
+
const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
|
|
2094
|
+
return list.includes(fault);
|
|
2095
|
+
}
|
|
2096
|
+
var SLOW_FAULT_DELAY_MS = 5e3;
|
|
2097
|
+
function dropOneField(body) {
|
|
2098
|
+
let parsed;
|
|
2099
|
+
try {
|
|
2100
|
+
parsed = JSON.parse(body);
|
|
2101
|
+
} catch {
|
|
2102
|
+
return body;
|
|
2103
|
+
}
|
|
2104
|
+
if (Array.isArray(parsed)) {
|
|
2105
|
+
let key;
|
|
2106
|
+
for (const el of parsed) {
|
|
2107
|
+
if (el && typeof el === "object" && !Array.isArray(el)) {
|
|
2108
|
+
key ??= Object.keys(el)[0];
|
|
2109
|
+
if (key) delete el[key];
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return JSON.stringify(parsed);
|
|
2113
|
+
}
|
|
2114
|
+
if (parsed && typeof parsed === "object") {
|
|
2115
|
+
const obj = parsed;
|
|
2116
|
+
const key = Object.keys(obj)[0];
|
|
2117
|
+
if (key) delete obj[key];
|
|
2118
|
+
return JSON.stringify(obj);
|
|
2119
|
+
}
|
|
2120
|
+
return body;
|
|
2121
|
+
}
|
|
2122
|
+
function computeFault(fault, input) {
|
|
2123
|
+
const base = {
|
|
2124
|
+
status: input.status,
|
|
2125
|
+
body: input.body,
|
|
2126
|
+
contentType: input.contentType ?? null,
|
|
2127
|
+
delayMs: 0,
|
|
2128
|
+
changed: false
|
|
2129
|
+
};
|
|
2130
|
+
switch (fault) {
|
|
2131
|
+
case "status-500":
|
|
2132
|
+
return { ...base, status: 500, changed: input.status !== 500 };
|
|
2133
|
+
case "empty-body":
|
|
2134
|
+
return { ...base, body: "", changed: input.body.length > 0 };
|
|
2135
|
+
case "drop-field": {
|
|
2136
|
+
const body = dropOneField(input.body);
|
|
2137
|
+
return { ...base, body, changed: body !== input.body };
|
|
2138
|
+
}
|
|
2139
|
+
case "stale-value": {
|
|
2140
|
+
const body = input.priorBody ?? input.body;
|
|
2141
|
+
return { ...base, body, changed: body !== input.body };
|
|
2142
|
+
}
|
|
2143
|
+
case "slow":
|
|
2144
|
+
return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
|
|
2145
|
+
default:
|
|
2146
|
+
return base;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/internal/probe/sign.ts
|
|
2151
|
+
var import_node_crypto = require("crypto");
|
|
2152
|
+
function signProbeMessage(secret, nonce, ts, specJson) {
|
|
2153
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
|
|
2154
|
+
}
|
|
2155
|
+
function buildProbeHeader(secret, spec, now = Date.now()) {
|
|
2156
|
+
const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
2157
|
+
const specJson = JSON.stringify(spec);
|
|
2158
|
+
const sig = signProbeMessage(secret, nonce, now, specJson);
|
|
2159
|
+
return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/internal/probe/interception.ts
|
|
2163
|
+
function parseProbeTrace(traceHeader) {
|
|
2164
|
+
const none = { appliedFault: null, serverError: false };
|
|
2165
|
+
if (!traceHeader) return none;
|
|
2166
|
+
try {
|
|
2167
|
+
const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
|
|
2168
|
+
if (!Array.isArray(spans)) return none;
|
|
2169
|
+
const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
|
|
2170
|
+
if (!root) return none;
|
|
2171
|
+
const attrs = root.attrs ?? {};
|
|
2172
|
+
const appliedRaw = attrs["piwi.probe.applied"];
|
|
2173
|
+
const status = Number(attrs["http.status_code"]) || 0;
|
|
2174
|
+
return {
|
|
2175
|
+
appliedFault: appliedRaw != null ? String(appliedRaw) : null,
|
|
2176
|
+
serverError: root.status === "error" || status >= 500
|
|
2177
|
+
};
|
|
2178
|
+
} catch {
|
|
2179
|
+
return none;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
function fulfillHeaders(responseHeaders, contentType) {
|
|
2183
|
+
const headers = { ...responseHeaders };
|
|
2184
|
+
delete headers["content-length"];
|
|
2185
|
+
delete headers["content-encoding"];
|
|
2186
|
+
if (contentType) headers["content-type"] = contentType;
|
|
2187
|
+
return headers;
|
|
2188
|
+
}
|
|
2189
|
+
function originOf(url) {
|
|
2190
|
+
try {
|
|
2191
|
+
const origin = new URL(url).origin;
|
|
2192
|
+
return origin && origin !== "null" ? origin : null;
|
|
2193
|
+
} catch {
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function installProbeInterception(page, item) {
|
|
2198
|
+
if (!isFaultAllowed(item.level ?? "client", item.fault)) {
|
|
2199
|
+
return { applied: () => false, appliedFault: () => null, serverError: () => false };
|
|
2200
|
+
}
|
|
2201
|
+
const state = createProbeState();
|
|
2202
|
+
let applied = false;
|
|
2203
|
+
let appliedFault = null;
|
|
2204
|
+
let serverError = false;
|
|
2205
|
+
let appOrigin = null;
|
|
2206
|
+
const probeSecret = process.env[PIWI_PROBE_ENV.secret];
|
|
2207
|
+
page.on("framenavigated", (frame) => {
|
|
2208
|
+
if (frame === page.mainFrame()) {
|
|
2209
|
+
markNavigated(state);
|
|
2210
|
+
const origin = originOf(page.url());
|
|
2211
|
+
if (origin) appOrigin = origin;
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
await page.route("**/*", async (route) => {
|
|
2215
|
+
const request = route.request();
|
|
2216
|
+
if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
|
|
2217
|
+
await route.fallback();
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (item.level === "server") {
|
|
2221
|
+
if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
|
|
2222
|
+
await route.fallback();
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
|
|
2226
|
+
if (item.dependency) spec.dependency = item.dependency;
|
|
2227
|
+
try {
|
|
2228
|
+
const response = await route.fetch({
|
|
2229
|
+
headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
|
|
2230
|
+
});
|
|
2231
|
+
const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
|
|
2232
|
+
appliedFault = trace.appliedFault;
|
|
2233
|
+
serverError = trace.serverError;
|
|
2234
|
+
applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
|
|
2235
|
+
await route.fulfill({ response });
|
|
2236
|
+
} catch {
|
|
2237
|
+
applied = false;
|
|
2238
|
+
await route.fallback();
|
|
2239
|
+
}
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
const response = await route.fetch();
|
|
2244
|
+
const body = await response.text();
|
|
2245
|
+
const out = computeFault(item.fault, {
|
|
2246
|
+
status: response.status(),
|
|
2247
|
+
body,
|
|
2248
|
+
contentType: response.headers()["content-type"] ?? null
|
|
2249
|
+
});
|
|
2250
|
+
applied = out.changed;
|
|
2251
|
+
if (applied) appliedFault = item.fault;
|
|
2252
|
+
if (out.delayMs > 0) await new Promise((resolve) => setTimeout(resolve, out.delayMs));
|
|
2253
|
+
const headers = fulfillHeaders(response.headers(), out.contentType);
|
|
2254
|
+
await route.fulfill({ status: out.status, body: out.body, headers });
|
|
2255
|
+
} catch {
|
|
2256
|
+
applied = false;
|
|
2257
|
+
await route.fallback();
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1756
2263
|
// src/internal/capture/inspect-on-failure.ts
|
|
1757
2264
|
function isCi(ci) {
|
|
1758
2265
|
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
@@ -1789,88 +2296,295 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
1789
2296
|
}
|
|
1790
2297
|
|
|
1791
2298
|
// src/internal/capture/pick-on-failure.ts
|
|
1792
|
-
var
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
2299
|
+
var path4 = __toESM(require("path"));
|
|
2300
|
+
|
|
2301
|
+
// ../core/src/locator-chain.ts
|
|
2302
|
+
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
2303
|
+
"getByRole",
|
|
2304
|
+
"getByTestId",
|
|
2305
|
+
"getByText",
|
|
2306
|
+
"getByLabel",
|
|
2307
|
+
"getByPlaceholder",
|
|
2308
|
+
"getByAltText",
|
|
2309
|
+
"getByTitle",
|
|
2310
|
+
"locator",
|
|
2311
|
+
"frameLocator"
|
|
2312
|
+
]);
|
|
2313
|
+
var NARROWING_METHODS = /* @__PURE__ */ new Set([
|
|
2314
|
+
"filter",
|
|
2315
|
+
"first",
|
|
2316
|
+
"last",
|
|
2317
|
+
"nth",
|
|
2318
|
+
"and",
|
|
2319
|
+
"or",
|
|
2320
|
+
"visible",
|
|
2321
|
+
"contentFrame",
|
|
2322
|
+
"owner"
|
|
2323
|
+
]);
|
|
2324
|
+
var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
|
|
2325
|
+
var LocatorParseError = class extends Error {
|
|
2326
|
+
constructor(message) {
|
|
2327
|
+
super(message);
|
|
2328
|
+
this.name = "LocatorParseError";
|
|
1802
2329
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2330
|
+
};
|
|
2331
|
+
var SIMPLE_ESCAPES = {
|
|
2332
|
+
n: "\n",
|
|
2333
|
+
t: " ",
|
|
2334
|
+
r: "\r",
|
|
2335
|
+
b: "\b",
|
|
2336
|
+
f: "\f",
|
|
2337
|
+
v: "\v",
|
|
2338
|
+
"0": "\0"
|
|
2339
|
+
};
|
|
2340
|
+
var Parser = class {
|
|
2341
|
+
constructor(src) {
|
|
2342
|
+
this.src = src;
|
|
2343
|
+
this.i = 0;
|
|
2344
|
+
}
|
|
2345
|
+
parseTopLevel() {
|
|
2346
|
+
const chain = this.parseChain();
|
|
2347
|
+
this.skipSpace();
|
|
2348
|
+
if (this.i < this.src.length) this.fail("unexpected text");
|
|
2349
|
+
return chain;
|
|
2350
|
+
}
|
|
2351
|
+
fail(message) {
|
|
2352
|
+
throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
|
|
2353
|
+
}
|
|
2354
|
+
skipSpace() {
|
|
2355
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
2356
|
+
}
|
|
2357
|
+
peek() {
|
|
2358
|
+
return this.src[this.i];
|
|
2359
|
+
}
|
|
2360
|
+
expect(ch) {
|
|
2361
|
+
this.skipSpace();
|
|
2362
|
+
if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
|
|
2363
|
+
this.i++;
|
|
2364
|
+
}
|
|
2365
|
+
identifier() {
|
|
2366
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
|
|
2367
|
+
if (!m) this.fail("expected a name");
|
|
2368
|
+
this.i += m[0].length;
|
|
2369
|
+
return m[0];
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* A chain of calls joined by `.`. With `lenient`, parsing stops before the
|
|
2373
|
+
* first link that doesn't parse instead of failing, so a chain followed by
|
|
2374
|
+
* other text (an error message) is read up to where it ends. `spans` receives
|
|
2375
|
+
* where each top-level call starts and ends.
|
|
2376
|
+
*/
|
|
2377
|
+
parseChain(opts = {}) {
|
|
2378
|
+
this.skipSpace();
|
|
2379
|
+
const calls = [];
|
|
2380
|
+
for (; ; ) {
|
|
2381
|
+
const before = this.i;
|
|
2382
|
+
if (calls.length > 0) this.i++;
|
|
2383
|
+
this.skipSpace();
|
|
2384
|
+
const start = this.i;
|
|
2385
|
+
try {
|
|
2386
|
+
calls.push(this.parseCall(calls.length === 0));
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
if (!opts.lenient || calls.length === 0) throw error;
|
|
2389
|
+
this.i = before;
|
|
2390
|
+
break;
|
|
2391
|
+
}
|
|
2392
|
+
opts.spans?.push({ start, end: this.i });
|
|
2393
|
+
const end = this.i;
|
|
2394
|
+
this.skipSpace();
|
|
2395
|
+
if (this.peek() !== ".") {
|
|
2396
|
+
this.i = end;
|
|
2397
|
+
break;
|
|
2398
|
+
}
|
|
1841
2399
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2400
|
+
return { calls };
|
|
2401
|
+
}
|
|
2402
|
+
/** Where the parser stopped. */
|
|
2403
|
+
get position() {
|
|
2404
|
+
return this.i;
|
|
2405
|
+
}
|
|
2406
|
+
parseCall(first) {
|
|
2407
|
+
this.skipSpace();
|
|
2408
|
+
const method = this.identifier();
|
|
2409
|
+
if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
|
|
2410
|
+
if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
|
|
2411
|
+
this.expect("(");
|
|
2412
|
+
const args = [];
|
|
2413
|
+
this.skipSpace();
|
|
2414
|
+
if (this.peek() === ")") {
|
|
2415
|
+
this.i++;
|
|
2416
|
+
return { method, args };
|
|
2417
|
+
}
|
|
2418
|
+
for (; ; ) {
|
|
2419
|
+
args.push(this.parseValue());
|
|
2420
|
+
this.skipSpace();
|
|
2421
|
+
if (this.peek() === ",") {
|
|
2422
|
+
this.i++;
|
|
2423
|
+
this.skipSpace();
|
|
2424
|
+
if (this.peek() === ")") {
|
|
2425
|
+
this.i++;
|
|
2426
|
+
break;
|
|
2427
|
+
}
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
this.expect(")");
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
return { method, args };
|
|
2434
|
+
}
|
|
2435
|
+
parseValue() {
|
|
2436
|
+
this.skipSpace();
|
|
2437
|
+
const ch = this.peek();
|
|
2438
|
+
if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
|
|
2439
|
+
if (ch === "/") return this.parseRegex();
|
|
2440
|
+
if (ch === "{") return this.parseObject();
|
|
2441
|
+
if (ch !== void 0 && /[-\d]/.test(ch)) {
|
|
2442
|
+
const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
|
|
2443
|
+
if (!m) this.fail("expected a number");
|
|
2444
|
+
this.i += m[0].length;
|
|
2445
|
+
return { type: "number", value: Number(m[0]) };
|
|
2446
|
+
}
|
|
2447
|
+
const rest = this.src.slice(this.i);
|
|
2448
|
+
if (/^true\b/.test(rest)) {
|
|
2449
|
+
this.i += 4;
|
|
2450
|
+
return { type: "boolean", value: true };
|
|
2451
|
+
}
|
|
2452
|
+
if (/^false\b/.test(rest)) {
|
|
2453
|
+
this.i += 5;
|
|
2454
|
+
return { type: "boolean", value: false };
|
|
2455
|
+
}
|
|
2456
|
+
if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
|
|
2457
|
+
return this.fail("expected a value");
|
|
2458
|
+
}
|
|
2459
|
+
/** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
|
|
2460
|
+
parseString() {
|
|
2461
|
+
const quote = this.src[this.i];
|
|
2462
|
+
let out = "";
|
|
2463
|
+
this.i++;
|
|
2464
|
+
while (this.i < this.src.length) {
|
|
2465
|
+
const c = this.src[this.i];
|
|
2466
|
+
if (c === quote) {
|
|
2467
|
+
this.i++;
|
|
2468
|
+
return out;
|
|
2469
|
+
}
|
|
2470
|
+
if (c !== "\\") {
|
|
2471
|
+
out += c;
|
|
2472
|
+
this.i++;
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const next = this.src[this.i + 1];
|
|
2476
|
+
if (next === void 0) break;
|
|
2477
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
2478
|
+
if (simple !== void 0) {
|
|
2479
|
+
out += simple;
|
|
2480
|
+
this.i += 2;
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
|
|
2484
|
+
if (hex) {
|
|
2485
|
+
out += String.fromCodePoint(parseInt(hex[1], 16));
|
|
2486
|
+
this.i += 1 + hex[0].length;
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (next === "u" || next === "x") this.fail("malformed escape");
|
|
2490
|
+
out += next;
|
|
2491
|
+
this.i += 2;
|
|
2492
|
+
}
|
|
2493
|
+
return this.fail("unterminated string");
|
|
2494
|
+
}
|
|
2495
|
+
parseRegex() {
|
|
2496
|
+
let j = this.i + 1;
|
|
2497
|
+
let inClass = false;
|
|
2498
|
+
while (j < this.src.length) {
|
|
2499
|
+
const c = this.src[j];
|
|
2500
|
+
if (c === "\\") {
|
|
2501
|
+
j += 2;
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
if (c === "[") inClass = true;
|
|
2505
|
+
else if (c === "]") inClass = false;
|
|
2506
|
+
else if (c === "/" && !inClass) break;
|
|
2507
|
+
j++;
|
|
2508
|
+
}
|
|
2509
|
+
if (j >= this.src.length) this.fail("unterminated regex");
|
|
2510
|
+
const source = this.src.slice(this.i + 1, j);
|
|
2511
|
+
const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
|
|
2512
|
+
this.i = j + 1 + flags.length;
|
|
2513
|
+
return { type: "regex", source, flags };
|
|
2514
|
+
}
|
|
2515
|
+
parseObject() {
|
|
2516
|
+
this.expect("{");
|
|
2517
|
+
const entries = [];
|
|
2518
|
+
for (; ; ) {
|
|
2519
|
+
this.skipSpace();
|
|
2520
|
+
if (this.peek() === "}") {
|
|
2521
|
+
this.i++;
|
|
2522
|
+
break;
|
|
2523
|
+
}
|
|
2524
|
+
const ch = this.peek();
|
|
2525
|
+
const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
|
|
2526
|
+
this.expect(":");
|
|
2527
|
+
entries.push([key, this.parseValue()]);
|
|
2528
|
+
this.skipSpace();
|
|
2529
|
+
if (this.peek() === ",") {
|
|
2530
|
+
this.i++;
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
this.expect("}");
|
|
2534
|
+
break;
|
|
1847
2535
|
}
|
|
1848
|
-
|
|
2536
|
+
return { type: "object", entries };
|
|
1849
2537
|
}
|
|
1850
|
-
|
|
2538
|
+
};
|
|
2539
|
+
function parseLocatorChain(expr) {
|
|
2540
|
+
const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
|
|
2541
|
+
if (!trimmed) throw new LocatorParseError("empty expression");
|
|
2542
|
+
return new Parser(trimmed).parseTopLevel();
|
|
1851
2543
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
if (c === "'" || c === '"') {
|
|
1858
|
-
i = endOfString(expr, i);
|
|
1859
|
-
continue;
|
|
1860
|
-
}
|
|
1861
|
-
if (c === "(") depth++;
|
|
1862
|
-
else if (c === ")") {
|
|
1863
|
-
depth--;
|
|
1864
|
-
if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
|
|
1865
|
-
}
|
|
2544
|
+
function tryParseLocatorChain(expr) {
|
|
2545
|
+
try {
|
|
2546
|
+
return parseLocatorChain(expr);
|
|
2547
|
+
} catch {
|
|
2548
|
+
return null;
|
|
1866
2549
|
}
|
|
1867
|
-
return expr.slice(leafStart);
|
|
1868
2550
|
}
|
|
2551
|
+
function parseLeafLocatorCall(expr) {
|
|
2552
|
+
const chain = tryParseLocatorChain(expr);
|
|
2553
|
+
if (!chain) return null;
|
|
2554
|
+
for (let i = chain.calls.length - 1; i >= 0; i--) {
|
|
2555
|
+
if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
|
|
2556
|
+
}
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
function locatorCallValues(call, opts = {}) {
|
|
2560
|
+
const value = (arg) => {
|
|
2561
|
+
switch (arg.type) {
|
|
2562
|
+
case "string":
|
|
2563
|
+
case "number":
|
|
2564
|
+
case "boolean":
|
|
2565
|
+
return arg.value;
|
|
2566
|
+
case "regex":
|
|
2567
|
+
return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
|
|
2568
|
+
case "object": {
|
|
2569
|
+
const out = {};
|
|
2570
|
+
for (const [key, v] of arg.entries) {
|
|
2571
|
+
const plain = value(v);
|
|
2572
|
+
if (plain !== void 0) out[key] = plain;
|
|
2573
|
+
}
|
|
2574
|
+
return out;
|
|
2575
|
+
}
|
|
2576
|
+
case "chain":
|
|
2577
|
+
return void 0;
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
return call.args.map(value).filter((v) => v !== void 0);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/internal/capture/pick-on-failure.ts
|
|
2584
|
+
var ANSI_RE = /\[[0-9;]*m/g;
|
|
1869
2585
|
function parseLeafLocatorExpression(rawExpr) {
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
if (!m) return null;
|
|
1873
|
-
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
2586
|
+
const leaf = parseLeafLocatorCall(rawExpr);
|
|
2587
|
+
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
1874
2588
|
}
|
|
1875
2589
|
function deriveFailedLocator(testInfo) {
|
|
1876
2590
|
const info = testInfo;
|
|
@@ -1883,7 +2597,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
|
1883
2597
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
1884
2598
|
if (!parsed) continue;
|
|
1885
2599
|
const loc = err.location;
|
|
1886
|
-
const location = loc ? `${
|
|
2600
|
+
const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
1887
2601
|
return { method: parsed.method, args: parsed.args, location };
|
|
1888
2602
|
}
|
|
1889
2603
|
return null;
|
|
@@ -2032,13 +2746,13 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
2032
2746
|
}
|
|
2033
2747
|
|
|
2034
2748
|
// src/internal/support/aria-sampling.ts
|
|
2035
|
-
var
|
|
2036
|
-
var
|
|
2037
|
-
var
|
|
2749
|
+
var path5 = __toESM(require("path"));
|
|
2750
|
+
var os3 = __toESM(require("os"));
|
|
2751
|
+
var fs3 = __toESM(require("fs"));
|
|
2038
2752
|
|
|
2039
2753
|
// src/internal/support/instance-id.ts
|
|
2040
2754
|
var crypto = __toESM(require("crypto"));
|
|
2041
|
-
var
|
|
2755
|
+
var os2 = __toESM(require("os"));
|
|
2042
2756
|
function hashForProject(projectName) {
|
|
2043
2757
|
return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
2044
2758
|
}
|
|
@@ -2048,14 +2762,14 @@ function ariaSampleIdentity(filePath, title) {
|
|
|
2048
2762
|
return `${filePath}\0${title}`;
|
|
2049
2763
|
}
|
|
2050
2764
|
function getAriaSampleFilePath(projectName) {
|
|
2051
|
-
return
|
|
2765
|
+
return path5.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
2052
2766
|
}
|
|
2053
2767
|
var cachedSets = /* @__PURE__ */ new Map();
|
|
2054
2768
|
function loadAriaSampleSet(projectName) {
|
|
2055
2769
|
if (cachedSets.has(projectName)) return cachedSets.get(projectName);
|
|
2056
2770
|
let set = null;
|
|
2057
2771
|
try {
|
|
2058
|
-
const raw =
|
|
2772
|
+
const raw = fs3.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
2059
2773
|
const parsed = JSON.parse(raw);
|
|
2060
2774
|
if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
|
|
2061
2775
|
set = new Set(parsed.identities.filter((x) => typeof x === "string"));
|
|
@@ -2067,7 +2781,7 @@ function loadAriaSampleSet(projectName) {
|
|
|
2067
2781
|
return set;
|
|
2068
2782
|
}
|
|
2069
2783
|
function relativeTestFile(file) {
|
|
2070
|
-
return
|
|
2784
|
+
return path5.relative(process.cwd(), file).split(path5.sep).join("/");
|
|
2071
2785
|
}
|
|
2072
2786
|
function isDueForAriaSample(testInfo) {
|
|
2073
2787
|
const projectName = process.env.PIWI_PROJECT_NAME;
|
|
@@ -2123,6 +2837,9 @@ function createSink() {
|
|
|
2123
2837
|
stashedPageState: null,
|
|
2124
2838
|
stashedAria: null,
|
|
2125
2839
|
stashedAriaJson: null,
|
|
2840
|
+
pageInventories: [],
|
|
2841
|
+
probeItem: null,
|
|
2842
|
+
probeInterception: null,
|
|
2126
2843
|
pickOffered: false,
|
|
2127
2844
|
userPick: null
|
|
2128
2845
|
};
|
|
@@ -2299,6 +3016,62 @@ async function readPageState(page) {
|
|
|
2299
3016
|
return null;
|
|
2300
3017
|
}
|
|
2301
3018
|
}
|
|
3019
|
+
var inventoriedPageKeys = /* @__PURE__ */ new Set();
|
|
3020
|
+
var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
3021
|
+
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
3022
|
+
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
3023
|
+
function withTimeout(promise, ms) {
|
|
3024
|
+
return new Promise((resolve, reject) => {
|
|
3025
|
+
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
3026
|
+
promise.then(
|
|
3027
|
+
(value) => {
|
|
3028
|
+
clearTimeout(timer);
|
|
3029
|
+
resolve(value);
|
|
3030
|
+
},
|
|
3031
|
+
(error) => {
|
|
3032
|
+
clearTimeout(timer);
|
|
3033
|
+
reject(error);
|
|
3034
|
+
}
|
|
3035
|
+
);
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
async function readPageInventory(page) {
|
|
3039
|
+
try {
|
|
3040
|
+
const inventory = await withTimeout(
|
|
3041
|
+
page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
|
|
3042
|
+
PAGE_INVENTORY_EVAL_TIMEOUT_MS
|
|
3043
|
+
);
|
|
3044
|
+
return inventory ? { ...inventory, capturedAt: Date.now() } : null;
|
|
3045
|
+
} catch {
|
|
3046
|
+
return null;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
async function recordPageWindow(sink, page) {
|
|
3050
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
3051
|
+
let url;
|
|
3052
|
+
try {
|
|
3053
|
+
url = page.url();
|
|
3054
|
+
} catch {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (!url || url.startsWith("about:")) return;
|
|
3058
|
+
const key = inventoryPageKey(url);
|
|
3059
|
+
const capturedAt = Date.now();
|
|
3060
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
3061
|
+
if (key && !inventoriedPageKeys.has(key)) {
|
|
3062
|
+
inventoriedPageKeys.add(key);
|
|
3063
|
+
const content = await readPageInventory(page);
|
|
3064
|
+
if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
|
|
3065
|
+
sink.pageInventories.push({
|
|
3066
|
+
url: content?.url ?? url,
|
|
3067
|
+
controls: content?.controls ?? [],
|
|
3068
|
+
links: content?.links ?? [],
|
|
3069
|
+
capturedAt
|
|
3070
|
+
});
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
|
|
3074
|
+
}
|
|
2302
3075
|
async function stashPageState(sink, closing) {
|
|
2303
3076
|
const page = sink.lastActivePage;
|
|
2304
3077
|
if (!page || isPageClosed(page)) return;
|
|
@@ -2323,6 +3096,9 @@ async function stashPageState(sink, closing) {
|
|
|
2323
3096
|
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
2324
3097
|
await sampleAria();
|
|
2325
3098
|
}
|
|
3099
|
+
if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
3100
|
+
await recordPageWindow(sink, page);
|
|
3101
|
+
}
|
|
2326
3102
|
}
|
|
2327
3103
|
async function maybeOpenPicker(sink, closing) {
|
|
2328
3104
|
if (sink.pickOffered) return;
|
|
@@ -2566,6 +3342,13 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
2566
3342
|
function instrumentPage(page) {
|
|
2567
3343
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
2568
3344
|
INSTRUMENTED_PAGES.add(page);
|
|
3345
|
+
const probeSink = currentSink;
|
|
3346
|
+
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
3347
|
+
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
3348
|
+
probeSink.probeInterception = interception;
|
|
3349
|
+
}).catch(() => {
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
2569
3352
|
const ctx = pageContext(page);
|
|
2570
3353
|
if (ctx) instrumentContext(ctx);
|
|
2571
3354
|
if (typeof page.close === "function") {
|
|
@@ -2601,6 +3384,14 @@ function instrumentPage(page) {
|
|
|
2601
3384
|
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
2602
3385
|
}
|
|
2603
3386
|
}
|
|
3387
|
+
if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
3388
|
+
page.on("framenavigated", (frame) => {
|
|
3389
|
+
const sink = currentSink;
|
|
3390
|
+
if (!sink || frame !== page.mainFrame()) return;
|
|
3391
|
+
void recordPageWindow(sink, page).catch(() => {
|
|
3392
|
+
});
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
2604
3395
|
page.on("console", (msg) => {
|
|
2605
3396
|
const sink = currentSink;
|
|
2606
3397
|
if (!sink) return;
|
|
@@ -2660,14 +3451,14 @@ function instrumentPage(page) {
|
|
|
2660
3451
|
const logHeader = headers["x-piwi-logs"];
|
|
2661
3452
|
if (logHeader) {
|
|
2662
3453
|
try {
|
|
2663
|
-
entry.serverLogs = JSON.parse((0,
|
|
3454
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
2664
3455
|
} catch {
|
|
2665
3456
|
}
|
|
2666
3457
|
}
|
|
2667
3458
|
const traceHeader = headers["x-piwi-trace"];
|
|
2668
3459
|
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
2669
3460
|
try {
|
|
2670
|
-
entry.serverTraces = JSON.parse((0,
|
|
3461
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
2671
3462
|
} catch {
|
|
2672
3463
|
}
|
|
2673
3464
|
}
|
|
@@ -2852,6 +3643,16 @@ async function flushSink(sink, testInfo) {
|
|
|
2852
3643
|
});
|
|
2853
3644
|
}
|
|
2854
3645
|
}
|
|
3646
|
+
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
3647
|
+
if (pageReadable) await recordPageWindow(sink, page);
|
|
3648
|
+
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
3649
|
+
if (windows.length > 0) {
|
|
3650
|
+
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
3651
|
+
contentType: "application/json",
|
|
3652
|
+
body: Buffer.from(JSON.stringify(windows))
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
2855
3656
|
}
|
|
2856
3657
|
var piwiFixtures = {
|
|
2857
3658
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
@@ -2877,11 +3678,36 @@ var piwiFixtures = {
|
|
|
2877
3678
|
async ({}, use, testInfo) => {
|
|
2878
3679
|
const sink = createSink();
|
|
2879
3680
|
sink.testInfo = testInfo;
|
|
3681
|
+
if (isProbeMode())
|
|
3682
|
+
sink.probeItem = probeItemForTest({
|
|
3683
|
+
title: testInfo.title,
|
|
3684
|
+
file: testInfo.file,
|
|
3685
|
+
titlePath: testInfo.titlePath
|
|
3686
|
+
});
|
|
2880
3687
|
currentSink = sink;
|
|
2881
3688
|
try {
|
|
2882
3689
|
await use();
|
|
2883
3690
|
} finally {
|
|
2884
3691
|
currentSink = null;
|
|
3692
|
+
if (sink.probeItem) {
|
|
3693
|
+
const applied = sink.probeInterception?.applied() ?? false;
|
|
3694
|
+
const level = sink.probeItem.level ?? "client";
|
|
3695
|
+
const handled = level === "server" ? classifyProbeHandled({
|
|
3696
|
+
consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
|
|
3697
|
+
dialogs: sink.dialogs.length,
|
|
3698
|
+
backendError: sink.probeInterception?.serverError() ?? false
|
|
3699
|
+
}) : "n/a";
|
|
3700
|
+
recordProbeOutcome({
|
|
3701
|
+
testCaseId: sink.probeItem.testCaseId,
|
|
3702
|
+
routeKey: sink.probeItem.routeKey,
|
|
3703
|
+
fault: sink.probeItem.fault,
|
|
3704
|
+
applied,
|
|
3705
|
+
outcome: outcomeFromStatus(testInfo.status, applied),
|
|
3706
|
+
level,
|
|
3707
|
+
dependency: sink.probeItem.dependency ?? null,
|
|
3708
|
+
handled
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
2885
3711
|
await flushSink(sink, testInfo);
|
|
2886
3712
|
}
|
|
2887
3713
|
},
|