diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts
index 71386683..70c0d908 100644
--- a/src/common/QueryHandler.ts
+++ b/src/common/QueryHandler.ts
@@ -112,7 +112,60 @@ const _defaultHandler = makeQueryHandler({
element.querySelectorAll(selector),
});
-const _builtInHandlers = new Map([['aria', ariaHandler]]);
+const pierceHandler = makeQueryHandler({
+ queryOne: (element, selector) => {
+ let found: Element | null = null;
+ const search = (root: Element | ShadowRoot) => {
+ const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
+ do {
+ const currentNode = iter.currentNode as HTMLElement;
+ if (currentNode.shadowRoot) {
+ search(currentNode.shadowRoot);
+ }
+ if (currentNode instanceof ShadowRoot) {
+ continue;
+ }
+ if (!found && currentNode.matches(selector)) {
+ found = currentNode;
+ }
+ } while (!found && iter.nextNode());
+ };
+ if (element instanceof Document) {
+ element = element.documentElement;
+ }
+ search(element);
+ return found;
+ },
+
+ queryAll: (element, selector) => {
+ const result: Element[] = [];
+ const collect = (root: Element | ShadowRoot) => {
+ const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
+ do {
+ const currentNode = iter.currentNode as HTMLElement;
+ if (currentNode.shadowRoot) {
+ collect(currentNode.shadowRoot);
+ }
+ if (currentNode instanceof ShadowRoot) {
+ continue;
+ }
+ if (currentNode.matches(selector)) {
+ result.push(currentNode);
+ }
+ } while (iter.nextNode());
+ };
+ if (element instanceof Document) {
+ element = element.documentElement;
+ }
+ collect(element);
+ return result;
+ },
+});
+
+const _builtInHandlers = new Map([
+ ['aria', ariaHandler],
+ ['pierce', pierceHandler],
+]);
const _queryHandlers = new Map(_builtInHandlers);
/**
diff --git a/test/queryselector.spec.ts b/test/queryselector.spec.ts
index 2b36c0cb..93a62a8f 100644
--- a/test/queryselector.spec.ts
+++ b/test/queryselector.spec.ts
@@ -68,6 +68,45 @@ describe('querySelector', function () {
});
});
+ describe('pierceHandler', function () {
+ beforeEach(async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ ``
+ );
+ });
+ it('should find first element in shadow', async () => {
+ const { page } = getTestState();
+ const div = await page.$('pierce/.foo');
+ const text = await div.evaluate(
+ (element: Element) => element.textContent
+ );
+ expect(text).toBe('Hello');
+ });
+ it('should find all elements in shadow', async () => {
+ const { page } = getTestState();
+ const divs = await page.$$('pierce/.foo');
+ const text = await Promise.all(
+ divs.map((div) =>
+ div.evaluate((element: Element) => element.textContent)
+ )
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ });
+
// The tests for $$eval are repeated later in this file in the test group 'QueryAll'.
// This is done to also test a query handler where QueryAll returns an Element[]
// as opposed to NodeListOf.