An extension of DOM-testing-library to provide hooks into the shadow dom

Overview

Why?

Currently, DOM-testing-library does not support checking shadow roots for elements. This can be troublesome when you're looking for something with a "button" that's nested inside a shadowRoot.

testing-library/dom-testing-library#413

Prior art

https://github.com/Westbrook/dom-testing-library

testing-library__dom is a hard fork of DOM testing library which presents its own set of challenges. shadow-dom-testing-library looks to augment the existing functionality.

Preinstallation

Make sure you are using a library which supports rendering shadow roots. For Jest users, this means ensuring you have JSDOM >= 16.2 and Jest >= 26.2

Installation

npm install -D shadow-dom-testing-library

Example usage

// my-button.jsx
export default () => <sl-button>I get wrapped by a button in the shadowRoot!</sl-button>

// my-button.test.jsx
import { render } from "@testing-library/react"
import { screen } from "shadow-dom-testing-library"
import Button from "./my-button"

test("Find the button in the shadow root", async () => {
  render(<Button />)
  const btn = await screen.findByShadowRole("button")
  expect(btn).toBeInTheDocument()
})

API

All queries found here: https://testing-library.com/docs/queries/about/#priority are implemented with a "Shadow" prefix prior to the query type.

API examples

import { render } from "@testing-library/react"
import { getByShadowRole, findByShadowLabelText, queryAllByShadowTitle } from "shadow-dom-testing-library"

test("Find the button", () => {
  const { container } = render(<Button />)

  getByShadowRole(container, "button")
  await findByShadowLabelText(container, /Car Manufacturer/i)
  queryAllByShadowTitle(container, "delete")
})

Usage with screen

Shadow dom testing library ships its own "screen" that you're familiar with. It has all the <ByShadow> functions prebound to the document.

import { render } from "@testing-library/react"
import { screen } from "shadow-dom-testing-library"

