• Stars
    star
    218
  • Rank 175,316 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 6 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

querySelector that can pierce Shadow DOM roots without knowing the path through nested shadow roots. Useful for automated testing of Web Components. Production use is not advised, this is for test environments/tools such as Web Driver, Playwright, Puppeteer

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.

// 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.
  • collectAllElementsDeep - collects all elements on the page, including shadow dom

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.

Known limitations

  • Source ordering of results may not be preserved. Due to the nature of how this library works, by breaking down selectors into parts, when using multiple selectors (e.g. split by commas) the results will be based on the order of the query, not the order the result appear in the dom. This is different from the native querySelectorAll functionality. You can read more about this here: #54

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

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

Puppeteer

Update: As of 5.4.0 Puppeteer now has a built in shadow Dom selector, this module might not be required for Puppeteer anymore. They don't have any documentation: puppeteer/puppeteer#6509

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/main/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/main/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

More Repositories

1

webdriverio

Next-gen browser and mobile automation test framework for Node.js
TypeScript
8,718
star
2

selenium-standalone

A Node.js based package and CLI library for launching Selenium with WebDrivers support (Chrome, Firefox, IE, Edge)
JavaScript
901
star
3

cucumber-boilerplate

Boilerplate project to run WebdriverIO tests with Cucumber
JavaScript
520
star
4

appium-boilerplate

Boilerplate project to run WebdriverIO tests with Appium to test native applications on iOS and Android
TypeScript
410
star
5

visual-testing

Image comparison / visual regression testing for WebdriverIO
TypeScript
107
star
6

native-demo-app

Guinea pig app for showcasing test automation with WebdriverIO and Appium
TypeScript
81
star
7

expect-webdriverio

WebdriverIO Assertion Library
TypeScript
70
star
8

jasmine-boilerplate

Boilerplate project to run WebdriverIO tests with Jasmine
TypeScript
44
star
9

awesome-webdriverio

A curated list of awesome WebdriverIO resources, libraries and tools
39
star
10

workshop

A hands on course to learn WebdriverIO from scratch
JavaScript
37
star
11

chrome-recorder

Generate WebdriverIO Tests from Google Chrome DevTools Recordings.
TypeScript
26
star
12

codemod

A codemod to transform Protractor into WebdriverIO tests
JavaScript
24
star
13

create-wdio

One-line installer for WebdriverIO
TypeScript
21
star
14

wdio-wait-for

a lightweight library of useful expected conditions for the WebdriverIO framework.
TypeScript
18
star
15

example-recipes

A set of common automation recipes using WebdriverIO
JavaScript
18
star
16

webdriverio-schematics

Add WebdriverIO to an Angular CLI project
TypeScript
18
star
17

component-testing-examples

This repository contains several examples testing web components using WebdriverIO
TypeScript
12
star
18

recorder-extension

WebdriverIO Extension for Devtools that allows you to export tests directly from the Recorder panel.
JavaScript
9
star
19

bx

Command line tool to run JavaScript, TypeScript or HTML files in the browser.
TypeScript
7
star
20

electron-boilerplate

Boilerplate project to demonstrate testing ElectronJS Applications with WebdriverIO
TypeScript
7
star
21

guinea-pig

Guinea pig website for internal e2e testing purposes
CSS
4
star
22

.github

Default Community health files for WebdriverIO projects
2
star
23

expense-action

GitHub Action that enables WebdriverIO's expense workflow.
TypeScript
1
star