Skip to content

Commit

Permalink
feat: modify deepQuerySelectorAll behavior, add getAllElementsAndShad…
Browse files Browse the repository at this point in the history
…owRoots. (#36)

* feat: introduce getAllElementsAndShadowRoots, make deepQuerySelector behave closer to spec

* tests

* fix: deep query selectors actually work

* chore: prettier
  • Loading branch information
KonnorRogers committed Nov 11, 2022
1 parent 4e6f567 commit 697a0dc
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 41 deletions.
59 changes: 59 additions & 0 deletions __tests__/deep-query-selectors.test.tsx
@@ -0,0 +1,59 @@
import * as React from "react";
import { render } from "@testing-library/react";
import { Duplicates, TripleShadowRoots } from "../components";
import { deepQuerySelector, deepQuerySelectorAll } from "../src/index";

describe("deepQuerySelector()", () => {
test("Should find and return the first button.", () => {
const { container, baseElement } = render(<Duplicates />);

const btn = document
.querySelector("duplicate-buttons")
?.shadowRoot?.querySelector("button");
const secondButton = document
.querySelector("duplicate-buttons")
?.shadowRoot?.querySelectorAll("button")[1];

expect(deepQuerySelector(container, "button")).toBeInstanceOf(
HTMLButtonElement
);
expect(deepQuerySelector(container, "button")).toBe(btn);
expect(deepQuerySelector(container, "button")).not.toBe(secondButton);

expect(deepQuerySelector(baseElement, "button")).toBeInstanceOf(
HTMLButtonElement
);
expect(deepQuerySelector(baseElement, "button")).toBe(btn);
expect(deepQuerySelector(baseElement, "button")).not.toBe(secondButton);
});

test("Should find and return the 3rd level button element", () => {
const { container, baseElement } = render(<TripleShadowRoots />);
let el = deepQuerySelector(container, "button");
expect(el).toBeInstanceOf(HTMLButtonElement);
el = deepQuerySelector(baseElement, "button");
expect(el).toBeInstanceOf(HTMLButtonElement);
});

test("Should not find and 3rd level button element when shallow is true", () => {
const { container, baseElement } = render(<TripleShadowRoots />);
let el = deepQuerySelector(container, "button", { shallow: true });
expect(el).not.toBeInstanceOf(HTMLButtonElement);
el = deepQuerySelector(baseElement, "button", { shallow: true });
expect(el).not.toBeInstanceOf(HTMLButtonElement);
});
});

describe("deepQuerySelectorAll()", () => {
test("Should find and return both buttons", () => {
const { container, baseElement } = render(<Duplicates />);
let btns = deepQuerySelectorAll(container, "button");

expect(btns).toHaveLength(2);
btns.forEach((btn) => expect(btn).toBeInstanceOf(HTMLButtonElement));

btns = deepQuerySelectorAll(baseElement, "button");
expect(btns).toHaveLength(2);
btns.forEach((btn) => expect(btn).toBeInstanceOf(HTMLButtonElement));
});
});
3 changes: 2 additions & 1 deletion components.tsx
Expand Up @@ -131,7 +131,8 @@ class TripleShadowRootsElement extends HTMLElement {
if (this.isConnected === false) return ""

this.shadowRoot.innerHTML = `
<nested-shadow-roots></nested-shadow-roots>
<nested-shadow-roots>
</nested-shadow-roots>
`;
}
}
Expand Down
100 changes: 71 additions & 29 deletions src/deep-query-selectors.ts
Expand Up @@ -2,32 +2,70 @@ import { Container, ShadowOptions } from "./types";

export function deepQuerySelector(
container: Container,
selectors: string,
options: ShadowOptions = { shallow: false },
elements: (Element | ShadowRoot)[] = []
) {
const els = deepQuerySelectorAll(container, selectors, options, elements);
selector: string,
options: ShadowOptions = { shallow: false }
): Element | null {
const els = deepQuerySelectorAll(container, selector, options);

if (Array.isArray(els) && els.length > 0) {
return els[0];
return els[0] as Element | null;
}

return null;
}

