feat(text selector): pierce shadow roots (#1619)

This commit is contained in:
Dmitry Gozman 2020-04-02 18:03:30 -07:00 committed by GitHub
parent 75571e8eb8
commit a9be3c5191
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 25 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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', () => {