Skip to content

Commit

Permalink
feat: accessibility locator
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed Apr 22, 2024
1 parent c7c92a2 commit 8ea8e1e
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 51 deletions.
110 changes: 105 additions & 5 deletions src/bidiMapper/modules/context/BrowsingContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,8 @@ export class BrowsingContextImpl {
#getLocatorDelegate(
locator: BrowsingContext.Locator,
maxNodeCount: number | undefined,
startNodes: Script.SharedReference[]
startNodes: Script.SharedReference[],
bindings: Script.RemoteReference
): {
functionDeclaration: string;
argumentsLocalValues: Script.LocalValue[];
Expand Down Expand Up @@ -1227,9 +1228,90 @@ export class BrowsingContextImpl {
],
};
case 'accessibility':
throw new UnsupportedOperationException(
'accessibility locator is not supported yet'
);
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-accessibility-attributes
if (!locator.value.name && !locator.value.role) {
throw new InvalidSelectorException(
'Either name or role has to be specified'
);
}
return {
functionDeclaration: String(
(
name: string,
role: string,
bindings: any,
maxNodeCount: number,
...startNodes: Element[]
) => {
const returnedNodes: Element[] = [];

function collect(
contextNodes: Element[],
selector: {role: string; name: string}
) {
for (const contextNode of contextNodes) {
let match = true;

if (selector.role) {
const role = bindings.getAccessibleRole(contextNode);
if (selector.role !== role) {
match = false;
}
}

if (selector.name) {
const name = bindings.getAccessibleName(contextNode);
if (selector.name !== name) {
match = false;
}
}

if (match) {
if (
maxNodeCount !== 0 &&
returnedNodes.length === maxNodeCount
) {
break;
}

returnedNodes.push(contextNode);
}

const childNodes: Element[] = [];
for (const child of contextNode.children) {
if (child instanceof HTMLElement) {
childNodes.push(child);
}
}

collect(
childNodes,
selector
);
}
}

startNodes = startNodes.length > 0 ? startNodes : [document.body];
collect(startNodes, {
role,
name,
});
return returnedNodes;
}
),
argumentsLocalValues: [
// `name`
{type: 'string', value: locator.value.name || ''},
// `role`
{type: 'string', value: locator.value.role || ''},
// `bindings`.
bindings,
// `maxNodeCount` with `0` means no limit.
{type: 'number', value: maxNodeCount ?? 0},
// `startNodes`
...startNodes,
],
};
}
}

Expand All @@ -1240,10 +1322,28 @@ export class BrowsingContextImpl {
maxNodeCount: number | undefined,
serializationOptions: Script.SerializationOptions | undefined
): Promise<BrowsingContext.LocateNodesResult> {
const bindings = await realm.evaluate(
/* expression=*/ '({getAccessibleName, getAccessibleRole})',
/* awaitPromise=*/ false,
/* resultOwnership=*/ Script.ResultOwnership.Root,
/* serializationOptions= */ undefined,
/* userActivation=*/ false,
/* includeCommandLineApi=*/ true
);

if (bindings.type !== 'success') {
throw new Error('Could not get bindings');
}

if (bindings.result.type !== 'object') {
throw new Error('Could not get bindings');
}

const locatorDelegate = this.#getLocatorDelegate(
locator,
maxNodeCount,
startNodes
startNodes,
{handle: bindings.result.handle!}
);

serializationOptions = {
Expand Down
4 changes: 3 additions & 1 deletion src/bidiMapper/modules/script/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ export abstract class Realm {
awaitPromise: boolean,
resultOwnership: Script.ResultOwnership = Script.ResultOwnership.None,
serializationOptions: Script.SerializationOptions = {},
userActivation = false
userActivation = false,
includeCommandLineApi = false
): Promise<Script.EvaluateResult> {
const cdpEvaluateResult = await this.cdpClient.sendCommand(
'Runtime.evaluate',
Expand All @@ -204,6 +205,7 @@ export abstract class Realm {
serializationOptions
),
userGesture: userActivation,
includeCommandLineAPI: includeCommandLineApi,
}
);

Expand Down
6 changes: 4 additions & 2 deletions src/bidiMapper/modules/script/WindowRealm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ export class WindowRealm extends Realm {
awaitPromise: boolean,
resultOwnership: Script.ResultOwnership,
serializationOptions: Script.SerializationOptions,
userActivation?: boolean
userActivation?: boolean,
includeCommandLineApi?: boolean
): Promise<Script.EvaluateResult> {
await this.#browsingContextStorage
.getContext(this.#browsingContextId)
Expand All @@ -209,7 +210,8 @@ export class WindowRealm extends Realm {
awaitPromise,
resultOwnership,
serializationOptions,
userActivation
userActivation,
includeCommandLineApi
);
}

Expand Down
95 changes: 52 additions & 43 deletions tests/browsing_context/test_locate_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,36 @@
from test_helpers import ANY_SHARED_ID, execute_command, goto_url


@pytest.mark.parametrize('locator', [
{
'type': 'innerText',
'value': 'foobarBARbaz'
},
{
'type': 'css',
'value': 'div'
},
{
'type': 'xpath',
'value': '//div'
},
])
@pytest.mark.parametrize(
'locator',
[
# {
# 'type': 'innerText',
# 'value': 'foobarBARbaz'
# },
# {
# 'type': 'css',
# 'value': 'div'
# },
# {
# 'type': 'xpath',
# 'value': '//div'
# },
{
'type': 'accessibility',
'value': {
'role': 'button',
'name': 'test'
}
},
])
@pytest.mark.asyncio
async def test_locate_nodes_locator_found(websocket, context_id, html,
locator):
await goto_url(
websocket, context_id,
html(
'<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBAR<span>baz</span></div>'
'<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBAR<span>baz</span></div><button>test</button>'
))
resp = await execute_command(
websocket, {
Expand Down Expand Up @@ -83,31 +92,31 @@ async def test_locate_nodes_locator_found(websocket, context_id, html,
}


@pytest.mark.parametrize('locator', [
{
'type': 'css',
'value': 'a*b'
},
{
'type': 'xpath',
'value': ''
},
])
@pytest.mark.asyncio
async def test_locate_nodes_locator_invalid(websocket, context_id, html,
locator):
with pytest.raises(Exception,
match=re.escape(
str({
'error': 'invalid selector',
'message': 'Not valid selector ' +
locator['value']
}))):
await execute_command(
websocket, {
'method': 'browsingContext.locateNodes',
'params': {
'context': context_id,
'locator': locator
}
})
# @pytest.mark.parametrize('locator', [
# {
# 'type': 'css',
# 'value': 'a*b'
# },
# {
# 'type': 'xpath',
# 'value': ''
# },
# ])
# @pytest.mark.asyncio
# async def test_locate_nodes_locator_invalid(websocket, context_id, html,
# locator):
# with pytest.raises(Exception,
# match=re.escape(
# str({
# 'error': 'invalid selector',
# 'message': 'Not valid selector ' +
# locator['value']
# }))):
# await execute_command(
# websocket, {
# 'method': 'browsingContext.locateNodes',
# 'params': {
# 'context': context_id,
# 'locator': locator
# }
# })

0 comments on commit 8ea8e1e

Please sign in to comment.