Skip to content

Commit

Permalink
feat: implement accessibility locator (#2148)
Browse files Browse the repository at this point in the history
This PR implements
https://w3c.github.io/webdriver-bidi/#locate-nodes-using-accessibility-attributes

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
OrKoN authored Apr 22, 2024
1 parent c7c92a2 commit e2a6303
Show file tree
Hide file tree
Showing 16 changed files with 129 additions and 209 deletions.
117 changes: 110 additions & 7 deletions src/bidiMapper/modules/context/BrowsingContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,14 +1036,15 @@ export class BrowsingContextImpl {
);
}

#getLocatorDelegate(
async #getLocatorDelegate(
realm: Realm,
locator: BrowsingContext.Locator,
maxNodeCount: number | undefined,
startNodes: Script.SharedReference[]
): {
): Promise<{
functionDeclaration: string;
argumentsLocalValues: Script.LocalValue[];
} {
}> {
switch (locator.type) {
case 'css':
return {
Expand Down Expand Up @@ -1226,10 +1227,111 @@ export class BrowsingContextImpl {
...startNodes,
],
};
case 'accessibility':
throw new UnsupportedOperationException(
'accessibility locator is not supported yet'
case 'accessibility': {
// 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'
);
}
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');
}
return {
functionDeclaration: String(
(
name: string,
role: string,
bindings: any,
maxNodeCount: number,
...startNodes: Element[]
) => {
const returnedNodes: Element[] = [];

let aborted = false;

function collect(
contextNodes: Element[],
selector: {role: string; name: string}
) {
if (aborted) {
return;
}
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
) {
aborted = true;
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`.
{handle: bindings.result.handle!},
// `maxNodeCount` with `0` means no limit.
{type: 'number', value: maxNodeCount ?? 0},
// `startNodes`
...startNodes,
],
};
}
}
}

Expand All @@ -1240,7 +1342,8 @@ export class BrowsingContextImpl {
maxNodeCount: number | undefined,
serializationOptions: Script.SerializationOptions | undefined
): Promise<BrowsingContext.LocateNodesResult> {
const locatorDelegate = this.#getLocatorDelegate(
const locatorDelegate = await this.#getLocatorDelegate(
realm,
locator,
maxNodeCount,
startNodes
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
13 changes: 12 additions & 1 deletion tests/browsing_context/test_locate_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@
'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" aria-label="test" role="button">foobarBARbaz</div><div data-class="two" aria-label="test" role="button">foobarBAR<span>baz</span></div>'
))
resp = await execute_command(
websocket, {
Expand All @@ -57,6 +64,8 @@ async def test_locate_nodes_locator_found(websocket, context_id, html,
'value': {
'attributes': {
'data-class': 'one',
'aria-label': 'test',
'role': 'button',
},
'childNodeCount': 1,
'localName': 'div',
Expand All @@ -71,6 +80,8 @@ async def test_locate_nodes_locator_found(websocket, context_id, html,
'value': {
'attributes': {
'data-class': 'two',
'aria-label': 'test',
'role': 'button',
},
'childNodeCount': 2,
'localName': 'div',
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,3 @@

[test_locate_with_multiple_context_nodes[xpath-.//div[@data-class='one'\]\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value5-expected5\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value6-expected6\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value7-expected7\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value3\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value4\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value5\]]
expected: FAIL

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,3 @@

[test_locate_with_multiple_context_nodes[xpath-.//div[@data-class='one'\]\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value5-expected5\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value6-expected6\]]
expected: FAIL

[test_locate_with_context_nodes[accessibility-value7-expected7\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value3\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value4\]]
expected: FAIL

[test_locate_with_multiple_context_nodes[accessibility-value5\]]
expected: FAIL

This file was deleted.

Loading

0 comments on commit e2a6303

Please sign in to comment.