export function deepQuerySelectorAll(
/**
* `deepQuerySelector` behaves like a normal querySelector except it will recurse into the container ShadowRoot
* and shadowRoot of children. It will not return shadow roots.
*
* @example
* // <my-element>
* // #shadowRoot <slot name="blah"></slot>
* // <div></div>
* // </my-element>
* deepQuerySelectorAll(myElement, "*") // => [slot, div]
* deepQuerySelectorAll(myElement, "slot[name='blah']") // => [slot]
*/
export function deepQuerySelectorAll<T extends HTMLElement>(
container: Container,
selector: string,
options: ShadowOptions = { shallow: false }
): T[] {
const elements = getAllElementsAndShadowRoots(container, options);

const queriedElements = elements
.map((el) => Array.from(el.querySelectorAll<T>(selector)))
.flat(Infinity) as T[];
return [...new Set(queriedElements)];
}

// This could probably get really slow and memory intensive in large DOMs,
// maybe an infinite generator in the future?
export function getAllElementsAndShadowRoots(
container: Container,
options: ShadowOptions = { shallow: false }
) {
const selector = "*";
return recurse(container, selector, options);
}

function recurse(
container: Container,
selectors: string,
selector: string,
options: ShadowOptions = { shallow: false },
elements: (Element | ShadowRoot)[] = []
elementsToProcess: (Element | ShadowRoot | Document)[] = [],
elements: (Element | ShadowRoot | Document)[] = []
) {
// if "document" is passed in, it will also pick up "<html>" causing the query to run twice.
if (container instanceof Document) {
container = document.documentElement;
}

// Make sure we're checking the container element!
elements.push(container);
// I haven't figured this one out, but for some reason when using the buildQueries
// from DOM-testing-library, not reassigning here causes an infinite loop.
// I've even tried calling "elementsToProcess.includes / .find" with no luck.
elementsToProcess = [container];
elements.push(container); // Make sure we're checking the container element!

// Accounts for if the container houses a textNode
if (
Expand All @@ -38,27 +76,31 @@ export function deepQuerySelectorAll(
elements.push(container.shadowRoot);
}

container.querySelectorAll(selectors).forEach((el: Element | HTMLElement) => {
if (el.shadowRoot == null || el.shadowRoot.mode === "closed") {
elements.push(el);
return;
}
elementsToProcess.forEach((containerElement) => {
containerElement
.querySelectorAll(selector)
.forEach((el: Element | HTMLElement) => {
if (el.shadowRoot == null || el.shadowRoot.mode === "closed") {
elements.push(el);
return;
}

// comment this to not add shadowRoots.
// This is here because queryByRole() requires the parent element which in some cases is the shadow root.
elements.push(el.shadowRoot);
// This is here because queryByRole() requires the parent element which in some cases is the shadow root.
elements.push(el.shadowRoot);

if (options.shallow === true) {
el.shadowRoot
.querySelectorAll(selectors)
.forEach((el) => elements.push(el));
return;
}
if (options.shallow === true) {
el.shadowRoot.querySelectorAll(selector).forEach((el) => {
elements.push(el);
});
return;
}

el.shadowRoot
.querySelectorAll(selectors)
.forEach((el) => elements.push(el));
deepQuerySelectorAll(el.shadowRoot, selectors, options, elements);
el.shadowRoot.querySelectorAll(selector).forEach((el) => {
elements.push(el);
elementsToProcess.push(el);
});
recurse(el.shadowRoot, selector, options, elementsToProcess, elements);
});
});

// We can sometimes hit duplicate nodes this way, lets stop that.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -24,6 +24,7 @@ export * from "./shadow-queries";
export {
deepQuerySelector,
deepQuerySelectorAll,
getAllElementsAndShadowRoots,
} from "./deep-query-selectors";

export {
Expand Down
20 changes: 9 additions & 11 deletions src/shadow-queries.ts
Expand Up @@ -12,7 +12,7 @@ import {
queryAllByTestId,
} from "@testing-library/dom";

import { deepQuerySelectorAll } from "./deep-query-selectors";
import { getAllElementsAndShadowRoots } from "./deep-query-selectors";
import {
ScreenShadowMatcherParams,
ScreenShadowRoleMatcherParams,
Expand All @@ -22,8 +22,6 @@ import {
ShadowSelectorMatcherParams,
} from "./types";

const scopeQuery = "*";

function toShadowQueries<T extends Function[]>(queries: T): T {
return queries.map((query): Function => {
return (...args: any[]): unknown => {
Expand All @@ -49,7 +47,7 @@ function queryAllByShadowRole<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByRole(el as HTMLElement, role, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -89,7 +87,7 @@ function queryAllByShadowLabelText<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByLabelText(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -129,7 +127,7 @@ function queryAllByShadowPlaceholderText<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByPlaceholderText(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -169,7 +167,7 @@ function queryAllByShadowText<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByText(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -209,7 +207,7 @@ function queryAllByShadowDisplayValue<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByDisplayValue(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -249,7 +247,7 @@ function queryAllByShadowAltText<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByAltText(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -289,7 +287,7 @@ function queryAllByShadowTitle<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByTitle(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down Expand Up @@ -329,7 +327,7 @@ function queryAllByShadowTestId<T extends HTMLElement = HTMLElement>(

return [
...new Set(
deepQuerySelectorAll(container, scopeQuery, options)
getAllElementsAndShadowRoots(container, options)
.map((el) => queryAllByTestId(el as HTMLElement, id, options))
.flat(Infinity)
),
Expand Down

0 comments on commit 697a0dc

Please sign in to comment.