@@ -26,6 +26,7 @@ import {
2626 NoSuchElementException ,
2727 NoSuchFrameException ,
2828 NoSuchHistoryEntryException ,
29+ NoSuchNodeException ,
2930 Script ,
3031 Session ,
3132 type UAClientHints ,
@@ -43,7 +44,7 @@ import type {ContextConfigStorage} from '../browser/ContextConfigStorage.js';
4344import type { CdpTarget } from '../cdp/CdpTarget.js' ;
4445import type { Realm } from '../script/Realm.js' ;
4546import type { RealmStorage } from '../script/RealmStorage.js' ;
46- import { getSharedId } from '../script/SharedId.js' ;
47+ import { getSharedId , parseSharedId } from '../script/SharedId.js' ;
4748import { WindowRealm } from '../script/WindowRealm.js' ;
4849import type { EventManager } from '../session/EventManager.js' ;
4950
@@ -1441,17 +1442,17 @@ export class BrowsingContextImpl {
14411442 ) ;
14421443 }
14431444
1444- async #getLocatorDelegate(
1445- realm : Realm ,
1445+ #getLocatorDelegate(
14461446 locator : BrowsingContext . Locator ,
14471447 maxNodeCount : number | undefined ,
14481448 startNodes : Script . SharedReference [ ] ,
1449- ) : Promise < {
1449+ ) : {
14501450 functionDeclaration : string ;
14511451 argumentsLocalValues : Script . LocalValue [ ] ;
1452- } > {
1452+ } {
14531453 switch ( locator . type ) {
14541454 case 'context' :
1455+ case 'accessibility' :
14551456 throw new Error ( 'Unreachable' ) ;
14561457 case 'css' :
14571458 return {
@@ -1660,125 +1661,6 @@ export class BrowsingContextImpl {
16601661 ...startNodes ,
16611662 ] ,
16621663 } ;
1663- case 'accessibility' : {
1664- // https://w3c.github.io/webdriver-bidi/#locate-nodes-using-accessibility-attributes
1665- if ( ! locator . value . name && ! locator . value . role ) {
1666- throw new InvalidSelectorException (
1667- 'Either name or role has to be specified' ,
1668- ) ;
1669- }
1670-
1671- // The next two commands cause a11y caches for the target to be
1672- // preserved. We probably do not need to disable them if the
1673- // client is using a11y features, but we could by calling
1674- // Accessibility.disable.
1675- await Promise . all ( [
1676- this . #cdpTarget. cdpClient . sendCommand ( 'Accessibility.enable' ) ,
1677- this . #cdpTarget. cdpClient . sendCommand ( 'Accessibility.getRootAXNode' ) ,
1678- ] ) ;
1679- const bindings = await realm . evaluate (
1680- /* expression=*/ '({getAccessibleName, getAccessibleRole})' ,
1681- /* awaitPromise=*/ false ,
1682- /* resultOwnership=*/ Script . ResultOwnership . Root ,
1683- /* serializationOptions= */ undefined ,
1684- /* userActivation=*/ false ,
1685- /* includeCommandLineApi=*/ true ,
1686- ) ;
1687-
1688- if ( bindings . type !== 'success' ) {
1689- throw new Error ( 'Could not get bindings' ) ;
1690- }
1691-
1692- if ( bindings . result . type !== 'object' ) {
1693- throw new Error ( 'Could not get bindings' ) ;
1694- }
1695- return {
1696- functionDeclaration : String (
1697- (
1698- name : string ,
1699- role : string ,
1700- bindings : any ,
1701- maxNodeCount : number ,
1702- ...startNodes : Element [ ]
1703- ) => {
1704- const returnedNodes : Element [ ] = [ ] ;
1705-
1706- let aborted = false ;
1707-
1708- function collect (
1709- contextNodes : Element [ ] ,
1710- selector : { role : string ; name : string } ,
1711- ) {
1712- if ( aborted ) {
1713- return ;
1714- }
1715- for ( const contextNode of contextNodes ) {
1716- let match = true ;
1717-
1718- if ( selector . role ) {
1719- const role = bindings . getAccessibleRole ( contextNode ) ;
1720- if ( selector . role !== role ) {
1721- match = false ;
1722- }
1723- }
1724-
1725- if ( selector . name ) {
1726- const name = bindings . getAccessibleName ( contextNode ) ;
1727- if ( selector . name !== name ) {
1728- match = false ;
1729- }
1730- }
1731-
1732- if ( match ) {
1733- if (
1734- maxNodeCount !== 0 &&
1735- returnedNodes . length === maxNodeCount
1736- ) {
1737- aborted = true ;
1738- break ;
1739- }
1740-
1741- returnedNodes . push ( contextNode ) ;
1742- }
1743-
1744- const childNodes : Element [ ] = [ ] ;
1745- for ( const child of contextNode . children ) {
1746- if ( child instanceof HTMLElement ) {
1747- childNodes . push ( child ) ;
1748- }
1749- }
1750-
1751- collect ( childNodes , selector ) ;
1752- }
1753- }
1754-
1755- startNodes =
1756- startNodes . length > 0
1757- ? startNodes
1758- : Array . from ( document . documentElement . children ) . filter (
1759- ( c ) => c instanceof HTMLElement ,
1760- ) ;
1761- collect ( startNodes , {
1762- role,
1763- name,
1764- } ) ;
1765- return returnedNodes ;
1766- } ,
1767- ) ,
1768- argumentsLocalValues : [
1769- // `name`
1770- { type : 'string' , value : locator . value . name || '' } ,
1771- // `role`
1772- { type : 'string' , value : locator . value . role || '' } ,
1773- // `bindings`.
1774- { handle : bindings . result . handle ! } ,
1775- // `maxNodeCount` with `0` means no limit.
1776- { type : 'number' , value : maxNodeCount ?? 0 } ,
1777- // `startNodes`
1778- ...startNodes ,
1779- ] ,
1780- } ;
1781- }
17821664 }
17831665 }
17841666
@@ -1790,49 +1672,25 @@ export class BrowsingContextImpl {
17901672 serializationOptions : Script . SerializationOptions | undefined ,
17911673 ) : Promise < BrowsingContext . LocateNodesResult > {
17921674 if ( locator . type === 'context' ) {
1793- if ( startNodes . length !== 0 ) {
1794- throw new InvalidArgumentException ( 'Start nodes are not supported' ) ;
1795- }
1796- const contextId = locator . value . context ;
1797- if ( ! contextId ) {
1798- throw new InvalidSelectorException ( 'Invalid context' ) ;
1799- }
1800- const context = this . #browsingContextStorage. getContext ( contextId ) ;
1801- const parent = context . parent ;
1802- if ( ! parent ) {
1803- throw new InvalidArgumentException ( 'This context has no container' ) ;
1804- }
1805- try {
1806- const { backendNodeId} = await parent . #cdpTarget. cdpClient . sendCommand (
1807- 'DOM.getFrameOwner' ,
1808- {
1809- frameId : contextId ,
1810- } ,
1811- ) ;
1812- const { object} = await parent . #cdpTarget. cdpClient . sendCommand (
1813- 'DOM.resolveNode' ,
1814- {
1815- backendNodeId,
1816- } ,
1817- ) ;
1818- const locatorResult = await realm . callFunction (
1819- `function () { return this; }` ,
1820- false ,
1821- { handle : object . objectId ! } ,
1822- [ ] ,
1823- Script . ResultOwnership . None ,
1824- serializationOptions ,
1825- ) ;
1826- if ( locatorResult . type === 'exception' ) {
1827- throw new Error ( 'Unknown exception' ) ;
1828- }
1829- return { nodes : [ locatorResult . result as Script . NodeRemoteValue ] } ;
1830- } catch {
1831- throw new InvalidArgumentException ( 'Context does not exist' ) ;
1832- }
1675+ return await this . #locateNodesByContextLocator(
1676+ locator ,
1677+ startNodes ,
1678+ realm ,
1679+ serializationOptions ,
1680+ ) ;
1681+ }
1682+
1683+ if ( locator . type === 'accessibility' ) {
1684+ return await this . #locateNodesByAccessibility(
1685+ locator ,
1686+ startNodes ,
1687+ maxNodeCount ,
1688+ realm ,
1689+ ) ;
18331690 }
1834- const locatorDelegate = await this . #getLocatorDelegate(
1835- realm ,
1691+
1692+ // Select by injecting a script into the realm.
1693+ const locatorDelegate = this . #getLocatorDelegate(
18361694 locator ,
18371695 maxNodeCount ,
18381696 startNodes ,
@@ -1910,6 +1768,149 @@ export class BrowsingContextImpl {
19101768 return { nodes} ;
19111769 }
19121770
1771+ async #locateNodesByContextLocator(
1772+ locator : BrowsingContext . ContextLocator ,
1773+ startNodes : Script . SharedReference [ ] ,
1774+ realm : Realm ,
1775+ serializationOptions : Script . SerializationOptions | undefined ,
1776+ ) : Promise < BrowsingContext . LocateNodesResult > {
1777+ if ( startNodes . length !== 0 ) {
1778+ throw new InvalidArgumentException ( 'Start nodes are not supported' ) ;
1779+ }
1780+ const contextId = locator . value . context ;
1781+ if ( ! contextId ) {
1782+ throw new InvalidSelectorException ( 'Invalid context' ) ;
1783+ }
1784+ const context = this . #browsingContextStorage. getContext ( contextId ) ;
1785+ const parent = context . parent ;
1786+ if ( ! parent ) {
1787+ throw new InvalidArgumentException ( 'This context has no container' ) ;
1788+ }
1789+ try {
1790+ const { backendNodeId} = await parent . #cdpTarget. cdpClient . sendCommand (
1791+ 'DOM.getFrameOwner' ,
1792+ {
1793+ frameId : contextId ,
1794+ } ,
1795+ ) ;
1796+ const { object} = await parent . #cdpTarget. cdpClient . sendCommand (
1797+ 'DOM.resolveNode' ,
1798+ {
1799+ backendNodeId,
1800+ } ,
1801+ ) ;
1802+ const locatorResult = await realm . callFunction (
1803+ `function () { return this; }` ,
1804+ false ,
1805+ { handle : object . objectId ! } ,
1806+ [ ] ,
1807+ Script . ResultOwnership . None ,
1808+ serializationOptions ,
1809+ ) ;
1810+ if ( locatorResult . type === 'exception' ) {
1811+ throw new Error ( 'Unknown exception' ) ;
1812+ }
1813+ return { nodes : [ locatorResult . result as Script . NodeRemoteValue ] } ;
1814+ } catch {
1815+ throw new InvalidArgumentException ( 'Context does not exist' ) ;
1816+ }
1817+ }
1818+
1819+ async #locateNodesByAccessibility(
1820+ locator : BrowsingContext . AccessibilityLocator ,
1821+ startNodes : Script . SharedReference [ ] ,
1822+ maxNodeCount : number | undefined ,
1823+ realm : Realm ,
1824+ ) {
1825+ if ( ! locator . value . name && ! locator . value . role ) {
1826+ throw new InvalidSelectorException (
1827+ 'Either name or role has to be specified' ,
1828+ ) ;
1829+ }
1830+ await this . #cdpTarget. cdpClient . sendCommand ( 'Accessibility.enable' ) ;
1831+
1832+ const startBackendNodeIds : number [ ] = [ ] ;
1833+ if ( startNodes . length === 0 ) {
1834+ const { root : documentRoot } =
1835+ await this . #cdpTarget. cdpClient . sendCommand ( 'DOM.getDocument' ) ;
1836+ startBackendNodeIds . push ( documentRoot . backendNodeId ) ;
1837+ } else {
1838+ for ( const node of startNodes ) {
1839+ if ( node . sharedId ) {
1840+ const parsed = parseSharedId ( node . sharedId ) ;
1841+ if ( ! parsed ) {
1842+ throw new NoSuchNodeException ( `Invalid sharedId: ${ node . sharedId } ` ) ;
1843+ }
1844+ startBackendNodeIds . push ( parsed . backendNodeId ) ;
1845+ } else {
1846+ if ( node . handle ) {
1847+ const { nodeId} = await this . #cdpTarget. cdpClient . sendCommand (
1848+ 'DOM.requestNode' ,
1849+ {
1850+ objectId : node . handle ,
1851+ } ,
1852+ ) ;
1853+ const { node : describedNode } =
1854+ await this . #cdpTarget. cdpClient . sendCommand ( 'DOM.describeNode' , {
1855+ nodeId,
1856+ } ) ;
1857+ startBackendNodeIds . push ( describedNode . backendNodeId ) ;
1858+ } else {
1859+ throw new NoSuchNodeException (
1860+ 'Start node must have sharedId or handle' ,
1861+ ) ;
1862+ }
1863+ }
1864+ }
1865+ }
1866+
1867+ const matchedBackendNodeIds = new Set < number > ( ) ;
1868+ for ( const backendNodeId of startBackendNodeIds ) {
1869+ const { nodes} = await this . #cdpTarget. cdpClient . sendCommand (
1870+ 'Accessibility.queryAXTree' ,
1871+ {
1872+ backendNodeId,
1873+ accessibleName : locator . value . name ,
1874+ role : locator . value . role ,
1875+ } ,
1876+ ) ;
1877+
1878+ for ( const node of nodes ) {
1879+ if ( node . backendDOMNodeId && node . role ?. type === 'role' ) {
1880+ matchedBackendNodeIds . add ( node . backendDOMNodeId ) ;
1881+ if (
1882+ maxNodeCount !== undefined &&
1883+ maxNodeCount > 0 &&
1884+ matchedBackendNodeIds . size >= maxNodeCount
1885+ ) {
1886+ break ;
1887+ }
1888+ }
1889+ }
1890+ }
1891+
1892+ const resultNodes = await Promise . all (
1893+ Array . from ( matchedBackendNodeIds ) . map ( async ( backendNodeId ) => {
1894+ const { object} = await this . #cdpTarget. cdpClient . sendCommand (
1895+ 'DOM.resolveNode' ,
1896+ {
1897+ backendNodeId,
1898+ } ,
1899+ ) ;
1900+ // We need to use `serializeCdpObject` to convert it to BiDi format.
1901+ // We use `Script.ResultOwnership.None` as `locateNodes` returns weak references (nodes).
1902+ return await realm . serializeCdpObject (
1903+ object ,
1904+ Script . ResultOwnership . None ,
1905+ ) ;
1906+ } ) ,
1907+ ) ;
1908+
1909+ return {
1910+ nodes : resultNodes . filter ( ( result ) => result . type === 'node' ) ,
1911+ } ;
1912+ }
1913+
19131914 #getAllRelatedCdpTargets( ) {
19141915 const targets = new Set < CdpTarget > ( ) ;
19151916 targets . add ( this . cdpTarget ) ;
0 commit comments