test("Lets test some rendering", () => {
  render(<Button />
  screen.getByShadowRole("button")
  await screen.findByShadowLabelText(/Car Manufacturer/i)
  screen.queryAllByShadowTitle("delete")
})

In addition, every <ByShadow> function also accepts a "shallow" option. The shallow option means to only go 1 shadowRoot deep. Perhaps in the future a "recurseDepth" will be implemented to specify shadowRoot depth recursion.

import { render } from "@testing-library/react"
import { screen, getByShadowRole } from "shadow-dom-testing-library"

test("Lets test some rendering", () => {
  render(<Button />)
  getByShadowRole(document, "button", { shallow: true })
  await screen.findByShadowLabelText(/Car Manufacturer/i, { shallow: true })
  screen.queryAllByShadowTitle("delete", { shallow: true })
})

Additional APIs

Shadow DOM testing library also ships its own "deepQuerySelector" and "deepQuerySelectorAll" functions for if you need more fine-grained access to the DOM.

import { deepQuerySelector, deepQuerySelectorAll } from "shadow-dom-testing-library"

const elements = deepQuerySelectorAll(document, "my-button")
const element = deepQuerySelector(document, "my-button", { shallow: true })

Caution

Be careful with the shadowQueries and deepQueries. These functions recurse through every shadow root which can easily lead to unintended elements being found in your tests.

Also, this library is very new, use with caution. Feel free to report any issues.

Performance

Recursing through the Shadow DOM can be expensive if you render a large number of elements in an element. Benchmarks have not been measured, but it will easily be much worse than a regular querySelector call.

Additional notes

Shadow queries will work for both Light DOM and for Shadow DOM elements. For example you can search for a "button" in the Light DOM.

Example of light dom query

function SimpleButton () {
  const [count, setCount] = React.useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}


import { screen } from "shadow-dom-testing-library"

test("Regular buttons should also work with shadow query", async () => {
  render(<SimpleButton />)
  fireEvent.click(await screen.findByRole('button'))
  const el = await screen.findByText(/1/)
  expect(el).toBeInTheDocument()
})
Comments
  • Importing

    Importing "screen" giving me the error.

    Hello, I am using this library in my Ioinic Angular project.

    When I import like below, this library gives me the error while running the test:

    import { screen } from 'shadow-dom-testing-library';

    The error looks like following:

    ` Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
    
    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
    
    By default "node_modules" folder is ignored by transformers.
    
    ....................
    

    SyntaxError: Cannot use import statement outside a module

      14 | } from '@testing-library/dom';
      15 |
    > 16 | import { screen } from 'shadow-dom-testing-library';`
    

    I have following libraries:

    "@ionic/angular": "6.2.5", "@ionic/core": "6.2.5", "@angular/core": "~14.1.1", "shadow-dom-testing-library": "^1.1.4", "@testing-library/dom": "^8.17.1", [email protected], npm 6, node 12

    opened by pct-fahmed 7
  • Can't work with Storybook + Webpack 5 starting 1.13.0

    Can't work with Storybook + Webpack 5 starting 1.13.0

    Hi,

    Sorry in advance for the vague issue because I couldn't build a repro and the error logs are hard to decipher, but here it is:

    • I am using Node 18
    • Which forces me to use Webpack 5 in Storybook, Webpack 4 won't work
    • The new peer deps to "@testing-library/dom" and in particular "jsdom" raises issues related to some variables not being polyfilled

    Samples of the error log:

    ModuleNotFoundError: Module not found: Error: Can't resolve 'stream' in '/home/ubuntu/webapp/node_modules/jsdom/lib/jsdom/living/helpers'
    
    BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
    This is no longer the case. Verify if you need this module and configure a polyfill for it.
    
    
    ModuleNotFoundError: Module not found: Error: Can't resolve 'perf_hooks' in '/home/ubuntu/webapp/node_modules/jsdom/lib/jsdom'
        at /home/ubuntu/webapp/node_modules/webpack/lib/Compilation.js:2016:28
        at /home/ubuntu/webapp/node_modules/webpack/lib/NormalModuleFactory.js:798:13
        at eval (eval at create (/home/ubuntu/webapp/node_modules/webpack/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:10:1)
    

    Any idea where to start?

    opened by eric-burel 6
  • screen.debug() doesn't print nested shadow roots.

    screen.debug() doesn't print nested shadow roots.

    The new change for print shadow roots is awesome! Thanks for adding that. I noticed that it doesn't expand nested shadow roots though (so one web-component inside the shadow root of another web-component).

    In looking at the code, it seems to be because of this line: https://github.com/KonnorRogers/shadow-dom-testing-library/blob/main/src/prettyShadowDOM.ts#L34

    If node.shadowRoot !== null then it won't have any children. It seems like the line would need to be node.shadowRoot.children in that case. In my limited testing, that seems to fix the issue I described.

    opened by CreativeTechGuy 5
  • `deepQuerySelectorAll` doesn't recurse into root container's shadowRoot

    `deepQuerySelectorAll` doesn't recurse into root container's shadowRoot

    If the code hits these lines it has identified that the container is a shadow root, but it doesn't then look inside of that shadow root. This seems to only be an issue for the initial container element.

    This results in a wildly different output if elem vs elem.parentElement is passed. (Given that elem is a shadowRoot.) In this example, if elem is passed, the result will be [elem, elem.shadowRoot] which misses everything that is inside of that shadowRoot.

    Related #23

    opened by CreativeTechGuy 4
  • Is it possible to use this to query the shadow DOM for SVGs?

    Is it possible to use this to query the shadow DOM for SVGs?

    When using <use> in SVG, elements referenced by use are rendered in a shadow DOM. I've tried following the approach outlined here to query the shadow DOM via React Testing Library, where it waits for the existence of a host element's shadowRoot property, but in my testing, host.shadowRoot never appears.

    Is it possible to get to SVG use elements' shadow DOM descendants with this library?

    opened by jrnail23 4
  • Duplicate query results

    Duplicate query results

    Hi, first of all want to thank you for a great work, this library is something we've been looking for a long time. Now to the topic, it seems that shadow queries are returning duplicate nodes for the same result in some cases. Created a little sandbox to illustrate the issue. This example uses lightDOM, but I have the same problems with web components and shadowDOM.

    I saw that you're creating a Set from results in deepQuerySelectorAll() to remove the duplicates, doing the same solution and returning the Set from queryAllByShadow...() methods would solve the issue in question, I believe, unless I'm missing something.

    Hopefully that's enough of information, if not, I'll be happy to provide with more :) thanks again.

    opened by AugV 3
  • `deepQuerySelector` and `deepQuerySelectorAll` are not exported

    `deepQuerySelector` and `deepQuerySelectorAll` are not exported

    The README lists additional APIs deepQuerySelector and deepQuerySelectorAll here but those don't seem to be exported.

    This would be very useful as an escape hatch when we need a raw querySelector. Can this be exposed?

    opened by CreativeTechGuy 2
  • GitHub not linked on npm package page

    GitHub not linked on npm package page

    The GitHub repo for this package is not listed on its npm page:

    https://www.npmjs.com/package/shadow-dom-testing-library

    I had to dig a bit to find this 😄

    opened by TranquilMarmot 2
  • `screen.debug()` deletes elements from output

    `screen.debug()` deletes elements from output

    Take the following example which is very similar to how toJSDOM is implemented in this library:

    const htmlString = "<tr><td>Cell 1 <a>Link in Cell 1</a></td><td>Cell 2</td>";
    const dom = new DOMParser().parseFromString(htmlString, "text/html");
    console.log(dom.body.firstElementChild.outerHTML);
    

    This will return just "<a>Link in Cell 1</a>".

    The reason being, <tr> and <td> aren't valid tags in HTML if it isn't inside of a <table>. So when parsing, it seems to throw those away since they aren't correct. (As the very forgiving HTML parser likes to do.) As a result, the dom.body looks like: <body>Cell 1 <a>Link in Cell 1</a>Cell 2</body> as it has stripped all of those tags. Now since the next line is expecting just one root node, when it picks the first element, it ends up picking the a since that is the only element which remains.

    In my testing, it seems like using the XML parser doesn't have this issue, but I'm not sure what other issues/differences the XML parser might introduce.

    opened by CreativeTechGuy 1
  • TypeScript - deepQuerySelector returns Element instead of HTMLElement

    TypeScript - deepQuerySelector returns Element instead of HTMLElement

    opened by CreativeTechGuy 1
  • `screen.debug()` modifies DOM (Schrödinger's DOM)

    `screen.debug()` modifies DOM (Schrödinger's DOM)

    I was experiencing a strange issue where if I had screen.debug() in my test, the following steps would behave differently. After some digging, I realized it was because the debug method actually modified the DOM. It appears to be from these lines where node is a reference to the actual DOM and an element is inserted.

    Can the node be copied first so that the actual DOM isn't changed as a result of debugging?

    opened by CreativeTechGuy 1
  • `deepQuerySelectorAll` fails to recurse into descendant shadow roots

    `deepQuerySelectorAll` fails to recurse into descendant shadow roots

    Walkthrough of what I believe is happening:

    • Call deepQuerySelectorAll passing in an element with a .shadowRoot property. (This also applies to within() queries for the same reason.)
    • This library will add just the container to the list of elements to process. Since the container is a shadow root element, this querySelectorAll will return nothing.
    • Since that querySelectorAll returned nothing, the recurse method will exit with a return value of [elem, elem.shadowRoot].
    • Then deepQuerySelectorAll will do a querySelectorAll for each of these values. But at this point, it's just a normal query selector so nested shadow roots are ignored.

    As a result, passing in an element with a shadow root to deepQuerySelectorAll, or more notably within() will result in missing any nested shadow roots.

    opened by CreativeTechGuy 5
  • Feature: aggregate slot content for

    Feature: aggregate slot content for "within" queries.

    <my-wrapper>
      <div slot="start"><button></button></div>
    </my-wrapper>
    
    within(wrapper.startSlot).getByShadowRole("button")
    

    ^ should return button, but due to how projection of slots work, it doesn't.

    this is a simple example but more complex reasoning may involve finding a specific row of a table which may have many slots.

    opened by KonnorRogers 0
Owner
Konnor Rogers
Full stack web developer https://blog.konnor.site https://konnor.site
Konnor Rogers
A testing focused Remix Stack, that integrates E2E & Unit testing with Playwright, Vitest, MSW and Testing Library. Driven by Prisma ORM. Deploys to Fly.io

Live Demo · Twitter A testing focused Remix Stack, that integrates E2E & Unit testing with Playwright, Vitest, MSW and Testing Library. Driven by Pris

Remix Stacks 18 Oct 31, 2022
Long shadow jQuery plugin

Long Shadow jQuery Plugin Easy text-shadow with long shadow jquery plugin UNPKG <script src="https://unpkg.com/jquery-longshadow"></script> Use $(sele

Dang Van Thanh 15 Jul 20, 2022
Atomico a micro-library for creating webcomponents using only functions, hooks and virtual-dom.

Atomico simplifies learning, workflow and maintenance when creating webcomponents. Scalable and reusable interfaces: with Atomico the code is simpler

Atomico 898 Dec 31, 2022
wagmi hooks đŸ€ Storybook interaction testing

A quick demonstration of how Storybook decorators can be combined with a mocked wagmi client to facilitate automated interaction testing for web3-enab

Mike Ryan 21 Dec 13, 2022
Shield is a development framework for circom developers. The core reason is to provide libraries, plugins, and testing tools to ensure code quality and security.

SHIELD Shield is a development framework for circom developers but we plan it to other languages such as CAIRO, SNARKYJS etc. The core reason is to pr

Xord 41 Dec 22, 2022
Javascript-testing-practical-approach-2021-course-v3 - Javascript Testing, a Practical Approach (v3)

Javascript Testing, a Practical Approach Description This is the reference repository with all the contents and the examples of the "Javascript Testin

Stefano Magni 2 Nov 14, 2022
AREX: It is a “Differential Testing” and “Record and Replay Testing” Tool.

AREX: It is a “Differential Testing” and “Record and Replay Testing” Tool. Test restful API by record, replay and stub request/response. Differential

ArexTest 15 Nov 1, 2022
A lightweight extension to automatically detect and provide verbose warnings for embedded iframe elements in order to protect against Browser-In-The-Browser (BITB) attacks.

Enhanced iFrame Protection - Browser Extension Enhanced iFrame Protection (EIP) is a lightweight extension to automatically detect and provide verbose

odacavo 16 Dec 24, 2022
Chrome Extension To Reveal Observable Notebooks As Quarto QMD {ojs} Blocks & provide downloads of FileAttachments and zipped Quarto project

reveal-qmd Chrome Extension To Reveal Observable Notebooks As Quarto QMD {ojs} Blocks Usage: git clone [email protected]:hrbrmstr/reveal-qmd In Chrome (e

boB Rudis 20 Nov 29, 2022
curtains.js is a lightweight vanilla WebGL javascript library that turns HTML DOM elements into interactive textured planes.

What is it ? Shaders are the new front-end web developpment big thing, with the ability to create very powerful 3D interactions and animations. A lot

Martin Laxenaire 1.4k Jan 1, 2023
Minimal Typescript / NextJS dApp template bootstrapped with wagmi Ethereum react hooks library.

Welcome to the NextJS wagmi starter template ?? Looking to get up and running with a Typescript / NextJS dApp as quickly as possible? You're in the ri

Seth 78 Jan 4, 2023
Tiny API that provide product/library name for a URL

JSer.info Product Name API Tiny API that provide product/library name for a URL. Usage Supported All products. curl https://jser-product-name.deno.dev

JSer.info 6 Oct 21, 2022
WPPConnect/WA-JS API SERVER is a small api server to provide url preview for @wppconnect/wa-js library

WPPConnect/WA-JS API SERVER WPPConnect/WA-JS API SERVER is a small api server to provide url preview for @wppconnect/wa-js library Our online channels

null 13 Aug 11, 2022
Node.js library that provide a cache for file metadata or file content.

@file-cache A cache library for file metadata or file content. It is useful for process that work a given series of files and that only need to repeat

azu 16 Aug 6, 2022
An easy-to-use library that provide acronymous sending on form changes

Form Async Form Async is an easy-to-use library that provide acronymous sending on form changes. It's a great solution to preventing data loss when fi

Marc-Antoine Loignon 3 Jan 5, 2022
Custom Vitest matchers to test the state of the DOM, forked from jest-dom.

vitest-dom Custom Vitest matchers to test the state of the DOM This library is a fork of @testing-library/jest-dom. It shares that library's implement

Chance Strickland 14 Dec 16, 2022
Tesodev-search-app - Personal Search App with React-Hooks

Tesodev-search-app Personal Search App with React-Hooks View on Heroku : [https://tesodev-staff-search-app.herokuapp.com/] Instructions Clone this rep

Rahmi Köse 1 Nov 10, 2022
Finance-Tracker - A basic finance-tracker application built using Next.js, React Hooks and Context API

Next.js + Tailwind CSS Example This example shows how to use Tailwind CSS (v3.0) with Next.js. It follows the steps outlined in the official Tailwind

Osemwengie Benjamin 1 Jan 2, 2022
Solid hooks for Firebase v9.

solid-firebase Solid hooks for Firebase. Quick start Install it: yarn add firebase solid-firebase Configure firebase app: import { render } from 'soli

Robert Soriano 40 Dec 23, 2022