browserclaw 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +353 -334
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -23
- package/dist/index.d.ts +25 -23
- package/dist/index.js +353 -334
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -1218,11 +1218,14 @@ function safeWriteJson(filePath, data) {
|
|
|
1218
1218
|
function setDeep(obj, keys, value) {
|
|
1219
1219
|
let node = obj;
|
|
1220
1220
|
for (const key of keys.slice(0, -1)) {
|
|
1221
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return;
|
|
1221
1222
|
const next = node[key];
|
|
1222
1223
|
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
1223
1224
|
node = node[key];
|
|
1224
1225
|
}
|
|
1225
|
-
|
|
1226
|
+
const lastKey = keys[keys.length - 1];
|
|
1227
|
+
if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
|
|
1228
|
+
node[lastKey] = value;
|
|
1226
1229
|
}
|
|
1227
1230
|
function parseHexRgbToSignedArgbInt(hex) {
|
|
1228
1231
|
const cleaned = hex.trim().replace(/^#/, "");
|
|
@@ -1779,137 +1782,10 @@ var STEALTH_SCRIPT = `(function() {
|
|
|
1779
1782
|
});
|
|
1780
1783
|
})()`;
|
|
1781
1784
|
|
|
1782
|
-
// src/
|
|
1783
|
-
var
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
this.name = "BrowserTabNotFoundError";
|
|
1787
|
-
}
|
|
1788
|
-
};
|
|
1789
|
-
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1790
|
-
const ctrl = new AbortController();
|
|
1791
|
-
const t = setTimeout(() => {
|
|
1792
|
-
ctrl.abort();
|
|
1793
|
-
}, timeoutMs);
|
|
1794
|
-
try {
|
|
1795
|
-
const res = await fetch(url, { signal: ctrl.signal });
|
|
1796
|
-
if (!res.ok) return null;
|
|
1797
|
-
return await res.json();
|
|
1798
|
-
} catch {
|
|
1799
|
-
return null;
|
|
1800
|
-
} finally {
|
|
1801
|
-
clearTimeout(t);
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
function appendCdpPath2(cdpUrl, cdpPath) {
|
|
1805
|
-
try {
|
|
1806
|
-
const url = new URL(cdpUrl);
|
|
1807
|
-
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
1808
|
-
return url.toString();
|
|
1809
|
-
} catch {
|
|
1810
|
-
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1814
|
-
const CDP_SESSION_TIMEOUT_MS = 1e4;
|
|
1815
|
-
const session = await Promise.race([
|
|
1816
|
-
page.context().newCDPSession(page),
|
|
1817
|
-
new Promise((_, reject) => {
|
|
1818
|
-
setTimeout(() => {
|
|
1819
|
-
reject(new Error("newCDPSession timed out after 10s"));
|
|
1820
|
-
}, CDP_SESSION_TIMEOUT_MS);
|
|
1821
|
-
})
|
|
1822
|
-
]);
|
|
1823
|
-
try {
|
|
1824
|
-
return await fn(session);
|
|
1825
|
-
} finally {
|
|
1826
|
-
await session.detach().catch(() => {
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
async function withPageScopedCdpClient(opts) {
|
|
1831
|
-
return await withPlaywrightPageCdpSession(opts.page, async (session) => {
|
|
1832
|
-
return await opts.fn((method, params) => session.send(method, params));
|
|
1833
|
-
});
|
|
1834
|
-
}
|
|
1835
|
-
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1836
|
-
function noProxyAlreadyCoversLocalhost() {
|
|
1837
|
-
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1838
|
-
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1839
|
-
}
|
|
1840
|
-
function isLoopbackCdpUrl(url) {
|
|
1841
|
-
try {
|
|
1842
|
-
return isLoopbackHost(new URL(url).hostname);
|
|
1843
|
-
} catch {
|
|
1844
|
-
return false;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
var NoProxyLeaseManager = class {
|
|
1848
|
-
leaseCount = 0;
|
|
1849
|
-
snapshot = null;
|
|
1850
|
-
acquire(url) {
|
|
1851
|
-
if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
|
|
1852
|
-
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1853
|
-
const noProxy = process.env.NO_PROXY;
|
|
1854
|
-
const noProxyLower = process.env.no_proxy;
|
|
1855
|
-
const current = noProxy ?? noProxyLower ?? "";
|
|
1856
|
-
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1857
|
-
process.env.NO_PROXY = applied;
|
|
1858
|
-
process.env.no_proxy = applied;
|
|
1859
|
-
this.snapshot = { noProxy, noProxyLower, applied };
|
|
1860
|
-
}
|
|
1861
|
-
this.leaseCount += 1;
|
|
1862
|
-
let released = false;
|
|
1863
|
-
return () => {
|
|
1864
|
-
if (released) return;
|
|
1865
|
-
released = true;
|
|
1866
|
-
this.release();
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
release() {
|
|
1870
|
-
if (this.leaseCount <= 0) return;
|
|
1871
|
-
this.leaseCount -= 1;
|
|
1872
|
-
if (this.leaseCount > 0 || !this.snapshot) return;
|
|
1873
|
-
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
1874
|
-
const currentNoProxy = process.env.NO_PROXY;
|
|
1875
|
-
const currentNoProxyLower = process.env.no_proxy;
|
|
1876
|
-
if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
|
|
1877
|
-
if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
|
|
1878
|
-
else delete process.env.NO_PROXY;
|
|
1879
|
-
if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
|
|
1880
|
-
else delete process.env.no_proxy;
|
|
1881
|
-
}
|
|
1882
|
-
this.snapshot = null;
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
var noProxyLeaseManager = new NoProxyLeaseManager();
|
|
1886
|
-
async function withNoProxyForCdpUrl(url, fn) {
|
|
1887
|
-
const release = noProxyLeaseManager.acquire(url);
|
|
1888
|
-
try {
|
|
1889
|
-
return await fn();
|
|
1890
|
-
} finally {
|
|
1891
|
-
release?.();
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
new http__default.default.Agent();
|
|
1895
|
-
new https__default.default.Agent();
|
|
1896
|
-
function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
1897
|
-
const headers = { ...baseHeaders };
|
|
1898
|
-
try {
|
|
1899
|
-
const parsed = new URL(endpoint);
|
|
1900
|
-
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1901
|
-
if (parsed.username || parsed.password) {
|
|
1902
|
-
const credentials = Buffer.from(
|
|
1903
|
-
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1904
|
-
).toString("base64");
|
|
1905
|
-
headers.Authorization = `Basic ${credentials}`;
|
|
1906
|
-
}
|
|
1907
|
-
} catch {
|
|
1908
|
-
}
|
|
1909
|
-
return headers;
|
|
1910
|
-
}
|
|
1911
|
-
var cachedByCdpUrl = /* @__PURE__ */ new Map();
|
|
1912
|
-
var connectingByCdpUrl = /* @__PURE__ */ new Map();
|
|
1785
|
+
// src/page-utils.ts
|
|
1786
|
+
var MAX_CONSOLE_MESSAGES = 500;
|
|
1787
|
+
var MAX_PAGE_ERRORS = 200;
|
|
1788
|
+
var MAX_NETWORK_REQUESTS = 500;
|
|
1913
1789
|
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
1914
1790
|
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
1915
1791
|
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
@@ -1929,56 +1805,6 @@ function bumpDownloadArmId() {
|
|
|
1929
1805
|
nextDownloadArmId += 1;
|
|
1930
1806
|
return nextDownloadArmId;
|
|
1931
1807
|
}
|
|
1932
|
-
var BlockedBrowserTargetError = class extends Error {
|
|
1933
|
-
constructor() {
|
|
1934
|
-
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1935
|
-
this.name = "BlockedBrowserTargetError";
|
|
1936
|
-
}
|
|
1937
|
-
};
|
|
1938
|
-
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1939
|
-
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1940
|
-
function blockedTargetKey(cdpUrl, targetId) {
|
|
1941
|
-
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1942
|
-
}
|
|
1943
|
-
function isBlockedTarget(cdpUrl, targetId) {
|
|
1944
|
-
const normalized = targetId?.trim() ?? "";
|
|
1945
|
-
if (normalized === "") return false;
|
|
1946
|
-
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1947
|
-
}
|
|
1948
|
-
function markTargetBlocked(cdpUrl, targetId) {
|
|
1949
|
-
const normalized = targetId?.trim() ?? "";
|
|
1950
|
-
if (normalized === "") return;
|
|
1951
|
-
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1952
|
-
}
|
|
1953
|
-
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1954
|
-
const normalized = targetId?.trim() ?? "";
|
|
1955
|
-
if (normalized === "") return;
|
|
1956
|
-
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1957
|
-
}
|
|
1958
|
-
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1959
|
-
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1960
|
-
for (const key of blockedTargetsByCdpUrl) {
|
|
1961
|
-
if (key.startsWith(prefix)) return true;
|
|
1962
|
-
}
|
|
1963
|
-
return false;
|
|
1964
|
-
}
|
|
1965
|
-
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1966
|
-
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1967
|
-
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1968
|
-
if (existing) return existing;
|
|
1969
|
-
const created = /* @__PURE__ */ new WeakSet();
|
|
1970
|
-
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1971
|
-
return created;
|
|
1972
|
-
}
|
|
1973
|
-
function isBlockedPageRef(cdpUrl, page) {
|
|
1974
|
-
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1975
|
-
}
|
|
1976
|
-
function markPageRefBlocked(cdpUrl, page) {
|
|
1977
|
-
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1978
|
-
}
|
|
1979
|
-
function clearBlockedPageRef(cdpUrl, page) {
|
|
1980
|
-
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1981
|
-
}
|
|
1982
1808
|
function ensureContextState(context) {
|
|
1983
1809
|
const existing = contextStates.get(context);
|
|
1984
1810
|
if (existing) return existing;
|
|
@@ -1986,16 +1812,8 @@ function ensureContextState(context) {
|
|
|
1986
1812
|
contextStates.set(context, state);
|
|
1987
1813
|
return state;
|
|
1988
1814
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
var MAX_CONSOLE_MESSAGES = 500;
|
|
1992
|
-
var MAX_PAGE_ERRORS = 200;
|
|
1993
|
-
var MAX_NETWORK_REQUESTS = 500;
|
|
1994
|
-
function normalizeCdpUrl(raw) {
|
|
1995
|
-
return raw.replace(/\/$/, "");
|
|
1996
|
-
}
|
|
1997
|
-
function roleRefsKey(cdpUrl, targetId) {
|
|
1998
|
-
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1815
|
+
function getPageState(page) {
|
|
1816
|
+
return pageStates.get(page);
|
|
1999
1817
|
}
|
|
2000
1818
|
function findNetworkRequestById(state, id) {
|
|
2001
1819
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
@@ -2116,77 +1934,353 @@ function ensurePageState(page) {
|
|
|
2116
1934
|
observedPages.delete(page);
|
|
2117
1935
|
});
|
|
2118
1936
|
}
|
|
2119
|
-
return state;
|
|
1937
|
+
return state;
|
|
1938
|
+
}
|
|
1939
|
+
function setDialogHandlerOnPage(page, handler) {
|
|
1940
|
+
const state = ensurePageState(page);
|
|
1941
|
+
state.dialogHandler = handler;
|
|
1942
|
+
}
|
|
1943
|
+
function applyStealthToPage(page) {
|
|
1944
|
+
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1945
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1946
|
+
console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
function observeContext(context) {
|
|
1950
|
+
if (observedContexts.has(context)) return;
|
|
1951
|
+
observedContexts.add(context);
|
|
1952
|
+
ensureContextState(context);
|
|
1953
|
+
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1954
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1955
|
+
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
1956
|
+
});
|
|
1957
|
+
for (const page of context.pages()) {
|
|
1958
|
+
ensurePageState(page);
|
|
1959
|
+
applyStealthToPage(page);
|
|
1960
|
+
}
|
|
1961
|
+
context.on("page", (page) => {
|
|
1962
|
+
ensurePageState(page);
|
|
1963
|
+
applyStealthToPage(page);
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
function observeBrowser(browser) {
|
|
1967
|
+
for (const context of browser.contexts()) observeContext(context);
|
|
1968
|
+
}
|
|
1969
|
+
function toAIFriendlyError(error, selector) {
|
|
1970
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1971
|
+
if (message.includes("strict mode violation")) {
|
|
1972
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
1973
|
+
const count = countMatch ? countMatch[1] : "multiple";
|
|
1974
|
+
return new Error(
|
|
1975
|
+
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
1979
|
+
return new Error(
|
|
1980
|
+
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
1984
|
+
return new Error(
|
|
1985
|
+
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
|
|
1989
|
+
if (timeoutMatch) {
|
|
1990
|
+
return new Error(
|
|
1991
|
+
`Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
|
|
1995
|
+
return new Error(cleaned || message);
|
|
1996
|
+
}
|
|
1997
|
+
function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
1998
|
+
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/ref-resolver.ts
|
|
2002
|
+
var roleRefsByTarget = /* @__PURE__ */ new Map();
|
|
2003
|
+
var MAX_ROLE_REFS_CACHE = 50;
|
|
2004
|
+
function normalizeCdpUrl(raw) {
|
|
2005
|
+
return raw.replace(/\/$/, "");
|
|
2006
|
+
}
|
|
2007
|
+
function roleRefsKey(cdpUrl, targetId) {
|
|
2008
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
2009
|
+
}
|
|
2010
|
+
function rememberRoleRefsForTarget(opts) {
|
|
2011
|
+
const targetId = opts.targetId.trim();
|
|
2012
|
+
if (targetId === "") return;
|
|
2013
|
+
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
2014
|
+
refs: opts.refs,
|
|
2015
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
2016
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
2017
|
+
});
|
|
2018
|
+
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
2019
|
+
const first = roleRefsByTarget.keys().next();
|
|
2020
|
+
if (first.done === true) break;
|
|
2021
|
+
roleRefsByTarget.delete(first.value);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
function storeRoleRefsForTarget(opts) {
|
|
2025
|
+
const state = ensurePageState(opts.page);
|
|
2026
|
+
state.roleRefs = opts.refs;
|
|
2027
|
+
state.roleRefsFrameSelector = opts.frameSelector;
|
|
2028
|
+
state.roleRefsMode = opts.mode;
|
|
2029
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
2030
|
+
rememberRoleRefsForTarget({
|
|
2031
|
+
cdpUrl: opts.cdpUrl,
|
|
2032
|
+
targetId: opts.targetId,
|
|
2033
|
+
refs: opts.refs,
|
|
2034
|
+
frameSelector: opts.frameSelector,
|
|
2035
|
+
mode: opts.mode
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
function restoreRoleRefsForTarget(opts) {
|
|
2039
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
2040
|
+
if (targetId === "") return;
|
|
2041
|
+
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
2042
|
+
if (!entry) return;
|
|
2043
|
+
const state = ensurePageState(opts.page);
|
|
2044
|
+
if (state.roleRefs) return;
|
|
2045
|
+
state.roleRefs = entry.refs;
|
|
2046
|
+
state.roleRefsFrameSelector = entry.frameSelector;
|
|
2047
|
+
state.roleRefsMode = entry.mode;
|
|
2048
|
+
}
|
|
2049
|
+
function clearRoleRefsForCdpUrl(cdpUrl) {
|
|
2050
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
2051
|
+
for (const key of roleRefsByTarget.keys()) {
|
|
2052
|
+
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function parseRoleRef(raw) {
|
|
2056
|
+
const trimmed = raw.trim();
|
|
2057
|
+
if (!trimmed) return null;
|
|
2058
|
+
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
2059
|
+
return /^e\d+$/.test(normalized) ? normalized : null;
|
|
2060
|
+
}
|
|
2061
|
+
function requireRef(value) {
|
|
2062
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
2063
|
+
const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
|
|
2064
|
+
if (!ref) throw new Error("ref is required");
|
|
2065
|
+
return ref;
|
|
2066
|
+
}
|
|
2067
|
+
function requireRefOrSelector(ref, selector) {
|
|
2068
|
+
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2069
|
+
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2070
|
+
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2071
|
+
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2072
|
+
}
|
|
2073
|
+
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2074
|
+
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2075
|
+
}
|
|
2076
|
+
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2077
|
+
const normalized = Math.floor(value ?? 0);
|
|
2078
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2079
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2080
|
+
return normalized;
|
|
2081
|
+
}
|
|
2082
|
+
function refLocator(page, ref) {
|
|
2083
|
+
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2084
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2085
|
+
if (/^e\d+$/.test(normalized)) {
|
|
2086
|
+
const state = getPageState(page);
|
|
2087
|
+
if (state?.roleRefsMode === "aria") {
|
|
2088
|
+
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2089
|
+
}
|
|
2090
|
+
const info = state?.roleRefs?.[normalized];
|
|
2091
|
+
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2092
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2093
|
+
const role = info.role;
|
|
2094
|
+
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2095
|
+
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2096
|
+
}
|
|
2097
|
+
return page.locator(`aria-ref=${normalized}`);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/connection.ts
|
|
2101
|
+
var BrowserTabNotFoundError = class extends Error {
|
|
2102
|
+
constructor(message = "Tab not found") {
|
|
2103
|
+
super(message);
|
|
2104
|
+
this.name = "BrowserTabNotFoundError";
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
async function fetchJsonForCdp(url, timeoutMs) {
|
|
2108
|
+
const ctrl = new AbortController();
|
|
2109
|
+
const t = setTimeout(() => {
|
|
2110
|
+
ctrl.abort();
|
|
2111
|
+
}, timeoutMs);
|
|
2112
|
+
try {
|
|
2113
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
2114
|
+
if (!res.ok) return null;
|
|
2115
|
+
return await res.json();
|
|
2116
|
+
} catch {
|
|
2117
|
+
return null;
|
|
2118
|
+
} finally {
|
|
2119
|
+
clearTimeout(t);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
function appendCdpPath2(cdpUrl, cdpPath) {
|
|
2123
|
+
try {
|
|
2124
|
+
const url = new URL(cdpUrl);
|
|
2125
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
2126
|
+
return url.toString();
|
|
2127
|
+
} catch {
|
|
2128
|
+
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
async function withPlaywrightPageCdpSession(page, fn) {
|
|
2132
|
+
const CDP_SESSION_TIMEOUT_MS = 1e4;
|
|
2133
|
+
const session = await Promise.race([
|
|
2134
|
+
page.context().newCDPSession(page),
|
|
2135
|
+
new Promise((_, reject) => {
|
|
2136
|
+
setTimeout(() => {
|
|
2137
|
+
reject(new Error("newCDPSession timed out after 10s"));
|
|
2138
|
+
}, CDP_SESSION_TIMEOUT_MS);
|
|
2139
|
+
})
|
|
2140
|
+
]);
|
|
2141
|
+
try {
|
|
2142
|
+
return await fn(session);
|
|
2143
|
+
} finally {
|
|
2144
|
+
await session.detach().catch(() => {
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
async function withPageScopedCdpClient(opts) {
|
|
2149
|
+
return await withPlaywrightPageCdpSession(opts.page, async (session) => {
|
|
2150
|
+
return await opts.fn((method, params) => session.send(method, params));
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
2154
|
+
function noProxyAlreadyCoversLocalhost() {
|
|
2155
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
2156
|
+
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
2157
|
+
}
|
|
2158
|
+
function isLoopbackCdpUrl(url) {
|
|
2159
|
+
try {
|
|
2160
|
+
return isLoopbackHost(new URL(url).hostname);
|
|
2161
|
+
} catch {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2120
2164
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2165
|
+
var NoProxyLeaseManager = class {
|
|
2166
|
+
leaseCount = 0;
|
|
2167
|
+
snapshot = null;
|
|
2168
|
+
acquire(url) {
|
|
2169
|
+
if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
|
|
2170
|
+
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
2171
|
+
const noProxy = process.env.NO_PROXY;
|
|
2172
|
+
const noProxyLower = process.env.no_proxy;
|
|
2173
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
2174
|
+
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
2175
|
+
process.env.NO_PROXY = applied;
|
|
2176
|
+
process.env.no_proxy = applied;
|
|
2177
|
+
this.snapshot = { noProxy, noProxyLower, applied };
|
|
2178
|
+
}
|
|
2179
|
+
this.leaseCount += 1;
|
|
2180
|
+
let released = false;
|
|
2181
|
+
return () => {
|
|
2182
|
+
if (released) return;
|
|
2183
|
+
released = true;
|
|
2184
|
+
this.release();
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
release() {
|
|
2188
|
+
if (this.leaseCount <= 0) return;
|
|
2189
|
+
this.leaseCount -= 1;
|
|
2190
|
+
if (this.leaseCount > 0 || !this.snapshot) return;
|
|
2191
|
+
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
2192
|
+
const currentNoProxy = process.env.NO_PROXY;
|
|
2193
|
+
const currentNoProxyLower = process.env.no_proxy;
|
|
2194
|
+
if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
|
|
2195
|
+
if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
|
|
2196
|
+
else delete process.env.NO_PROXY;
|
|
2197
|
+
if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
|
|
2198
|
+
else delete process.env.no_proxy;
|
|
2199
|
+
}
|
|
2200
|
+
this.snapshot = null;
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
var noProxyLeaseManager = new NoProxyLeaseManager();
|
|
2204
|
+
async function withNoProxyForCdpUrl(url, fn) {
|
|
2205
|
+
const release = noProxyLeaseManager.acquire(url);
|
|
2206
|
+
try {
|
|
2207
|
+
return await fn();
|
|
2208
|
+
} finally {
|
|
2209
|
+
release?.();
|
|
2210
|
+
}
|
|
2125
2211
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2212
|
+
new http__default.default.Agent();
|
|
2213
|
+
new https__default.default.Agent();
|
|
2214
|
+
function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
2215
|
+
const headers = { ...baseHeaders };
|
|
2216
|
+
try {
|
|
2217
|
+
const parsed = new URL(endpoint);
|
|
2218
|
+
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
2219
|
+
if (parsed.username || parsed.password) {
|
|
2220
|
+
const credentials = Buffer.from(
|
|
2221
|
+
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
2222
|
+
).toString("base64");
|
|
2223
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
2224
|
+
}
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
return headers;
|
|
2131
2228
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
2139
|
-
});
|
|
2140
|
-
for (const page of context.pages()) {
|
|
2141
|
-
ensurePageState(page);
|
|
2142
|
-
applyStealthToPage(page);
|
|
2229
|
+
var cachedByCdpUrl = /* @__PURE__ */ new Map();
|
|
2230
|
+
var connectingByCdpUrl = /* @__PURE__ */ new Map();
|
|
2231
|
+
var BlockedBrowserTargetError = class extends Error {
|
|
2232
|
+
constructor() {
|
|
2233
|
+
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
2234
|
+
this.name = "BlockedBrowserTargetError";
|
|
2143
2235
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2236
|
+
};
|
|
2237
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
2238
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
2239
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
2240
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
2148
2241
|
}
|
|
2149
|
-
function
|
|
2150
|
-
|
|
2242
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
2243
|
+
const normalized = targetId?.trim() ?? "";
|
|
2244
|
+
if (normalized === "") return false;
|
|
2245
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
2151
2246
|
}
|
|
2152
|
-
function
|
|
2153
|
-
const
|
|
2154
|
-
if (
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2247
|
+
function markTargetBlocked(cdpUrl, targetId) {
|
|
2248
|
+
const normalized = targetId?.trim() ?? "";
|
|
2249
|
+
if (normalized === "") return;
|
|
2250
|
+
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
2251
|
+
}
|
|
2252
|
+
function clearBlockedTarget(cdpUrl, targetId) {
|
|
2253
|
+
const normalized = targetId?.trim() ?? "";
|
|
2254
|
+
if (normalized === "") return;
|
|
2255
|
+
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
2256
|
+
}
|
|
2257
|
+
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
2258
|
+
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
2259
|
+
for (const key of blockedTargetsByCdpUrl) {
|
|
2260
|
+
if (key.startsWith(prefix)) return true;
|
|
2164
2261
|
}
|
|
2262
|
+
return false;
|
|
2165
2263
|
}
|
|
2166
|
-
function
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
cdpUrl: opts.cdpUrl,
|
|
2174
|
-
targetId: opts.targetId,
|
|
2175
|
-
refs: opts.refs,
|
|
2176
|
-
frameSelector: opts.frameSelector,
|
|
2177
|
-
mode: opts.mode
|
|
2178
|
-
});
|
|
2264
|
+
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
2265
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
2266
|
+
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
2267
|
+
if (existing) return existing;
|
|
2268
|
+
const created = /* @__PURE__ */ new WeakSet();
|
|
2269
|
+
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
2270
|
+
return created;
|
|
2179
2271
|
}
|
|
2180
|
-
function
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2272
|
+
function isBlockedPageRef(cdpUrl, page) {
|
|
2273
|
+
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
2274
|
+
}
|
|
2275
|
+
function markPageRefBlocked(cdpUrl, page) {
|
|
2276
|
+
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
2277
|
+
}
|
|
2278
|
+
function clearBlockedPageRef(cdpUrl, page) {
|
|
2279
|
+
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
2280
|
+
}
|
|
2281
|
+
async function setDialogHandler(opts) {
|
|
2282
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2283
|
+
setDialogHandlerOnPage(page, opts.handler);
|
|
2190
2284
|
}
|
|
2191
2285
|
async function connectBrowser(cdpUrl, authToken) {
|
|
2192
2286
|
const normalized = normalizeCdpUrl(cdpUrl);
|
|
@@ -2210,9 +2304,7 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
2210
2304
|
const onDisconnected = () => {
|
|
2211
2305
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
2212
2306
|
cachedByCdpUrl.delete(normalized);
|
|
2213
|
-
|
|
2214
|
-
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
2215
|
-
}
|
|
2307
|
+
clearRoleRefsForCdpUrl(normalized);
|
|
2216
2308
|
}
|
|
2217
2309
|
};
|
|
2218
2310
|
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
@@ -2476,87 +2568,12 @@ async function resolvePageByTargetIdOrThrow(opts) {
|
|
|
2476
2568
|
if (!page) throw new BrowserTabNotFoundError();
|
|
2477
2569
|
return page;
|
|
2478
2570
|
}
|
|
2479
|
-
function parseRoleRef(raw) {
|
|
2480
|
-
const trimmed = raw.trim();
|
|
2481
|
-
if (!trimmed) return null;
|
|
2482
|
-
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
2483
|
-
return /^e\d+$/.test(normalized) ? normalized : null;
|
|
2484
|
-
}
|
|
2485
|
-
function requireRef(value) {
|
|
2486
|
-
const raw = typeof value === "string" ? value.trim() : "";
|
|
2487
|
-
const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
|
|
2488
|
-
if (!ref) throw new Error("ref is required");
|
|
2489
|
-
return ref;
|
|
2490
|
-
}
|
|
2491
|
-
function requireRefOrSelector(ref, selector) {
|
|
2492
|
-
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2493
|
-
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2494
|
-
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2495
|
-
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2496
|
-
}
|
|
2497
|
-
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2498
|
-
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2499
|
-
}
|
|
2500
|
-
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2501
|
-
const normalized = Math.floor(value ?? 0);
|
|
2502
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2503
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2504
|
-
return normalized;
|
|
2505
|
-
}
|
|
2506
2571
|
async function getRestoredPageForTarget(opts) {
|
|
2507
2572
|
const page = await getPageForTargetId(opts);
|
|
2508
2573
|
ensurePageState(page);
|
|
2509
2574
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2510
2575
|
return page;
|
|
2511
2576
|
}
|
|
2512
|
-
function refLocator(page, ref) {
|
|
2513
|
-
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2514
|
-
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2515
|
-
if (/^e\d+$/.test(normalized)) {
|
|
2516
|
-
const state = pageStates.get(page);
|
|
2517
|
-
if (state?.roleRefsMode === "aria") {
|
|
2518
|
-
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2519
|
-
}
|
|
2520
|
-
const info = state?.roleRefs?.[normalized];
|
|
2521
|
-
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2522
|
-
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2523
|
-
const role = info.role;
|
|
2524
|
-
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2525
|
-
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2526
|
-
}
|
|
2527
|
-
return page.locator(`aria-ref=${normalized}`);
|
|
2528
|
-
}
|
|
2529
|
-
function toAIFriendlyError(error, selector) {
|
|
2530
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2531
|
-
if (message.includes("strict mode violation")) {
|
|
2532
|
-
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2533
|
-
const count = countMatch ? countMatch[1] : "multiple";
|
|
2534
|
-
return new Error(
|
|
2535
|
-
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2536
|
-
);
|
|
2537
|
-
}
|
|
2538
|
-
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2539
|
-
return new Error(
|
|
2540
|
-
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2541
|
-
);
|
|
2542
|
-
}
|
|
2543
|
-
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2544
|
-
return new Error(
|
|
2545
|
-
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2546
|
-
);
|
|
2547
|
-
}
|
|
2548
|
-
const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
|
|
2549
|
-
if (timeoutMatch) {
|
|
2550
|
-
return new Error(
|
|
2551
|
-
`Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
|
|
2552
|
-
);
|
|
2553
|
-
}
|
|
2554
|
-
const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
|
|
2555
|
-
return new Error(cleaned || message);
|
|
2556
|
-
}
|
|
2557
|
-
function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
2558
|
-
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2559
|
-
}
|
|
2560
2577
|
|
|
2561
2578
|
// src/actions/evaluate.ts
|
|
2562
2579
|
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
@@ -2581,7 +2598,8 @@ async function evaluateInAllFramesViaPlaywright(opts) {
|
|
|
2581
2598
|
frameName: frame.name(),
|
|
2582
2599
|
result
|
|
2583
2600
|
});
|
|
2584
|
-
} catch {
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
console.warn("[browserclaw] frame evaluate failed:", err instanceof Error ? err.message : String(err));
|
|
2585
2603
|
}
|
|
2586
2604
|
}
|
|
2587
2605
|
return results;
|
|
@@ -3296,7 +3314,7 @@ async function pressAndHoldViaCdp(opts) {
|
|
|
3296
3314
|
async function clickByTextViaPlaywright(opts) {
|
|
3297
3315
|
const page = await getRestoredPageForTarget(opts);
|
|
3298
3316
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3299
|
-
const locator = page.getByText(opts.text, { exact: opts.exact }).
|
|
3317
|
+
const locator = page.getByText(opts.text, { exact: opts.exact }).or(page.getByTitle(opts.text, { exact: opts.exact })).and(page.locator(":visible")).first();
|
|
3300
3318
|
try {
|
|
3301
3319
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3302
3320
|
} catch (err) {
|
|
@@ -5064,6 +5082,7 @@ function axValue(v) {
|
|
|
5064
5082
|
return "";
|
|
5065
5083
|
}
|
|
5066
5084
|
function formatAriaNodes(nodes, limit) {
|
|
5085
|
+
if (nodes.length === 0) return [];
|
|
5067
5086
|
const byId = /* @__PURE__ */ new Map();
|
|
5068
5087
|
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
5069
5088
|
const referenced = /* @__PURE__ */ new Set();
|