Skip to content

Commit

Permalink
feat: support innerText locators (#1988)
Browse files Browse the repository at this point in the history
Start implement innerText locator in
[`browsingContext.locateNodes`](https://w3c.github.io/webdriver-bidi/#commands-browsingcontextlocatenodes)
using WebAPI.

Out of scope:
* `maxNodeCount`
* `maxDepth`

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: sadym-chromium <sadym-chromium@users.noreply.github.com>
  • Loading branch information
sadym-chromium and sadym-chromium committed Mar 20, 2024
1 parent 6154420 commit 8c41582
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 50 deletions.
126 changes: 97 additions & 29 deletions src/bidiMapper/domains/context/BrowsingContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,48 +1032,116 @@ export class BrowsingContextImpl {
return await this.#locateNodesByLocator(this.#defaultRealm, params.locator);
}

#getLocatorFunction(locator: BrowsingContext.Locator): string {
#getLocatorDelegate(locator: BrowsingContext.Locator): {
functionDeclaration: string;
argumentsLocalValues: Script.LocalValue[];
} {
switch (locator.type) {
case 'css':
return String((cssSelector: string) => {
const results = document.querySelectorAll(cssSelector);
const array = [];
for (const item of results) {
array.push(item);
}
return array;
});
return {
functionDeclaration: String((cssSelector: string) => {
const results = document.querySelectorAll(cssSelector);
const array = [];
for (const item of results) {
array.push(item);
}
return array;
}),
argumentsLocalValues: [{type: 'string', value: locator.value}],
};
case 'xpath':
return String((xPathSelector: string) => {
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-xpath
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression(xPathSelector);
const xPathResult = expression.evaluate(
document,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
return {
functionDeclaration: String((xPathSelector: string) => {
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-xpath
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression(xPathSelector);
const xPathResult = expression.evaluate(
document,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);
const result = [];
for (let i = 0; i < xPathResult.snapshotLength; i++) {
result.push(xPathResult.snapshotItem(i));
}
return result;
}),
argumentsLocalValues: [{type: 'string', value: locator.value}],
};
case 'innerText':
// https://w3c.github.io/webdriver-bidi/#locate-nodes-using-inner-text
if (locator.value === '') {
throw new InvalidSelectorException(
'innerText locator cannot be empty'
);
const result = [];
for (let i = 0; i < xPathResult.snapshotLength; i++) {
result.push(xPathResult.snapshotItem(i));
}
return result;
});
default:
throw new UnsupportedOperationException(
`locateNodes does not support ${locator.type} locator type.`
);
}
return {
functionDeclaration: String(
(selector: string, fullMatch: boolean, ignoreCase: boolean) => {
const searchText = ignoreCase ? selector.toUpperCase() : selector;
const locateNodesUsingInnerText = (element: HTMLElement) => {
const returnedNodes: HTMLElement[] = [];
const nodeInnerText = ignoreCase
? element.innerText?.toUpperCase()
: element.innerText;
if (!nodeInnerText.includes(searchText)) {
return [];
}
const childNodes = [];
for (const child of element.children) {
if (child instanceof HTMLElement) {
childNodes.push(child);
}
}
if (childNodes.length === 0) {
if (fullMatch && nodeInnerText === searchText) {
returnedNodes.push(element);
} else {
if (!fullMatch) {
// Note: `nodeInnerText.includes(searchText)` is already checked
returnedNodes.push(element);
}
}
} else {
const childNodeMatches = childNodes
.map((child) => locateNodesUsingInnerText(child))
.flat(1);
if (childNodeMatches.length === 0) {
// Note: `nodeInnerText.includes(searchText)` is already checked
if (!fullMatch || nodeInnerText === searchText) {
returnedNodes.push(element);
}
} else {
returnedNodes.push(...childNodeMatches);
}
}
return returnedNodes;
};
// TODO: add maxDepth.
// TODO: provide proper start node.
// TODO: add maxNodeCount.
return locateNodesUsingInnerText(document.body);
}
),
argumentsLocalValues: [
{type: 'string', value: locator.value},
// `fullMatch` with default `true`.
{type: 'boolean', value: locator.matchType !== 'partial'},
// `ignoreCase` with default `false`.
{type: 'boolean', value: locator.ignoreCase === true},
],
};
}
}

async #locateNodesByLocator(
realm: Realm,
locator: BrowsingContext.Locator
): Promise<BrowsingContext.LocateNodesResult> {
const locatorFunction = this.#getLocatorFunction(locator);
const locatorDelegate = this.#getLocatorDelegate(locator);
const locatorResult = await realm.callFunction(
locatorFunction,
locatorDelegate.functionDeclaration,
{type: 'undefined'},
[{type: 'string', value: locator.value}],
locatorDelegate.argumentsLocalValues,
false,
Script.ResultOwnership.None,
{},
Expand Down
11 changes: 8 additions & 3 deletions tests/browsing_context/test_locate_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@


@pytest.mark.parametrize('locator', [
{
'type': 'innerText',
'value': 'foobarBARbaz'
},
{
'type': 'css',
'value': 'div'
Expand All @@ -29,11 +33,12 @@
},
])
@pytest.mark.asyncio
async def test_locate_nodes_locator(websocket, context_id, html, locator):
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">foobarBARbaz</div>'
'<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBAR<span>baz</span></div>'
))
resp = await execute_command(
websocket, {
Expand Down Expand Up @@ -67,7 +72,7 @@ async def test_locate_nodes_locator(websocket, context_id, html, locator):
'attributes': {
'data-class': 'two',
},
'childNodeCount': 1,
'childNodeCount': 2,
'localName': 'div',
'namespaceURI': 'http://www.w3.org/1999/xhtml',
'nodeType': 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[invalid.py]
[test_params_locator_value_invalid_value[innerText-\]]
expected: FAIL

[test_params_start_nodes_dom_node_not_element[document.querySelector('input#button').attributes[0\]\]]
expected: FAIL

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[locator.py]
[test_find_by_locator[innerText-foobarBARbaz\]]
expected: FAIL

[test_find_by_inner_text[ignore_case_true_full_match_no_max_depth\]]
expected: FAIL

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[invalid.py]
[test_params_locator_value_invalid_value[innerText-\]]
expected: FAIL

[test_params_start_nodes_dom_node_not_element[document.querySelector('input#button').attributes[0\]\]]
expected: FAIL

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[locator.py]
[test_find_by_locator[innerText-foobarBARbaz\]]
expected: FAIL

[test_find_by_inner_text[ignore_case_true_full_match_no_max_depth\]]
expected: FAIL

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[invalid.py]
[test_params_locator_value_invalid_value[innerText-\]]
expected: FAIL

[test_params_start_nodes_dom_node_not_element[document.querySelector('input#button').attributes[0\]\]]
expected: FAIL

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[locator.py]
[test_find_by_locator[innerText-foobarBARbaz\]]
expected: FAIL

[test_find_by_inner_text[ignore_case_true_full_match_no_max_depth\]]
expected: FAIL

Expand Down

0 comments on commit 8c41582

Please sign in to comment.