feat(text selector): pierce shadow roots (#1619)
This commit is contained in:
parent
75571e8eb8
commit
a9be3c5191
|
|
@ -76,6 +76,8 @@ Text engine finds an element that contains a text node with passed text. Example
|
|||
- Text body can be escaped with double quotes for precise matching, insisting on exact match, including specified whitespace and case. This means `text="Login "` will only match `<button>Login </button>` with exactly one space after "Login".
|
||||
- Text body can also be a JavaScript-like regex wrapped in `/` symbols. This means `text=/^\\s*Login$/i` will match `<button> loGIN</button>` with any number of spaces before "Login" and no spaces after.
|
||||
|
||||
> **NOTE** Text engine searches for elements inside open shadow roots, but not inside closed shadow roots or iframes.
|
||||
|
||||
> **NOTE** Malformed selector starting with `"` is automatically transformed to text selector. For example, Playwright converts `page.click('"Login"')` to `page.click('text="Login"')`.
|
||||
|
||||
### id, data-testid, data-test-id, data-test
|
||||
|
|
|
|||
|
|
@ -28,41 +28,19 @@ export const TextEngine: SelectorEngine = {
|
|||
continue;
|
||||
if (text.match(/^\s*[a-zA-Z0-9]+\s*$/) && TextEngine.query(root, text.trim()) === targetElement)
|
||||
return text.trim();
|
||||
if (TextEngine.query(root, JSON.stringify(text)) === targetElement)
|
||||
if (queryInternal(root, createMatcher(JSON.stringify(text))) === targetElement)
|
||||
return JSON.stringify(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
query(root: SelectorRoot, selector: string): Element | undefined {
|
||||
const document = root instanceof Document ? root : root.ownerDocument;
|
||||
if (!document)
|
||||
return;
|
||||
const matcher = createMatcher(selector);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
const element = node.parentElement;
|
||||
const text = node.nodeValue;
|
||||
if (element && text && matcher(text))
|
||||
return element;
|
||||
}
|
||||
return queryInternal(root, createMatcher(selector));
|
||||
},
|
||||
|
||||
queryAll(root: SelectorRoot, selector: string): Element[] {
|
||||
const result: Element[] = [];
|
||||
const document = root instanceof Document ? root : root.ownerDocument;
|
||||
if (!document)
|
||||
return result;
|
||||
const matcher = createMatcher(selector);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
const element = node.parentElement;
|
||||
const text = node.nodeValue;
|
||||
if (element && text && matcher(text))
|
||||
result.push(element);
|
||||
}
|
||||
queryAllInternal(root, createMatcher(selector), result);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
|
@ -81,3 +59,48 @@ function createMatcher(selector: string): Matcher {
|
|||
selector = selector.trim().toLowerCase();
|
||||
return text => text.toLowerCase().includes(selector);
|
||||
}
|
||||
|
||||
function queryInternal(root: SelectorRoot, matcher: Matcher): Element | undefined {
|
||||
const document = root instanceof Document ? root : root.ownerDocument!;
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
||||
const shadowRoots = [];
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
if (element.shadowRoot)
|
||||
shadowRoots.push(element.shadowRoot);
|
||||
} else {
|
||||
const element = node.parentElement;
|
||||
const text = node.nodeValue;
|
||||
if (element && element.nodeName !== 'SCRIPT' && element.nodeName !== 'STYLE' && text && matcher(text))
|
||||
return element;
|
||||
}
|
||||
}
|
||||
for (const shadowRoot of shadowRoots) {
|
||||
const element = queryInternal(shadowRoot, matcher);
|
||||
if (element)
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function queryAllInternal(root: SelectorRoot, matcher: Matcher, result: Element[]) {
|
||||
const document = root instanceof Document ? root : root.ownerDocument!;
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
||||
const shadowRoots = [];
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
if (element.shadowRoot)
|
||||
shadowRoots.push(element.shadowRoot);
|
||||
} else {
|
||||
const element = node.parentElement;
|
||||
const text = node.nodeValue;
|
||||
if (element && element.nodeName !== 'SCRIPT' && element.nodeName !== 'STYLE' && text && matcher(text))
|
||||
result.push(element);
|
||||
}
|
||||
}
|
||||
for (const shadowRoot of shadowRoots)
|
||||
queryAllInternal(shadowRoot, matcher, result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -540,6 +540,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
|
|||
expect(await page.$eval(`text=with`, e => e.outerHTML)).toBe('<div>textwithsubstring</div>');
|
||||
expect(await page.$(`text="with"`)).toBe(null);
|
||||
});
|
||||
|
||||
it('should work for open shadow roots', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
expect(await page.$eval(`text=root1`, e => e.outerHTML)).toBe('<span>Hello from root1</span>');
|
||||
expect(await page.$eval(`text=root2`, e => e.outerHTML)).toBe('<span>Hello from root2</span>');
|
||||
expect(await page.$eval(`text=root3`, e => e.outerHTML)).toBe('<span>Hello from root3</span>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors.register', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue