browser-pilot 0.0.12 → 0.0.13
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/README.md +13 -7
- package/dist/actions.cjs +63 -10
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +629 -57
- package/dist/browser.d.cts +9 -3
- package/dist/browser.d.ts +9 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +1 -1
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +1 -1
- package/dist/{chunk-NLIARNEE.mjs → chunk-A2ZRAEO3.mjs} +63 -10
- package/dist/{chunk-4MBSALQL.mjs → chunk-HP6R3W32.mjs} +1 -1
- package/dist/{chunk-RUWAXHDX.mjs → chunk-VDAMDOS6.mjs} +606 -57
- package/dist/cli.mjs +1145 -127
- package/dist/{client-7Nqka5MV.d.ts → client-DRqxBdHv.d.cts} +1 -1
- package/dist/{client-7Nqka5MV.d.cts → client-DRqxBdHv.d.ts} +1 -1
- package/dist/index.cjs +668 -66
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{types-j23Iqo2L.d.ts → types-BXMGFtnB.d.cts} +46 -5
- package/dist/{types-BOPu0OQZ.d.cts → types-CzgQjai9.d.ts} +46 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCDPClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HP6R3W32.mjs";
|
|
4
4
|
import {
|
|
5
5
|
createProvider
|
|
6
6
|
} from "./chunk-BRAFQUMG.mjs";
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TimeoutError,
|
|
12
12
|
ensureActionable,
|
|
13
13
|
generateHints
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-A2ZRAEO3.mjs";
|
|
15
15
|
|
|
16
16
|
// src/audio/encoding.ts
|
|
17
17
|
function bufferToBase64(data) {
|
|
@@ -1507,6 +1507,285 @@ var RequestInterceptor = class {
|
|
|
1507
1507
|
}
|
|
1508
1508
|
};
|
|
1509
1509
|
|
|
1510
|
+
// src/browser/special-selectors.ts
|
|
1511
|
+
function stripQuotes(value) {
|
|
1512
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1513
|
+
return value.slice(1, -1);
|
|
1514
|
+
}
|
|
1515
|
+
return value;
|
|
1516
|
+
}
|
|
1517
|
+
function parseTextSelector(selector) {
|
|
1518
|
+
if (!selector.startsWith("text:")) return null;
|
|
1519
|
+
let raw = selector.slice(5).trim();
|
|
1520
|
+
let exact = false;
|
|
1521
|
+
if (raw.startsWith("=")) {
|
|
1522
|
+
exact = true;
|
|
1523
|
+
raw = raw.slice(1).trim();
|
|
1524
|
+
}
|
|
1525
|
+
const query = stripQuotes(raw);
|
|
1526
|
+
if (!query) return null;
|
|
1527
|
+
return { query, exact };
|
|
1528
|
+
}
|
|
1529
|
+
function parseRoleSelector(selector) {
|
|
1530
|
+
if (!selector.startsWith("role:")) return null;
|
|
1531
|
+
const body = selector.slice(5);
|
|
1532
|
+
const separator = body.indexOf(":");
|
|
1533
|
+
const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
|
|
1534
|
+
const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
|
|
1535
|
+
if (!role) return null;
|
|
1536
|
+
return { role, name: name || void 0 };
|
|
1537
|
+
}
|
|
1538
|
+
var SPECIAL_SELECTOR_SCRIPT = `
|
|
1539
|
+
function bpNormalizeSpace(value) {
|
|
1540
|
+
return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function bpCollectElements(root) {
|
|
1544
|
+
var elements = [];
|
|
1545
|
+
|
|
1546
|
+
function visit(node) {
|
|
1547
|
+
if (!node || typeof node.querySelectorAll !== 'function') return;
|
|
1548
|
+
var matches = node.querySelectorAll('*');
|
|
1549
|
+
for (var i = 0; i < matches.length; i++) {
|
|
1550
|
+
var el = matches[i];
|
|
1551
|
+
elements.push(el);
|
|
1552
|
+
if (el.shadowRoot) {
|
|
1553
|
+
visit(el.shadowRoot);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
if (root && root.documentElement) {
|
|
1559
|
+
elements.push(root.documentElement);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
visit(root);
|
|
1563
|
+
return elements;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function bpIsVisible(el) {
|
|
1567
|
+
if (!el) return false;
|
|
1568
|
+
var style = getComputedStyle(el);
|
|
1569
|
+
if (style.display === 'none') return false;
|
|
1570
|
+
if (style.visibility === 'hidden') return false;
|
|
1571
|
+
if (parseFloat(style.opacity || '1') === 0) return false;
|
|
1572
|
+
var rect = el.getBoundingClientRect();
|
|
1573
|
+
return rect.width > 0 && rect.height > 0;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function bpInferRole(el) {
|
|
1577
|
+
if (!el || !el.tagName) return '';
|
|
1578
|
+
|
|
1579
|
+
var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
|
|
1580
|
+
if (explicitRole) return explicitRole.toLowerCase();
|
|
1581
|
+
|
|
1582
|
+
var tag = el.tagName.toLowerCase();
|
|
1583
|
+
if (tag === 'button') return 'button';
|
|
1584
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
1585
|
+
if (tag === 'textarea') return 'textbox';
|
|
1586
|
+
if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
|
|
1587
|
+
if (tag === 'option') return 'option';
|
|
1588
|
+
if (tag === 'summary') return 'button';
|
|
1589
|
+
|
|
1590
|
+
if (tag === 'input') {
|
|
1591
|
+
var type = (el.type || 'text').toLowerCase();
|
|
1592
|
+
if (type === 'checkbox') return 'checkbox';
|
|
1593
|
+
if (type === 'radio') return 'radio';
|
|
1594
|
+
if (type === 'search') return 'searchbox';
|
|
1595
|
+
if (type === 'number') return 'spinbutton';
|
|
1596
|
+
if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
|
|
1597
|
+
return 'button';
|
|
1598
|
+
}
|
|
1599
|
+
return 'textbox';
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
return '';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function bpTextFromIdRefs(refs) {
|
|
1606
|
+
if (!refs) return '';
|
|
1607
|
+
var ids = refs.split(/\\s+/).filter(Boolean);
|
|
1608
|
+
var parts = [];
|
|
1609
|
+
for (var i = 0; i < ids.length; i++) {
|
|
1610
|
+
var node = document.getElementById(ids[i]);
|
|
1611
|
+
if (!node) continue;
|
|
1612
|
+
var text = bpNormalizeSpace(node.innerText || node.textContent || '');
|
|
1613
|
+
if (text) parts.push(text);
|
|
1614
|
+
}
|
|
1615
|
+
return bpNormalizeSpace(parts.join(' '));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function bpAccessibleName(el) {
|
|
1619
|
+
if (!el) return '';
|
|
1620
|
+
|
|
1621
|
+
var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
|
|
1622
|
+
if (labelledBy) return labelledBy;
|
|
1623
|
+
|
|
1624
|
+
var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
|
|
1625
|
+
if (ariaLabel) return ariaLabel;
|
|
1626
|
+
|
|
1627
|
+
if (el.labels && el.labels.length) {
|
|
1628
|
+
var labels = [];
|
|
1629
|
+
for (var i = 0; i < el.labels.length; i++) {
|
|
1630
|
+
var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
|
|
1631
|
+
if (labelText) labels.push(labelText);
|
|
1632
|
+
}
|
|
1633
|
+
if (labels.length) return bpNormalizeSpace(labels.join(' '));
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (el.id) {
|
|
1637
|
+
var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
|
|
1638
|
+
if (fallbackLabel) {
|
|
1639
|
+
var fallbackText = bpNormalizeSpace(
|
|
1640
|
+
fallbackLabel.innerText || fallbackLabel.textContent || ''
|
|
1641
|
+
);
|
|
1642
|
+
if (fallbackText) return fallbackText;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
var type = (el.type || '').toLowerCase();
|
|
1647
|
+
if (
|
|
1648
|
+
el.tagName === 'INPUT' &&
|
|
1649
|
+
(type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
|
|
1650
|
+
) {
|
|
1651
|
+
var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
|
|
1652
|
+
if (inputValue) return inputValue;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
|
|
1656
|
+
if (alt) return alt;
|
|
1657
|
+
|
|
1658
|
+
var text = bpNormalizeSpace(el.innerText || el.textContent || '');
|
|
1659
|
+
if (text) return text;
|
|
1660
|
+
|
|
1661
|
+
var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
|
|
1662
|
+
if (placeholder) return placeholder;
|
|
1663
|
+
|
|
1664
|
+
var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
|
|
1665
|
+
if (title) return title;
|
|
1666
|
+
|
|
1667
|
+
var value = bpNormalizeSpace(el.value);
|
|
1668
|
+
if (value) return value;
|
|
1669
|
+
|
|
1670
|
+
return bpNormalizeSpace(el.name || el.id || '');
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function bpIsInteractive(role, el) {
|
|
1674
|
+
if (
|
|
1675
|
+
role === 'button' ||
|
|
1676
|
+
role === 'link' ||
|
|
1677
|
+
role === 'textbox' ||
|
|
1678
|
+
role === 'checkbox' ||
|
|
1679
|
+
role === 'radio' ||
|
|
1680
|
+
role === 'combobox' ||
|
|
1681
|
+
role === 'listbox' ||
|
|
1682
|
+
role === 'option' ||
|
|
1683
|
+
role === 'searchbox' ||
|
|
1684
|
+
role === 'spinbutton' ||
|
|
1685
|
+
role === 'switch' ||
|
|
1686
|
+
role === 'tab'
|
|
1687
|
+
) {
|
|
1688
|
+
return true;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (!el || !el.tagName) return false;
|
|
1692
|
+
var tag = el.tagName.toLowerCase();
|
|
1693
|
+
return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function bpFindByText(query, exact, includeHidden) {
|
|
1697
|
+
var needle = bpNormalizeSpace(query).toLowerCase();
|
|
1698
|
+
if (!needle) return null;
|
|
1699
|
+
|
|
1700
|
+
var best = null;
|
|
1701
|
+
var bestScore = -1;
|
|
1702
|
+
var elements = bpCollectElements(document);
|
|
1703
|
+
|
|
1704
|
+
for (var i = 0; i < elements.length; i++) {
|
|
1705
|
+
var el = elements[i];
|
|
1706
|
+
if (!includeHidden && !bpIsVisible(el)) continue;
|
|
1707
|
+
|
|
1708
|
+
var text = bpAccessibleName(el);
|
|
1709
|
+
if (!text) continue;
|
|
1710
|
+
|
|
1711
|
+
var haystack = text.toLowerCase();
|
|
1712
|
+
var matched = exact ? haystack === needle : haystack.includes(needle);
|
|
1713
|
+
if (!matched) continue;
|
|
1714
|
+
|
|
1715
|
+
var role = bpInferRole(el);
|
|
1716
|
+
var score = 0;
|
|
1717
|
+
if (bpIsInteractive(role, el)) score += 100;
|
|
1718
|
+
if (haystack === needle) score += 50;
|
|
1719
|
+
if (role === 'button' || role === 'link') score += 10;
|
|
1720
|
+
|
|
1721
|
+
if (score > bestScore) {
|
|
1722
|
+
best = el;
|
|
1723
|
+
bestScore = score;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
return best;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function bpFindByRole(role, name, includeHidden) {
|
|
1731
|
+
var targetRole = bpNormalizeSpace(role).toLowerCase();
|
|
1732
|
+
if (!targetRole) return null;
|
|
1733
|
+
|
|
1734
|
+
var nameNeedle = bpNormalizeSpace(name).toLowerCase();
|
|
1735
|
+
var best = null;
|
|
1736
|
+
var bestScore = -1;
|
|
1737
|
+
var elements = bpCollectElements(document);
|
|
1738
|
+
|
|
1739
|
+
for (var i = 0; i < elements.length; i++) {
|
|
1740
|
+
var el = elements[i];
|
|
1741
|
+
if (!includeHidden && !bpIsVisible(el)) continue;
|
|
1742
|
+
|
|
1743
|
+
var actualRole = bpInferRole(el);
|
|
1744
|
+
if (actualRole !== targetRole) continue;
|
|
1745
|
+
|
|
1746
|
+
var accessibleName = bpAccessibleName(el);
|
|
1747
|
+
if (nameNeedle) {
|
|
1748
|
+
var loweredName = accessibleName.toLowerCase();
|
|
1749
|
+
if (!loweredName.includes(nameNeedle)) continue;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
var score = 0;
|
|
1753
|
+
if (accessibleName) score += 10;
|
|
1754
|
+
if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
|
|
1755
|
+
|
|
1756
|
+
if (score > bestScore) {
|
|
1757
|
+
best = el;
|
|
1758
|
+
bestScore = score;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return best;
|
|
1763
|
+
}
|
|
1764
|
+
`;
|
|
1765
|
+
function buildSpecialSelectorLookupExpression(selector, options = {}) {
|
|
1766
|
+
const includeHidden = options.includeHidden === true;
|
|
1767
|
+
const text = parseTextSelector(selector);
|
|
1768
|
+
if (text) {
|
|
1769
|
+
return `(() => {
|
|
1770
|
+
${SPECIAL_SELECTOR_SCRIPT}
|
|
1771
|
+
return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
|
|
1772
|
+
})()`;
|
|
1773
|
+
}
|
|
1774
|
+
const role = parseRoleSelector(selector);
|
|
1775
|
+
if (role) {
|
|
1776
|
+
return `(() => {
|
|
1777
|
+
${SPECIAL_SELECTOR_SCRIPT}
|
|
1778
|
+
return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
|
|
1779
|
+
})()`;
|
|
1780
|
+
}
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
function buildSpecialSelectorPredicateExpression(selector, options = {}) {
|
|
1784
|
+
const lookup = buildSpecialSelectorLookupExpression(selector, options);
|
|
1785
|
+
if (!lookup) return null;
|
|
1786
|
+
return `(() => !!(${lookup}))()`;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1510
1789
|
// src/wait/strategies.ts
|
|
1511
1790
|
var DEEP_QUERY_SCRIPT = `
|
|
1512
1791
|
function deepQuery(selector, root = document) {
|
|
@@ -1540,18 +1819,19 @@ function deepQuery(selector, root = document) {
|
|
|
1540
1819
|
}
|
|
1541
1820
|
`;
|
|
1542
1821
|
async function isElementVisible(cdp, selector, contextId) {
|
|
1822
|
+
const specialExpression = buildSpecialSelectorPredicateExpression(selector);
|
|
1543
1823
|
const params = {
|
|
1544
|
-
expression: `(() => {
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1824
|
+
expression: specialExpression ?? `(() => {
|
|
1825
|
+
${DEEP_QUERY_SCRIPT}
|
|
1826
|
+
const el = deepQuery(${JSON.stringify(selector)});
|
|
1827
|
+
if (!el) return false;
|
|
1828
|
+
const style = getComputedStyle(el);
|
|
1829
|
+
if (style.display === 'none') return false;
|
|
1830
|
+
if (style.visibility === 'hidden') return false;
|
|
1831
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
1832
|
+
const rect = el.getBoundingClientRect();
|
|
1833
|
+
return rect.width > 0 && rect.height > 0;
|
|
1834
|
+
})()`,
|
|
1555
1835
|
returnByValue: true
|
|
1556
1836
|
};
|
|
1557
1837
|
if (contextId !== void 0) {
|
|
@@ -1561,11 +1841,14 @@ async function isElementVisible(cdp, selector, contextId) {
|
|
|
1561
1841
|
return result.result.value === true;
|
|
1562
1842
|
}
|
|
1563
1843
|
async function isElementAttached(cdp, selector, contextId) {
|
|
1844
|
+
const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
|
|
1845
|
+
includeHidden: true
|
|
1846
|
+
});
|
|
1564
1847
|
const params = {
|
|
1565
|
-
expression: `(() => {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1848
|
+
expression: specialExpression ?? `(() => {
|
|
1849
|
+
${DEEP_QUERY_SCRIPT}
|
|
1850
|
+
return deepQuery(${JSON.stringify(selector)}) !== null;
|
|
1851
|
+
})()`,
|
|
1569
1852
|
returnByValue: true
|
|
1570
1853
|
};
|
|
1571
1854
|
if (contextId !== void 0) {
|
|
@@ -2226,6 +2509,9 @@ var Page = class {
|
|
|
2226
2509
|
timeout: options.timeout ?? DEFAULT_TIMEOUT
|
|
2227
2510
|
});
|
|
2228
2511
|
} catch (e) {
|
|
2512
|
+
if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2229
2515
|
if (options.optional) return false;
|
|
2230
2516
|
throw e;
|
|
2231
2517
|
}
|
|
@@ -2614,7 +2900,12 @@ var Page = class {
|
|
|
2614
2900
|
returnByValue: true
|
|
2615
2901
|
});
|
|
2616
2902
|
if (!after.result.value) {
|
|
2617
|
-
|
|
2903
|
+
if (await this.tryToggleViaLabel(object.objectId, true)) {
|
|
2904
|
+
return true;
|
|
2905
|
+
}
|
|
2906
|
+
throw new Error(
|
|
2907
|
+
"Clicking the checkbox did not change its state. Tried the associated label too."
|
|
2908
|
+
);
|
|
2618
2909
|
}
|
|
2619
2910
|
return true;
|
|
2620
2911
|
});
|
|
@@ -2666,7 +2957,12 @@ var Page = class {
|
|
|
2666
2957
|
returnByValue: true
|
|
2667
2958
|
});
|
|
2668
2959
|
if (after.result.value) {
|
|
2669
|
-
|
|
2960
|
+
if (await this.tryToggleViaLabel(object.objectId, false)) {
|
|
2961
|
+
return true;
|
|
2962
|
+
}
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
"Clicking the checkbox did not change its state. Tried the associated label too."
|
|
2965
|
+
);
|
|
2670
2966
|
}
|
|
2671
2967
|
return true;
|
|
2672
2968
|
});
|
|
@@ -2990,7 +3286,7 @@ var Page = class {
|
|
|
2990
3286
|
}
|
|
2991
3287
|
const result = await this.cdp.send("Runtime.evaluate", params);
|
|
2992
3288
|
if (result.exceptionDetails) {
|
|
2993
|
-
throw new Error(
|
|
3289
|
+
throw new Error(this.formatEvaluationError(result.exceptionDetails));
|
|
2994
3290
|
}
|
|
2995
3291
|
return result.result.value;
|
|
2996
3292
|
}
|
|
@@ -3042,6 +3338,75 @@ var Page = class {
|
|
|
3042
3338
|
return result.result.value ?? "";
|
|
3043
3339
|
});
|
|
3044
3340
|
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Enumerate form controls on the page with labels and current state.
|
|
3343
|
+
*/
|
|
3344
|
+
async forms() {
|
|
3345
|
+
const result = await this.evaluateInFrame(
|
|
3346
|
+
`(() => {
|
|
3347
|
+
function normalize(value) {
|
|
3348
|
+
return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
function labelFor(el) {
|
|
3352
|
+
if (!el) return '';
|
|
3353
|
+
if (el.labels && el.labels.length) {
|
|
3354
|
+
return normalize(
|
|
3355
|
+
Array.from(el.labels)
|
|
3356
|
+
.map((label) => label.innerText || label.textContent || '')
|
|
3357
|
+
.join(' ')
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
|
|
3361
|
+
if (ariaLabel) return ariaLabel;
|
|
3362
|
+
if (el.id) {
|
|
3363
|
+
var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
|
|
3364
|
+
if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
|
|
3365
|
+
}
|
|
3366
|
+
var closest = el.closest && el.closest('label');
|
|
3367
|
+
if (closest) return normalize(closest.innerText || closest.textContent || '');
|
|
3368
|
+
return '';
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
|
|
3372
|
+
var tag = el.tagName.toLowerCase();
|
|
3373
|
+
var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
|
|
3374
|
+
var value = null;
|
|
3375
|
+
|
|
3376
|
+
if (tag === 'select') {
|
|
3377
|
+
value = el.multiple
|
|
3378
|
+
? Array.from(el.selectedOptions).map((opt) => opt.value)
|
|
3379
|
+
: el.value || null;
|
|
3380
|
+
} else if (tag === 'textarea' || tag === 'input') {
|
|
3381
|
+
value = typeof el.value === 'string' ? el.value : null;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
return {
|
|
3385
|
+
tag: tag,
|
|
3386
|
+
type: type,
|
|
3387
|
+
id: el.id || undefined,
|
|
3388
|
+
name: el.getAttribute('name') || undefined,
|
|
3389
|
+
value: value,
|
|
3390
|
+
checked: 'checked' in el ? !!el.checked : undefined,
|
|
3391
|
+
required: !!el.required,
|
|
3392
|
+
disabled: !!el.disabled,
|
|
3393
|
+
label: labelFor(el) || undefined,
|
|
3394
|
+
placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
|
|
3395
|
+
options:
|
|
3396
|
+
tag === 'select'
|
|
3397
|
+
? Array.from(el.options).map((opt) => ({
|
|
3398
|
+
value: opt.value || '',
|
|
3399
|
+
text: normalize(opt.text || opt.label || ''),
|
|
3400
|
+
selected: !!opt.selected,
|
|
3401
|
+
disabled: !!opt.disabled,
|
|
3402
|
+
}))
|
|
3403
|
+
: undefined,
|
|
3404
|
+
};
|
|
3405
|
+
});
|
|
3406
|
+
})()`
|
|
3407
|
+
);
|
|
3408
|
+
return result.result.value ?? [];
|
|
3409
|
+
}
|
|
3045
3410
|
// ============ File Handling ============
|
|
3046
3411
|
/**
|
|
3047
3412
|
* Set files on a file input
|
|
@@ -3514,7 +3879,8 @@ var Page = class {
|
|
|
3514
3879
|
/**
|
|
3515
3880
|
* Get an accessibility tree snapshot of the page
|
|
3516
3881
|
*/
|
|
3517
|
-
async snapshot() {
|
|
3882
|
+
async snapshot(options = {}) {
|
|
3883
|
+
const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
|
|
3518
3884
|
const [url, title, axTree] = await Promise.all([
|
|
3519
3885
|
this.url(),
|
|
3520
3886
|
this.title(),
|
|
@@ -3535,7 +3901,7 @@ var Page = class {
|
|
|
3535
3901
|
const buildNode = (nodeId) => {
|
|
3536
3902
|
const node = nodeMap.get(nodeId);
|
|
3537
3903
|
if (!node) return null;
|
|
3538
|
-
const role = node.role?.value ?? "generic";
|
|
3904
|
+
const role = (node.role?.value ?? "generic").toLowerCase();
|
|
3539
3905
|
const name = node.name?.value;
|
|
3540
3906
|
const value = node.value?.value;
|
|
3541
3907
|
const ref = nodeRefs.get(nodeId);
|
|
@@ -3551,7 +3917,7 @@ var Page = class {
|
|
|
3551
3917
|
return {
|
|
3552
3918
|
role,
|
|
3553
3919
|
name,
|
|
3554
|
-
value,
|
|
3920
|
+
value: value !== void 0 ? String(value) : void 0,
|
|
3555
3921
|
ref,
|
|
3556
3922
|
children: children.length > 0 ? children : void 0,
|
|
3557
3923
|
disabled,
|
|
@@ -3559,7 +3925,24 @@ var Page = class {
|
|
|
3559
3925
|
};
|
|
3560
3926
|
};
|
|
3561
3927
|
const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
|
|
3562
|
-
|
|
3928
|
+
let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
|
|
3929
|
+
if (roleFilter.size > 0) {
|
|
3930
|
+
const filteredAccessibilityTree = [];
|
|
3931
|
+
for (const node of nodes) {
|
|
3932
|
+
if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3935
|
+
const snapshotNode = buildNode(node.nodeId);
|
|
3936
|
+
if (!snapshotNode) {
|
|
3937
|
+
continue;
|
|
3938
|
+
}
|
|
3939
|
+
filteredAccessibilityTree.push({
|
|
3940
|
+
...snapshotNode,
|
|
3941
|
+
children: void 0
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
accessibilityTree = filteredAccessibilityTree;
|
|
3945
|
+
}
|
|
3563
3946
|
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
3564
3947
|
"button",
|
|
3565
3948
|
"link",
|
|
@@ -3581,37 +3964,44 @@ var Page = class {
|
|
|
3581
3964
|
]);
|
|
3582
3965
|
const interactiveElements = [];
|
|
3583
3966
|
for (const node of nodes) {
|
|
3584
|
-
const role = node.role?.value;
|
|
3585
|
-
if (role && interactiveRoles.has(role)) {
|
|
3967
|
+
const role = (node.role?.value ?? "").toLowerCase();
|
|
3968
|
+
if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
|
|
3586
3969
|
const ref = nodeRefs.get(node.nodeId);
|
|
3587
3970
|
const name = node.name?.value ?? "";
|
|
3588
3971
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
3972
|
+
const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
|
|
3973
|
+
const value = node.value?.value;
|
|
3589
3974
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
3590
3975
|
interactiveElements.push({
|
|
3591
3976
|
ref,
|
|
3592
3977
|
role,
|
|
3593
3978
|
name,
|
|
3594
3979
|
selector,
|
|
3595
|
-
disabled
|
|
3980
|
+
disabled,
|
|
3981
|
+
checked,
|
|
3982
|
+
value: value !== void 0 ? String(value) : void 0
|
|
3596
3983
|
});
|
|
3597
3984
|
}
|
|
3598
3985
|
}
|
|
3986
|
+
const formatNode = (node, depth = 0) => {
|
|
3987
|
+
let line = `${" ".repeat(depth)}- ${node.role}`;
|
|
3988
|
+
if (node.name) line += ` "${node.name}"`;
|
|
3989
|
+
line += ` ref:${node.ref}`;
|
|
3990
|
+
if (node.disabled) line += " (disabled)";
|
|
3991
|
+
if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
|
|
3992
|
+
return line;
|
|
3993
|
+
};
|
|
3599
3994
|
const formatTree = (nodes2, depth = 0) => {
|
|
3600
3995
|
const lines = [];
|
|
3601
3996
|
for (const node of nodes2) {
|
|
3602
|
-
|
|
3603
|
-
if (node.name) line += ` "${node.name}"`;
|
|
3604
|
-
line += ` [ref=${node.ref}]`;
|
|
3605
|
-
if (node.disabled) line += " (disabled)";
|
|
3606
|
-
if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
|
|
3607
|
-
lines.push(line);
|
|
3997
|
+
lines.push(formatNode(node, depth));
|
|
3608
3998
|
if (node.children) {
|
|
3609
3999
|
lines.push(formatTree(node.children, depth + 1));
|
|
3610
4000
|
}
|
|
3611
4001
|
}
|
|
3612
4002
|
return lines.join("\n");
|
|
3613
4003
|
};
|
|
3614
|
-
const text = formatTree(accessibilityTree);
|
|
4004
|
+
const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
|
|
3615
4005
|
const result = {
|
|
3616
4006
|
url,
|
|
3617
4007
|
title,
|
|
@@ -3620,7 +4010,9 @@ var Page = class {
|
|
|
3620
4010
|
interactiveElements,
|
|
3621
4011
|
text
|
|
3622
4012
|
};
|
|
3623
|
-
|
|
4013
|
+
if (roleFilter.size === 0) {
|
|
4014
|
+
this.lastSnapshot = result;
|
|
4015
|
+
}
|
|
3624
4016
|
return result;
|
|
3625
4017
|
}
|
|
3626
4018
|
/**
|
|
@@ -4175,7 +4567,7 @@ var Page = class {
|
|
|
4175
4567
|
}
|
|
4176
4568
|
/**
|
|
4177
4569
|
* Find an element using single or multiple selectors
|
|
4178
|
-
* Supports ref
|
|
4570
|
+
* Supports ref:, text:, and role: selectors.
|
|
4179
4571
|
*/
|
|
4180
4572
|
async findElement(selectors, options = {}) {
|
|
4181
4573
|
const { timeout = DEFAULT_TIMEOUT } = options;
|
|
@@ -4242,11 +4634,11 @@ var Page = class {
|
|
|
4242
4634
|
}
|
|
4243
4635
|
}
|
|
4244
4636
|
}
|
|
4245
|
-
const
|
|
4246
|
-
if (
|
|
4637
|
+
const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
|
|
4638
|
+
if (runtimeSelectors.length === 0) {
|
|
4247
4639
|
return null;
|
|
4248
4640
|
}
|
|
4249
|
-
const result = await waitForAnyElement(this.cdp,
|
|
4641
|
+
const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
|
|
4250
4642
|
state: "visible",
|
|
4251
4643
|
timeout,
|
|
4252
4644
|
contextId: this.currentFrameContextId ?? void 0
|
|
@@ -4254,6 +4646,14 @@ var Page = class {
|
|
|
4254
4646
|
if (!result.success || !result.selector) {
|
|
4255
4647
|
return null;
|
|
4256
4648
|
}
|
|
4649
|
+
const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
|
|
4650
|
+
if (specialSelectorMatch) {
|
|
4651
|
+
this._lastMatchedSelector = result.selector;
|
|
4652
|
+
return {
|
|
4653
|
+
...specialSelectorMatch,
|
|
4654
|
+
waitedMs: result.waitedMs
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4257
4657
|
await this.ensureRootNode();
|
|
4258
4658
|
const queryResult = await this.cdp.send("DOM.querySelector", {
|
|
4259
4659
|
nodeId: this.rootNodeId,
|
|
@@ -4300,6 +4700,122 @@ var Page = class {
|
|
|
4300
4700
|
waitedMs: result.waitedMs
|
|
4301
4701
|
};
|
|
4302
4702
|
}
|
|
4703
|
+
formatEvaluationError(details) {
|
|
4704
|
+
const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
|
|
4705
|
+
return `Evaluation failed: ${description}`;
|
|
4706
|
+
}
|
|
4707
|
+
async resolveSpecialSelector(selector, options = {}) {
|
|
4708
|
+
const expression = buildSpecialSelectorLookupExpression(selector, options);
|
|
4709
|
+
if (!expression) return null;
|
|
4710
|
+
const result = await this.evaluateInFrame(expression, {
|
|
4711
|
+
returnByValue: false
|
|
4712
|
+
});
|
|
4713
|
+
if (!result.result.objectId) {
|
|
4714
|
+
return null;
|
|
4715
|
+
}
|
|
4716
|
+
const resolved = await this.objectIdToNode(result.result.objectId);
|
|
4717
|
+
if (!resolved) {
|
|
4718
|
+
return null;
|
|
4719
|
+
}
|
|
4720
|
+
return {
|
|
4721
|
+
nodeId: resolved.nodeId,
|
|
4722
|
+
backendNodeId: resolved.backendNodeId,
|
|
4723
|
+
selector,
|
|
4724
|
+
waitedMs: 0
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
async readCheckedState(objectId) {
|
|
4728
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4729
|
+
objectId,
|
|
4730
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4731
|
+
returnByValue: true
|
|
4732
|
+
});
|
|
4733
|
+
return result.result.value === true;
|
|
4734
|
+
}
|
|
4735
|
+
async readInputType(objectId) {
|
|
4736
|
+
const result = await this.cdp.send(
|
|
4737
|
+
"Runtime.callFunctionOn",
|
|
4738
|
+
{
|
|
4739
|
+
objectId,
|
|
4740
|
+
functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
|
|
4741
|
+
returnByValue: true
|
|
4742
|
+
}
|
|
4743
|
+
);
|
|
4744
|
+
return result.result.value ?? null;
|
|
4745
|
+
}
|
|
4746
|
+
async getAssociatedLabelNodeId(objectId) {
|
|
4747
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4748
|
+
objectId,
|
|
4749
|
+
functionDeclaration: `function() {
|
|
4750
|
+
if (!(this instanceof HTMLInputElement)) return null;
|
|
4751
|
+
|
|
4752
|
+
if (this.id) {
|
|
4753
|
+
var labels = Array.from(document.querySelectorAll('label'));
|
|
4754
|
+
for (var i = 0; i < labels.length; i++) {
|
|
4755
|
+
if (labels[i].htmlFor === this.id) return labels[i];
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
return this.closest('label');
|
|
4760
|
+
}`,
|
|
4761
|
+
returnByValue: false
|
|
4762
|
+
});
|
|
4763
|
+
if (!result.result.objectId) {
|
|
4764
|
+
return null;
|
|
4765
|
+
}
|
|
4766
|
+
return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
|
|
4767
|
+
}
|
|
4768
|
+
async objectIdToNode(objectId) {
|
|
4769
|
+
const describeResult = await this.cdp.send("DOM.describeNode", {
|
|
4770
|
+
objectId,
|
|
4771
|
+
depth: 0
|
|
4772
|
+
});
|
|
4773
|
+
const backendNodeId = describeResult.node.backendNodeId;
|
|
4774
|
+
if (!backendNodeId) {
|
|
4775
|
+
return null;
|
|
4776
|
+
}
|
|
4777
|
+
if (describeResult.node.nodeId) {
|
|
4778
|
+
return {
|
|
4779
|
+
nodeId: describeResult.node.nodeId,
|
|
4780
|
+
backendNodeId
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
await this.ensureRootNode();
|
|
4784
|
+
const pushResult = await this.cdp.send(
|
|
4785
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
4786
|
+
{
|
|
4787
|
+
backendNodeIds: [backendNodeId]
|
|
4788
|
+
}
|
|
4789
|
+
);
|
|
4790
|
+
const nodeId = pushResult.nodeIds?.[0];
|
|
4791
|
+
if (!nodeId) {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
return { nodeId, backendNodeId };
|
|
4795
|
+
}
|
|
4796
|
+
async tryClickAssociatedLabel(objectId) {
|
|
4797
|
+
const inputType = await this.readInputType(objectId);
|
|
4798
|
+
if (inputType !== "checkbox" && inputType !== "radio") {
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
|
|
4802
|
+
if (!labelNodeId) {
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
try {
|
|
4806
|
+
await this.scrollIntoView(labelNodeId);
|
|
4807
|
+
await this.clickElement(labelNodeId);
|
|
4808
|
+
return true;
|
|
4809
|
+
} catch {
|
|
4810
|
+
return false;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
async tryToggleViaLabel(objectId, desiredChecked) {
|
|
4814
|
+
if (!await this.tryClickAssociatedLabel(objectId)) {
|
|
4815
|
+
return false;
|
|
4816
|
+
}
|
|
4817
|
+
return await this.readCheckedState(objectId) === desiredChecked;
|
|
4818
|
+
}
|
|
4303
4819
|
/**
|
|
4304
4820
|
* Ensure we have a valid root node ID
|
|
4305
4821
|
*/
|
|
@@ -4717,6 +5233,7 @@ var Browser = class _Browser {
|
|
|
4717
5233
|
cdp;
|
|
4718
5234
|
providerSession;
|
|
4719
5235
|
pages = /* @__PURE__ */ new Map();
|
|
5236
|
+
pageCounter = 0;
|
|
4720
5237
|
constructor(cdp, _provider, providerSession, _options) {
|
|
4721
5238
|
this.cdp = cdp;
|
|
4722
5239
|
this.providerSession = providerSession;
|
|
@@ -4746,7 +5263,11 @@ var Browser = class _Browser {
|
|
|
4746
5263
|
const pageName = name ?? "default";
|
|
4747
5264
|
const cached = this.pages.get(pageName);
|
|
4748
5265
|
if (cached) return cached;
|
|
4749
|
-
const targets = await this.cdp.send(
|
|
5266
|
+
const targets = await this.cdp.send(
|
|
5267
|
+
"Target.getTargets",
|
|
5268
|
+
void 0,
|
|
5269
|
+
null
|
|
5270
|
+
);
|
|
4750
5271
|
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
4751
5272
|
if (options?.targetUrl) {
|
|
4752
5273
|
const urlFilter = options.targetUrl;
|
|
@@ -4768,16 +5289,24 @@ var Browser = class _Browser {
|
|
|
4768
5289
|
targetId = options.targetId;
|
|
4769
5290
|
} else {
|
|
4770
5291
|
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
4771
|
-
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
4772
|
-
|
|
4773
|
-
|
|
5292
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
5293
|
+
"Target.createTarget",
|
|
5294
|
+
{
|
|
5295
|
+
url: "about:blank"
|
|
5296
|
+
},
|
|
5297
|
+
null
|
|
5298
|
+
)).targetId;
|
|
4774
5299
|
}
|
|
4775
5300
|
} else if (pageTargets.length > 0) {
|
|
4776
5301
|
targetId = pickBestTarget(pageTargets);
|
|
4777
5302
|
} else {
|
|
4778
|
-
const result = await this.cdp.send(
|
|
4779
|
-
|
|
4780
|
-
|
|
5303
|
+
const result = await this.cdp.send(
|
|
5304
|
+
"Target.createTarget",
|
|
5305
|
+
{
|
|
5306
|
+
url: "about:blank"
|
|
5307
|
+
},
|
|
5308
|
+
null
|
|
5309
|
+
);
|
|
4781
5310
|
targetId = result.targetId;
|
|
4782
5311
|
}
|
|
4783
5312
|
await this.cdp.attachToTarget(targetId);
|
|
@@ -4805,13 +5334,17 @@ var Browser = class _Browser {
|
|
|
4805
5334
|
* Create a new page (tab)
|
|
4806
5335
|
*/
|
|
4807
5336
|
async newPage(url = "about:blank") {
|
|
4808
|
-
const result = await this.cdp.send(
|
|
4809
|
-
|
|
4810
|
-
|
|
5337
|
+
const result = await this.cdp.send(
|
|
5338
|
+
"Target.createTarget",
|
|
5339
|
+
{
|
|
5340
|
+
url
|
|
5341
|
+
},
|
|
5342
|
+
null
|
|
5343
|
+
);
|
|
4811
5344
|
await this.cdp.attachToTarget(result.targetId);
|
|
4812
5345
|
const page = new Page(this.cdp, result.targetId);
|
|
4813
5346
|
await page.init();
|
|
4814
|
-
const name = `page-${this.
|
|
5347
|
+
const name = `page-${++this.pageCounter}`;
|
|
4815
5348
|
this.pages.set(name, page);
|
|
4816
5349
|
return page;
|
|
4817
5350
|
}
|
|
@@ -4821,14 +5354,30 @@ var Browser = class _Browser {
|
|
|
4821
5354
|
async closePage(name) {
|
|
4822
5355
|
const page = this.pages.get(name);
|
|
4823
5356
|
if (!page) return;
|
|
4824
|
-
const
|
|
4825
|
-
|
|
4826
|
-
if (pageTargets.length > 0) {
|
|
4827
|
-
await this.cdp.send("Target.closeTarget", {
|
|
4828
|
-
targetId: pageTargets[0].targetId
|
|
4829
|
-
});
|
|
4830
|
-
}
|
|
5357
|
+
const targetId = page.targetId;
|
|
5358
|
+
await this.cdp.send("Target.closeTarget", { targetId }, null);
|
|
4831
5359
|
this.pages.delete(name);
|
|
5360
|
+
const deadline = Date.now() + 5e3;
|
|
5361
|
+
while (Date.now() < deadline) {
|
|
5362
|
+
const { targetInfos } = await this.cdp.send(
|
|
5363
|
+
"Target.getTargets",
|
|
5364
|
+
void 0,
|
|
5365
|
+
null
|
|
5366
|
+
);
|
|
5367
|
+
if (!targetInfos.some((t) => t.targetId === targetId)) return;
|
|
5368
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
/**
|
|
5372
|
+
* List all page targets in the connected browser.
|
|
5373
|
+
*/
|
|
5374
|
+
async listTargets() {
|
|
5375
|
+
const { targetInfos } = await this.cdp.send(
|
|
5376
|
+
"Target.getTargets",
|
|
5377
|
+
void 0,
|
|
5378
|
+
null
|
|
5379
|
+
);
|
|
5380
|
+
return targetInfos.filter((target) => target.type === "page");
|
|
4832
5381
|
}
|
|
4833
5382
|
/**
|
|
4834
5383
|
* Get the WebSocket URL for this browser connection
|