npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mariusandra/query-selector-shadow-dom

v0.7.2-posthog.2

Published

use querySelector syntax to search for nodes inside of (nested) shadow roots

Downloads

28

Readme

Build Status npm version codecov

query-selector-shadow-dom

querySelector that can pierce Shadow DOM roots without knowing the path through nested shadow roots. Useful for automated testing of Web Components e.g. with Selenium, Puppeteer.

ko-fi


// available as an ES6 module for importing in Browser environments

import { querySelectorAllDeep, querySelectorDeep } from 'query-selector-shadow-dom';

What is a nested shadow root?

Image of Shadow DOM elements in dev tools You can see that .dropdown-item:not([hidden]) (Open downloads folder) is several layers deep in shadow roots, most tools will make you do something like

document.querySelector("body > downloads-manager").shadowRoot.querySelector("#toolbar").shadowRoot.querySelector(".dropdown-item:not([hidden])")

EW!

with query-selector-shadow-dom:

import { querySelectorAllDeep, querySelectorDeep } from 'query-selector-shadow-dom';
querySelectorDeep(".dropdown-item:not([hidden])");

API

  • querySelectorAllDeep - mirrors querySelectorAll from the browser, will return an Array of elements matching the query
  • querySelectorDeep - mirrors querySelector from the browser, will return the first matching element of the query.

Both of the methods above accept a 2nd parameter, see section Provide alternative node. This will change the starting element to search from i.e. it will find ancestors of that node that match the query.

Plugins

WebdriverIO

This plugin implements a custom selector strategy: https://webdriver.io/docs/selectors.html#custom-selector-strategies


// make sure you have selenium standalone running
const { remote } = require('webdriverio');
const { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriverio');

(async () => {
    const browser = await remote({
        logLevel: 'error',
        path: '/wd/hub',
        capabilities: {
            browserName: 'chrome'
        }
    })

    // The magic - registry custom strategy
    browser.addLocatorStrategy('shadow', locatorStrategy);


    // now you have a `shadow` custom locator.

    // All elements on the page
    await browser.waitUntil(() => browser.custom$("shadow", ".btn-in-shadow-dom"));
    const elements = await browser.$$("*");

    const elementsShadow = await browser.custom$$("shadow", "*");

    console.log("All Elements on Page Excluding Shadow Dom", elements.length);
    console.log("All Elements on Page Including Shadow Dom", elementsShadow.length);


    await browser.url('http://127.0.0.1:5500/test/')
    // find input element in shadow dom
    const input = await browser.custom$('shadow', '#type-to-input');
    // type to input ! Does not work in firefox, see above.
    await input.setValue('Typed text to input');
    // Firefox workaround
    // await browser.execute((input, val) => input.value = val, input, 'Typed text to input')

    await browser.deleteSession()
})().catch((e) => console.error(e))

How is this different to shadow$

shadow$ only goes one level deep in a shadow root.

Take this example. Image of Shadow DOM elements in dev tools You can see that .dropdown-item:not([hidden]) (Open downloads folder) is several layers deep in shadow roots, but this library will find it, shadow$ would not. You would have to construct a path via css or javascript all the way through to find the right element.

const { remote } = require('webdriverio')
const { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriverio');

(async () => {
    const browser = await remote({capabilities: {browserName: 'chrome'}})

    browser.addLocatorStrategy('shadow', locatorStrategy);

    await browser.url('chrome://downloads')
    const moreActions = await browser.custom$('shadow', '#moreActions');
    await moreActions.click();
    const span = await browser.custom$('shadow', '.dropdown-item:not([hidden])');
    const text = await span.getText()
    // prints `Open downloads folder`
    console.log(text);

    await browser.deleteSession()
})().catch((e) => console.error(e))

Known issues

  • https://webdriver.io/blog/2019/02/22/shadow-dom-support.html#browser-support

  • From the above, firefox setValue does NOT currently work. . A workaround for now is to use a custom command (or method on your component object) that sets the input field's value via browser.execute(function).

  • Safari pretty much doesn't work, not really a surprise.

There are some webdriver examples available in the examples folder of this repository. WebdriverIO examples

Puppeteer

There are some puppeteer examples available in the examples folder of this repository.

Puppeteer examples

Playwright

Update: as of Playwright v0.14.0 their CSS and text selectors work with shadow Dom out of the box, you don't need this library anymore for Playwright.

Playwright works really nicely with this package.

This module exposes a playwright selectorEngine: https://github.com/microsoft/playwright/blob/master/docs/api.md#selectorsregisterenginefunction-args

const { selectorEngine } = require("query-selector-shadow-dom/plugins/playwright");
const playwright = require('playwright');

 await selectors.register('shadow', createTagNameEngine);
...
  await page.goto('chrome://downloads');
  // shadow= allows a css query selector that automatically pierces shadow roots.
  await page.waitForSelector('shadow=#no-downloads span', {timeout: 3000})

For a full example see: https://github.com/Georgegriff/query-selector-shadow-dom/blob/master/examples/playwright

Protractor

This project provides a Protractor plugin, which can be enabled in your protractor.conf.js file:

exports.config = {
    plugins: [{
        package: 'query-selector-shadow-dom/plugins/protractor'
    }],
    
    // ... other Protractor-specific config
};

The plugin registers a new locator - by.shadowDomCss(selector /* string */), which can be used in regular Protractor tests:

element(by.shadowDomCss('#item-in-shadow-dom'))

The locator also works with Serenity/JS tests that use Protractor under the hood:

import 'query-selector-shadow-dom/plugins/protractor';
import { Target } from '@serenity-js/protractor'
import { by } from 'protractor';

const ElementOfInterest = Target.the('element of interest')
    .located(by.shadowDomCss('#item-in-shadow-dom'))

See the end-to-end tests for more examples.

Examples

Provide alternative node

    // query from another node
    querySelectorShadowDom.querySelectorAllDeep('child', document.querySelector('#startNode'));
    // query an iframe
    querySelectorShadowDom.querySelectorAllDeep('child', iframe.contentDocument);

This library does not allow you to query across iframe boundaries, you will need to get a reference to the iframe you want to interact with. If your iframe is inside of a shadow root you could cuse querySelectorDeep to find the iframe, then pass the contentDocument into the 2nd argument of querySelectorDeep or querySelectorAllDeep.

Chrome downloads page

In the below examples the components being searched for are nested within web components shadowRoots.


// Download and Paste the lib code in dist into chrome://downloads console to try it out :)

console.log(querySelectorShadowDom.querySelectorAllDeep('downloads-item:nth-child(4) #remove'));
console.log(querySelectorShadowDom.querySelectorAllDeep('#downloads-list .is-active a[href^="https://"]'));
console.log(querySelectorShadowDom.querySelectorDeep('#downloads-list div#title-area + a'));

Shady DOM

If using the polyfills and shady DOM, this library will still work.

Importing

  • Shipped as an ES6 module to be included using a bundler of your choice (or not).
  • ES5 version bundled ontop the window as window.querySelectorShadowDom available for easy include into a test framework

Running the code locally

npm install

Running the tests

npm test

Running the tests in watch mode

npm run watch

Running the build

npm run build