main 5f5f446d8392 cached
49 files
73.6 KB
18.0k tokens
35 symbols
1 requests
Download .txt
Repository: Georgegriff/query-selector-shadow-dom
Branch: main
Commit: 5f5f446d8392
Files: 49
Total size: 73.6 KB

Directory structure:
gitextract_u8vckhgp/

├── .babelrc
├── .eslintrc.js
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── LICENSE
├── README.md
├── codecept.conf.js
├── demo.js
├── examples/
│   ├── playwright/
│   │   └── custom-engine.js
│   ├── puppeteer/
│   │   ├── clicking-elements.js
│   │   ├── custom-engine.js
│   │   ├── multiple-elements.js
│   │   └── typing-to-elements.js
│   └── webdriverio/
│       ├── deeply-nested.js
│       ├── multiple-elements.js
│       └── typing-to-elements.js
├── jsconfig.json
├── karma.common.js
├── karma.conf.js
├── package.json
├── plugins/
│   ├── codeceptjs/
│   │   ├── README.md
│   │   └── index.js
│   ├── playwright/
│   │   └── index.js
│   ├── protractor/
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── puppeteer/
│   │   └── index.js
│   └── webdriverio/
│       └── index.js
├── protractor.conf.js
├── puppeteer-es5.js
├── rollup.config.js
├── src/
│   ├── normalize.js
│   └── querySelectorDeep.js
├── steps.d.ts
├── steps_file.js
└── test/
    ├── TestComponent.js
    ├── basic.spec.js
    ├── codeceptjs/
    │   ├── README.md
    │   ├── codecept.conf.js
    │   ├── components.test.js
    │   ├── jsconfig.json
    │   ├── steps.d.ts
    │   └── steps_file.js
    ├── createTestComponent.js
    ├── index.html
    ├── nopolyfills.spec.js
    └── protractor-locator.e2e.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
    "presets": [
      ["@babel/env", {
        "targets": {
          "browsers": ["last 2 versions", "IE >= 11"]
        }
      }]
    ],
    "comments" : false
  }

================================================
FILE: .eslintrc.js
================================================
module.exports = {
    "env": {
        "browser": true,
        "es2021": true,
        "node": true,
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 12,
        "sourceType": "module"
    },
    "globals": {
        "it": true,
        "describe": true,
        "expect": true,
        "beforeEach": true,
        "afterEach": true,
        "jasmine": true
    },
    "rules": { "semi": ["error", "always"]}
};


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
ko_fi: griffadev
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/workflows/ci.yml
================================================
---
name: CI

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: ['14', '16']
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js v${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - name: Install dependencies
        run: |
          npm ci
      - name: Build
        run: npm run build --if-present
      - name: Run Tests
        run: |
          npm run test:ci
          npm run e2e:protractor
        env:
          CHROMEDRIVER_FILEPATH: /usr/local/share/chromedriver-linux64/chromedriver

    timeout-minutes: 10


================================================
FILE: .gitignore
================================================
# IDEs
.vscode
.idea

# Build artifacts
node_modules
dist
coverage
.DS_Store
**/output/**


================================================
FILE: .npmignore
================================================


================================================
FILE: .nvmrc
================================================
v14.21.3


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 George Griffiths

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[![Build Status](https://travis-ci.org/Georgegriff/query-selector-shadow-dom.svg?branch=main)](https://travis-ci.org/Georgegriff/query-selector-shadow-dom) [![npm version](https://badge.fury.io/js/query-selector-shadow-dom.svg)](https://badge.fury.io/js/query-selector-shadow-dom)

# 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.

```javascript
// 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](./Chrome-example.png)
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

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

EW!

with query-selector-shadow-dom:

```javascript
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: https://github.com/Georgegriff/query-selector-shadow-dom/issues/54

## Plugins

### WebdriverIO

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

```javascript
// 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](./Chrome-example.png)
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.

```javascript
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](https://github.com/Georgegriff/query-selector-shadow-dom/blob/main/examples/webdriverio)

### 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: https://github.com/puppeteer/puppeteer/pull/6509

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

[Puppeteer examples](https://github.com/Georgegriff/query-selector-shadow-dom/blob/main/examples/puppeteer)

### 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

```javascript
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`](https://www.protractortest.org/#/api-overview) file:

```javascript
exports.config = {
  plugins: [
    {
      package: "query-selector-shadow-dom/plugins/protractor",
    },
  ],

  // ... other Protractor-specific config
};
```

The plugin registers a new [locator](https://www.protractortest.org/#/api?view=ProtractorBy) - `by.shadowDomCss(selector /* string */)`, which can be used in regular Protractor tests:

```javascript
element(by.shadowDomCss("#item-in-shadow-dom"));
```

The locator also works with [Serenity/JS](https://serenity-js.org) tests that [use Protractor](https://serenity-js.org/modules/protractor) under the hood:

```typescript
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](https://github.com/Georgegriff/query-selector-shadow-dom/blob/features/protractor-locator/test/protractor-locator.e2e.js) for more examples.

## Examples

### Provide alternative node

```javascript
// 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. </br>
If your iframe is inside of a shadow root you could use `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`.

```javascript
// 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 on top 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`


================================================
FILE: codecept.conf.js
================================================
const { setHeadlessWhen } = require('@codeceptjs/configure');

// turn on headless mode when running with HEADLESS=true environment variable
// HEADLESS=true npx codecept run
setHeadlessWhen(process.env.HEADLESS);

exports.config = {
  tests: 'test/codeceptjs/*.test.js',
  output: './output',
  helpers: {
    Playwright: {
      url: 'http://localhost',
      show: true,
      browser: 'chromium'
    }
  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'query-selector-shadow-dom',
  plugins: {
    retryFailedStep: {
      enabled: true
    },
    screenshotOnFail: {
      enabled: true
    }
  }
}

================================================
FILE: demo.js
================================================
// Paste this code into chrome://downloads console to try it out :)

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

================================================
FILE: examples/playwright/custom-engine.js
================================================
const { selectorEngine } = require("query-selector-shadow-dom/plugins/playwright");
const playwright = require('playwright')

const main = async () => {
  await playwright.selectors.register('shadow', selectorEngine)

  const browser = await playwright.chromium.launch({ headless: false})
  const context = await browser.newContext({ viewport: null })
  const page = await context.newPage()

  await page.goto('chrome://downloads')

  await page.waitForSelector('shadow=#no-downloads span', {timeout: 3000})
  await new Promise(resolve => setTimeout(resolve, 3000))   

  await page.close()
  await context.close()
  await browser.close()
}

main()

================================================
FILE: examples/puppeteer/clicking-elements.js
================================================
const puppeteer = require('puppeteer');
const {  QueryHandler } = require("query-selector-shadow-dom/plugins/puppeteer");
(async () => {
    try {
        await puppeteer.registerCustomQueryHandler('shadow', QueryHandler);
        const browser = await puppeteer.launch({
            headless: false,
            devtools: true
        })
        const page = await browser.newPage()
        await page.goto('http://127.0.0.1:5500/test/')

        // ensure btn exists and return it
        await page.waitForSelector("shadow/.btn-in-shadow-dom");
        const btn = await page.$("shadow/.btn-in-shadow-dom");
        await btn.click();
        // check btn was clicked (this page expected btn to change text of output)
        const outputSpan = await page.$("shadow/.output");
        const text = await page.evaluate((output) => output.innerText, outputSpan);
        // prints the text from the output
        console.log(text);

        await browser.close()
    } catch (e) {
        console.error(e);
    }

})()


================================================
FILE: examples/puppeteer/custom-engine.js
================================================
const {  QueryHandler } = require("query-selector-shadow-dom/plugins/puppeteer");
const puppeteer = require('puppeteer');

const main = async () => {
  await puppeteer.registerCustomQueryHandler('shadow', QueryHandler);

  const browser = await puppeteer.chromium.launch({ headless: false});
  const context = await browser.newContext({ viewport: null });
  const page = await context.newPage();

  await page.goto('chrome://downloads');

  const element = await page.waitForSelector('shadow/div', {timeout: 3000});
  const span = await element.$$("shadow/div > .illustration + span");
  console.log(span);
  await new Promise(resolve => setTimeout(resolve, 3000));

  await page.close()
  await context.close()
  await browser.close()
}

main()


================================================
FILE: examples/puppeteer/multiple-elements.js
================================================
const puppeteer = require('puppeteer');
const {  QueryHandler } = require("query-selector-shadow-dom/plugins/puppeteer");
(async () => {
    try {
        await puppeteer.registerCustomQueryHandler('shadow', QueryHandler);
        const browser = await puppeteer.launch({
            headless: false
        })
        const page = await browser.newPage()
        await page.goto('http://127.0.0.1:5500/test/')
        // wait for a web component to appear
        await page.waitForSelector("shadow/.btn-in-shadow-dom")
        const elements = await page.$$("*");
        const elementsShadow = await page.$$("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.close()

    } catch (e) {
        console.error(e);
    }
})()


================================================
FILE: examples/puppeteer/typing-to-elements.js
================================================
const puppeteer = require('puppeteer');
const {  QueryHandler } = require("query-selector-shadow-dom/plugins/puppeteer");
(async () => {
    try {
        await puppeteer.registerCustomQueryHandler('shadow', QueryHandler);
        const browser = await puppeteer.launch({
            headless: false
        })
        const page = await browser.newPage()
        await page.goto('http://127.0.0.1:5500/test/')

        const inputElement = await page.waitForSelector("shadow/#type-to-input");

        await inputElement.type("Typed text to input");

        const value = await page.evaluate(inputElement => inputElement.value, inputElement);
        console.log("Value", value);

        await browser.close()

    } catch (e) {
        console.error(e);
    }

})()


================================================
FILE: examples/webdriverio/deeply-nested.js
================================================
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'
        }
    })

    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))

================================================
FILE: examples/webdriverio/multiple-elements.js
================================================
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: 'firefox'
        }
    })

    browser.addLocatorStrategy('shadow', locatorStrategy);

    
    await browser.url('http://127.0.0.1:5500/test/')
    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.deleteSession()
})().catch((e) => console.error(e))

================================================
FILE: examples/webdriverio/typing-to-elements.js
================================================
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: 'firefox'
        }
    })

    browser.addLocatorStrategy('shadow', locatorStrategy);

    
    await browser.url('http://127.0.0.1:5500/test/')
    const input = await browser.custom$('shadow', '#type-to-input');
    await input.setValue('Typed text to input');
    // Firefox workaround
   // await browser.execute((input, val) => input.value = val, input, 'Typed text to input')
    console.log(await input.getValue())

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

================================================
FILE: jsconfig.json
================================================
{
  "compilerOptions": {
    "allowJs": true
  }
}

================================================
FILE: karma.common.js
================================================
const babel = require('rollup-plugin-babel');
const babelrc = require('babelrc-rollup').default;
const istanbul = require('rollup-plugin-istanbul');

const babelConfig = {
    'presets': [
        ['@babel/preset-env', {
            'targets': {
                'browsers': ['ff >= 60']
            },
            'loose': true
        }]
    ]
};

module.exports = function(overrides, config) {
    return Object.assign({
        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',


        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ['jasmine'],


        // list of files / patterns to load in the browser
        files: [
            { pattern: 'node_modules/@webcomponents/webcomponentsjs/bundles/**.js', served: true, included: true },
            { pattern: 'node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js', served: true, included: true },
            'test/**/*.spec.js'
        ],


        // list of files / patterns to exclude
        exclude: [],


        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {
            'src/**/*.js': ['rollup', 'coverage'],
            'test/**/*.js': ['rollup']
        },

        rollupPreprocessor: {
            /**
             * This is just a normal Rollup config object,
             * except that `input` is handled for you.
             */
            plugins: [
                istanbul({
                    exclude: ['test/**/*.js']
                }),
                babel(babelrc({
                    addExternalHelpersPlugin: false,
                    config: babelConfig,
                    exclude: 'node_modules/**'
                }))

            ],
            output: {
                format: 'iife', // Helps prevent naming collisions.
                name: 'querySelectorShadowDom', // Required for 'iife' format.
                sourcemap: 'inline' // Sensible for testing.
            }
        },

        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ['spec', 'coverage'],
        specReporter: {
            maxLogLines: 5, // limit number of lines logged per test
            suppressErrorSummary: true, // do not print error summary
            suppressFailed: false, // do not print information about failed tests
            suppressPassed: false, // do not print information about passed tests
            suppressSkipped: true, // do not print information about skipped tests
            showSpecTiming: true, // print the time elapsed for each spec
            failFast: true // test would finish with error when a first fail occurs. 
        },
        coverageReporter: {
            type: 'lcov', // lcov or lcovonly are required for generating lcov.info files
            dir: 'coverage/'
        },
        // web server port
        port: 9876,


        // enable / disable colors in the output (reporters and logs)
        colors: true,


        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,


        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,


        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: true,

        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
    }, overrides);
};

================================================
FILE: karma.conf.js
================================================
const KarmaConfig = require('./karma.common.js');
module.exports = function(config) {

    config.set(KarmaConfig({
        browsers: ['ChromeHeadless', 'Firefox']
    }, config));
}

================================================
FILE: package.json
================================================
{
  "name": "query-selector-shadow-dom",
  "version": "1.0.1",
  "description": "use querySelector syntax to search for nodes inside of (nested) shadow roots",
  "main": "src/querySelectorDeep.js",
  "scripts": {
    "prepublish": "npm run build",
    "prebuild": "npm run lint",
    "lint": "eslint src/**/*.js",
    "build": "rollup -c",
    "test": "karma start",
    "test:ci": "karma start --browsers ChromeHeadless,FirefoxHeadless",
    "e2e:protractor": "protractor protractor.conf.js",
    "watch": "npm-watch",
    "selenium": "./node_modules/.bin/selenium-standalone install && ./node_modules/.bin/selenium-standalone start"
  },
  "watch": {
    "test": "{src,test}/*.js"
  },
  "files": [
    "Chrome-example.png",
    "/src/",
    "dist/querySelectorShadowDom.js",
    "/plugins/"
  ],
  "author": "George Griffiths <GeorgeGriff>",
  "keywords": [
    "webcomponents",
    "puppeteer",
    "playwright",
    "automation",
    "queryselector",
    "shadowdom",
    "web-components",
    "testing",
    "webdriver",
    "protractor",
    "selenium",
    "webdriverio",
    "codeceptjs"
  ],
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/preset-env": "^7.1.0",
    "@wdio/selenium-standalone-service": "^6.4.0",
    "@webcomponents/webcomponentsjs": "^2.0.2",
    "babelrc-rollup": "^3.0.0",
    "eslint": "^7.19.0",
    "jasmine": "^3.1.0",
    "karma": "^6.2.0",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^2.0.3",
    "karma-firefox-launcher": "^1.1.0",
    "karma-jasmine": "^1.1.2",
    "karma-rollup-preprocessor": "^7.0.6",
    "karma-spec-reporter": "0.0.32",
    "npm-watch": "^0.7.0",
    "protractor": "^7.0.0",
    "puppeteer": "^5.2.0",
    "rollup": "^2.41.2",
    "rollup-plugin-babel": "^4.0.3",
    "rollup-plugin-istanbul": "^2.0.1",
    "rollup-plugin-sourcemaps": "^0.4.2",
    "rollup-plugin-terser": "^7.0.2",
    "selenium-standalone": "^6.19.0",
    "webdriverio": "^6.4.5"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/webdriverio/query-selector-shadow-dom"
  }
}


================================================
FILE: plugins/codeceptjs/README.md
================================================
# Support for Shadow DOM in CodeceptJS

- Supported CodeceptJS Helpers: *Playwright* only.
- CodeceptJS 2.6.0 released webdriver.io support as an alternative if you don't want to use Playwright

Support for this plugin is currently limited to Playwright, this is mostly due to the fact that playwright
allows for the addition of `custom selector engines`.

Goal/Example: To be able to write a test that works easily with shadow dom web components.
See Issues for what currently works and what doesn't

```javascript
Feature("The chrome downloads page");
Scenario("Can interact with the search box", async I => {
  I.amOnPage("chrome://downloads");
  I.see("Files you download appear here", {shadow: "#no-downloads span"});
  I.waitForVisible({shadow: "#no-downloads"}, 5);
  I.click({shadow: `[title="Search downloads"]`});
  I.waitForVisible({shadow: '#searchInput'}, 5);
  I.fillField({shadow: '#searchInput'}, "A download")
  I.waitForValue({shadow: '#searchInput'}, "A download", 5)
  I.waitForText("No search results found", 3, {shadow: "#no-downloads span"});
  I.clearField({shadow: '#searchInput'})
  I.waitForValue({shadow: '#searchInput'}, "", 5)
});

```

Setup:

1. `npm install query-selector-shadow-dom codeceptjs playwright`
2. Setup a codeceptjs project: https://codecept.io/quickstart/
3. In `codecept.config.js` add this shadow dom plugin

```javascript
 plugins: {
    shadowDom: {
      enabled: true,
      locator: "shadow",
      require: "query-selector-shadow-dom/plugins/codeceptjs"
    }
  }
```
4. Start using the custom locator `{shadow: "..."}` You may rename the locator in the config file from "shadow" to something else.
5. Read issues below as not everything currently works.

Issues:

## What works
- Most of the APIs listed here should work with shadow dom https://codecept.io/helpers/Playwright/#playwright

### The following methods are not supported as of right now:
- waitForNoVisibleElements (looking for help should be do-able, feel free to PR to CodeceptJS)


================================================
FILE: plugins/codeceptjs/index.js
================================================
const { selectorEngine } = require("../playwright");
const supportedHelpers = [
    'Playwright'
]
const playwright = require('playwright');

module.exports = function(config) {
    const container = codeceptjs.container;
    const event = codeceptjs.event;
    const helpers = container.helpers()
    let helperName
    for (helperName of supportedHelpers) {
        if (Object.keys(helpers).indexOf(helperName) > -1) {
          helper = helpers[helperName];
        }
    }
    if (!helper) {
        throw new Error(`Shadow dom plugin only supports: ${supportedHelpers.join(',')}`)
    }
    if (!config) {
        config = {}
    }
    if (!config.locator) {
        config.locator = "shadow"
    }

    event.dispatcher.on(event.suite.before, async () => {
        if(helperName === "Playwright") {
            // temp handle api change in playwright may need to move to major version lib for documentation
            try {
                await playwright.selectors.register(selectorEngine, { name: config.locator });
            } catch(e) {
                await playwright.selectors.register(config.locator, selectorEngine);
            }
        }
    });
}

================================================
FILE: plugins/playwright/index.js
================================================

const fs = require("fs");
const path = require("path");

// load the library in UMD format which self executes and adds window.querySelectorShadowDom
const querySelectorShadowDomUMD = fs.readFileSync(path.resolve(__dirname, "../../dist/querySelectorShadowDom.js"))

// a string because playwright does a .toString on a selector engine and we need to
// make sure that query-selector-shadow-dom is injected and loaded into the function closure
const engineString =`
    ${querySelectorShadowDomUMD}
    return {
        create(root, target) {
            return undefined;
        },
        query(root, selector) {
            return querySelectorShadowDom.querySelectorDeep(selector, root);
        },
        queryAll(root, selector) {
            return querySelectorShadowDom.querySelectorAllDeep(selector, root);
        }
    }
`
const selectorEngine = new Function("", engineString)

module.exports = { selectorEngine };

================================================
FILE: plugins/protractor/index.d.ts
================================================
declare module 'protractor' {
    import { Locator } from 'protractor';

    export interface ProtractorBy {
        /**
         * Find element within the Shadow DOM.
         *
         * @param {string} selector
         * @returns {Locator} location strategy
         */
        shadowDomCss(selector: string): Locator;
    }
}


================================================
FILE: plugins/protractor/index.js
================================================
const fs = require('fs');
const path = require('path');
const querySelectorAllDeep = fs.readFileSync(path.resolve(__dirname, "../../dist/querySelectorShadowDom.js"))

module.exports = {
    name: 'query-selector-shadow-dom',
    onPrepare: function() {
        global.by.addLocator('shadowDomCss', `
            var selector /* string */ = arguments[0];
            var parentElement /* WebElement? */ = arguments[1];
            var rootSelector /* string? */ = arguments[2];
            
            ${ querySelectorAllDeep }

            return querySelectorShadowDom.querySelectorAllDeep(selector, parentElement || document)
        `);
    }
}


================================================
FILE: plugins/puppeteer/index.js
================================================
const fs = require("fs");
const path = require("path");

const querySelectorShadowDomUMD = fs.readFileSync(path.resolve(__dirname, "../../dist/querySelectorShadowDom.js"))

const QueryHandler = {
    queryOne: new Function('element', 'selector', `
        ${querySelectorShadowDomUMD}
        return querySelectorShadowDom.querySelectorDeep(selector, element);
    `),
    queryAll: new Function('element', 'selector', `
    ${querySelectorShadowDomUMD}
        return querySelectorShadowDom.querySelectorAllDeep(selector, element);
    `)
};
module.exports.QueryHandler = QueryHandler;

================================================
FILE: plugins/webdriverio/index.js
================================================
const fs = require('fs');
const path = require('path');
const querySelectorAllDeep = fs.readFileSync(path.resolve(__dirname, "../../dist/querySelectorShadowDom.js"))

const selectorFunction = new Function('selector', 'element', `
${querySelectorAllDeep}
return querySelectorShadowDom.querySelectorAllDeep(selector, element);
`);

module.exports.locatorStrategy = selectorFunction;

================================================
FILE: protractor.conf.js
================================================
exports.config = {
    baseUrl: 'http://localhost:3000',

    chromeDriver: process.env.CHROMEDRIVER_FILEPATH || require(`chromedriver/lib/chromedriver`).path,
    SELENIUM_PROMISE_MANAGER: false,
    directConnect: true,

    // https://github.com/angular/protractor/blob/main/docs/timeouts.md
    allScriptsTimeout: 110000,

    specs: [ './test/protractor-locator.e2e.js', ],

    plugins: [{
        path: './plugins/protractor'
    }],

    onPrepare: function() {
        global.browser.waitForAngularEnabled(false);
    },

    capabilities: {
        browserName: 'chrome',

        chromeOptions: {
            args: [
                '--no-sandbox',
                '--disable-infobars',
                '--disable-dev-shm-usage',
                '--disable-extensions',
                '--log-level=3',
                '--disable-gpu',
                '--window-size=1920,1080',
                '--headless'
            ]
        }
    }
};


================================================
FILE: puppeteer-es5.js
================================================
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
(async() => {
    try {
        const browser = await puppeteer.launch()
        const page = await browser.newPage()
        await page.goto('https://www.polymer-project.org/2.0/docs/upgrade')
        await page.addScriptTag({
            path: path.join(__dirname, 'node_modules/query-selector-shadow-dom/dist/querySelectorShadowDom.js')
        });

        // execute standard javascript in the context of the page.
        const downloads = await page.evaluate(() => {
            const anchors = Array.from(querySelectorShadowDom.querySelectorAllDeep('a'))
            return anchors.map(anchor => anchor.href)
        })
        console.log(downloads)
        await browser.close()
    } catch (e) {
        console.error(e);
    }

})()

================================================
FILE: rollup.config.js
================================================
import babel from 'rollup-plugin-babel';
import babelrc from 'babelrc-rollup';
import { terser } from 'rollup-plugin-terser';

const babelConfig = {
    'presets': [
        ['@babel/preset-env', {
            'targets': {
                'browsers': ['last 2 versions', 'IE >= 11']
            },
            'loose': true
        }]
    ]
};

export default {
    input: 'src/querySelectorDeep.js',
    plugins: [
        babel(babelrc({
            addExternalHelpersPlugin: false,
            config: babelConfig,
            exclude: 'node_modules/**'
        })),
        terser()
    ],
    output: {
        format: 'iife',
        name: 'querySelectorShadowDom',
        file: 'dist/querySelectorShadowDom.js'
    }
};

================================================
FILE: src/normalize.js
================================================
/* istanbul ignore file */


// normalize-selector-rev-02.js
/*
  author: kyle simpson (@getify)
  original source: https://gist.github.com/getify/9679380

  modified for tests by david kaye (@dfkaye)
  21 march 2014

  rev-02 incorporate kyle's changes 3/2/42014
*/

export function normalizeSelector(sel) {
  // save unmatched text, if any
  function saveUnmatched() {
    if (unmatched) {
      // whitespace needed after combinator?
      if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) {
        tokens.push(" ");
      }

      // save unmatched text
      tokens.push(unmatched);
    }
  }

  var tokens = [],
    match,
    unmatched,
    regex,
    state = [0],
    next_match_idx = 0,
    prev_match_idx,
    not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/,
    whitespace_pattern = /^\s+$/,
    state_patterns = [
      /\s+|\/\*|["'>~+[(]/g, // general
      /\s+|\/\*|["'[\]()]/g, // [..] set
      /\s+|\/\*|["'[\]()]/g, // (..) set
      null, // string literal (placeholder)
      /\*\//g, // comment
    ];
  sel = sel.trim();

  // eslint-disable-next-line no-constant-condition
  while (true) {
    unmatched = "";

    regex = state_patterns[state[state.length - 1]];

    regex.lastIndex = next_match_idx;
    match = regex.exec(sel);

    // matched text to process?
    if (match) {
      prev_match_idx = next_match_idx;
      next_match_idx = regex.lastIndex;

      // collect the previous string chunk not matched before this token
      if (prev_match_idx < next_match_idx - match[0].length) {
        unmatched = sel.substring(
          prev_match_idx,
          next_match_idx - match[0].length
        );
      }

      // general, [ ] pair, ( ) pair?
      if (state[state.length - 1] < 3) {
        saveUnmatched();

        // starting a [ ] pair?
        if (match[0] === "[") {
          state.push(1);
        }
        // starting a ( ) pair?
        else if (match[0] === "(") {
          state.push(2);
        }
        // starting a string literal?
        else if (/^["']$/.test(match[0])) {
          state.push(3);
          state_patterns[3] = new RegExp(match[0], "g");
        }
        // starting a comment?
        else if (match[0] === "/*") {
          state.push(4);
        }
        // ending a [ ] or ( ) pair?
        else if (/^[\])]$/.test(match[0]) && state.length > 0) {
          state.pop();
        }
        // handling whitespace or a combinator?
        else if (/^(?:\s+|[~+>])$/.test(match[0])) {
          // need to insert whitespace before?
          if (
            tokens.length > 0 &&
            !whitespace_pattern.test(tokens[tokens.length - 1]) &&
            state[state.length - 1] === 0
          ) {
            // add normalized whitespace
            tokens.push(" ");
          }

          // case-insensitive attribute selector CSS L4
          if (
            state[state.length - 1] === 1 &&
            tokens.length === 5 &&
            tokens[2].charAt(tokens[2].length - 1) === "="
          ) {
            tokens[4] = " " + tokens[4];
          }

          // whitespace token we can skip?
          if (whitespace_pattern.test(match[0])) {
            continue;
          }
        }

        // save matched text
        tokens.push(match[0]);
      }
      // otherwise, string literal or comment
      else {
        // save unmatched text
        tokens[tokens.length - 1] += unmatched;

        // unescaped terminator to string literal or comment?
        if (not_escaped_pattern.test(tokens[tokens.length - 1])) {
          // comment terminator?
          if (state[state.length - 1] === 4) {
            // ok to drop comment?
            if (
              tokens.length < 2 ||
              whitespace_pattern.test(tokens[tokens.length - 2])
            ) {
              tokens.pop();
            }
            // otherwise, turn comment into whitespace
            else {
              tokens[tokens.length - 1] = " ";
            }

            // handled already
            match[0] = "";
          }

          state.pop();
        }

        // append matched text to existing token
        tokens[tokens.length - 1] += match[0];
      }
    }
    // otherwise, end of processing (no more matches)
    else {
      unmatched = sel.substr(next_match_idx);
      saveUnmatched();

      break;
    }
  }

  return tokens.join("").trim();
}


================================================
FILE: src/querySelectorDeep.js
================================================
import { normalizeSelector } from './normalize';

/**
* Finds first matching elements on the page that may be in a shadow root using a complex selector of n-depth
*
* Don't have to specify all shadow roots to button, tree is travered to find the correct element
*
* Example querySelectorAllDeep('downloads-item:nth-child(4) #remove');
*
* Example should work on chrome://downloads outputting the remove button inside of a download card component
*
* Example find first active download link element querySelectorDeep('#downloads-list .is-active a[href^="https://"]');
*
* Another example querySelectorAllDeep('#downloads-list div#title-area + a');
e.g.
*/
export function querySelectorAllDeep(selector, root = document, allElements = null) {
    return _querySelectorDeep(selector, true, root, allElements);
}

export function querySelectorDeep(selector, root = document, allElements = null) {
    return _querySelectorDeep(selector, false, root, allElements);
}

function _querySelectorDeep(selector, findMany, root, allElements = null) {
    selector = normalizeSelector(selector);
    let lightElement = root.querySelector(selector);

    if (document.head.createShadowRoot || document.head.attachShadow) {
        // no need to do any special if selector matches something specific in light-dom
        if (!findMany && lightElement) {
            return lightElement;
        }

        // split on commas because those are a logical divide in the operation
        const selectionsToMake = splitByCharacterUnlessQuoted(selector, ',');

        return selectionsToMake.reduce((acc, minimalSelector) => {
            // if not finding many just reduce the first match
            if (!findMany && acc) {
                return acc;
            }
            // do best to support complex selectors and split the query
            const splitSelector = splitByCharacterUnlessQuoted(minimalSelector
                    //remove white space at start of selector
                    .replace(/^\s+/g, '')
                    .replace(/\s*([>+~]+)\s*/g, '$1'), ' ')
                    // filter out entry white selectors
                    .filter((entry) => !!entry)
                    // convert "a > b" to ["a", "b"]
                    .map((entry) => splitByCharacterUnlessQuoted(entry, '>'));

            const possibleElementsIndex = splitSelector.length - 1;
            const lastSplitPart = splitSelector[possibleElementsIndex][splitSelector[possibleElementsIndex].length - 1];
            const possibleElements = collectAllElementsDeep(lastSplitPart, root, allElements);
            const findElements = findMatchingElement(splitSelector, possibleElementsIndex, root);
            if (findMany) {
                acc = acc.concat(possibleElements.filter(findElements));
                return acc;
            } else {
                acc = possibleElements.find(findElements);
                return acc || null;
            }
        }, findMany ? [] : null);


    } else {
        if (!findMany) {
            return lightElement;
        } else {
            return root.querySelectorAll(selector);
        }
    }

}

function findMatchingElement(splitSelector, possibleElementsIndex, root) {
    return (element) => {
        let position = possibleElementsIndex;
        let parent = element;
        let foundElement = false;
        while (parent && !isDocumentNode(parent)) {
            let foundMatch = true;
            if (splitSelector[position].length === 1) {
                foundMatch = parent.matches(splitSelector[position]);
            } else {
                // selector is in the format "a > b"
                // make sure a few parents match in order
                const reversedParts = ([]).concat(splitSelector[position]).reverse();
                let newParent = parent;
                for (const part of reversedParts) {
                    if (!newParent || !newParent.matches(part)) {
                        foundMatch = false;
                        break;
                    }
                    newParent = findParentOrHost(newParent, root);
                }
            }

            if (foundMatch && position === 0) {
                foundElement = true;
                break;
            }
            if (foundMatch) {
                position--;
            }
            parent = findParentOrHost(parent, root);
        }
        return foundElement;
    };

}

function splitByCharacterUnlessQuoted(selector, character) {
    return selector.match(/\\?.|^$/g).reduce((p, c) => {
        if (c === '"' && !p.sQuote) {
            p.quote ^= 1;
            p.a[p.a.length - 1] += c;
        } else if (c === '\'' && !p.quote) {
            p.sQuote ^= 1;
            p.a[p.a.length - 1] += c;

        } else if (!p.quote && !p.sQuote && c === character) {
            p.a.push('');
        } else {
            p.a[p.a.length - 1] += c;
        }
        return p;
    }, { a: [''] }).a;
}

/**
 * Checks if the node is a document node or not.
 * @param {Node} node
 * @returns {node is Document | DocumentFragment}
 */
function isDocumentNode(node) {
    return node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.DOCUMENT_NODE;
}

function findParentOrHost(element, root) {
    const parentNode = element.parentNode;
    return (parentNode && parentNode.host && parentNode.nodeType === 11) ? parentNode.host : parentNode === root ? null : parentNode;
}

/**
 * Finds all elements on the page, inclusive of those within shadow roots.
 * @param {string=} selector Simple selector to filter the elements by. e.g. 'a', 'div.main'
 * @return {!Array<string>} List of anchor hrefs.
 * @author ebidel@ (Eric Bidelman)
 * License Apache-2.0
 */
export function collectAllElementsDeep(selector = null, root, cachedElements = null) {
    let allElements = [];

    if (cachedElements) {
        allElements = cachedElements;
    } else {
        const findAllElements = function(nodes) {
            for (let i = 0; i < nodes.length; i++) {
                const el = nodes[i];
                allElements.push(el);
                // If the element has a shadow root, dig deeper.
                if (el.shadowRoot) {
                    findAllElements(el.shadowRoot.querySelectorAll('*'));
                }
            }
        };
        if(root.shadowRoot) {
            findAllElements(root.shadowRoot.querySelectorAll('*'));
        }
        findAllElements(root.querySelectorAll('*'));
    }

    return selector ? allElements.filter(el => el.matches(selector)) : allElements;	}



================================================
FILE: steps.d.ts
================================================
/// <reference types='codeceptjs' />
type steps_file = typeof import('./steps_file.js');

declare namespace CodeceptJS {
  interface SupportObject { I: CodeceptJS.I }
  interface CallbackOrder { [0]: CodeceptJS.I }
  interface Methods extends CodeceptJS.Playwright {}
  interface I extends ReturnType<steps_file> {}
  namespace Translation {
    interface Actions {}
  }
}


================================================
FILE: steps_file.js
================================================
// in this file you can append custom step methods to 'I' object

module.exports = function() {
  return actor({

    // Define custom steps here, use 'this' to access default methods of I.
    // It is recommended to place a general 'login' function here.

  });
}


================================================
FILE: test/TestComponent.js
================================================
export class TestComponent extends HTMLElement {

    constructor({ childClassName = 'test-child', childTextContent = 'Child Content', internalHTML = '' } = {}) {
        super();
        this.childClassName = childClassName;
        this.childTextContent = childTextContent;
        this.internalHTML = internalHTML;
        this.style.display = 'block';
        this.style.margin = '5px';
    }

    connectedCallback() {
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `<p class="${this.childClassName}">${this.childTextContent}</p><div>${this.internalHTML}</div><slot></slot>`;
    }

    add(child) {
        this.shadowRoot.appendChild(child);
    }

    addNested(child) {
        this.shadowRoot.querySelector('p').appendChild(child);
    }

    static get observedAttributes() {
        return ['child-class-name', 'child-text-content', 'internal-html'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        switch (name) {
            case 'child-class-name':
                this.childClassName = newValue;
                break;
            case 'child-text-content':
                this.childTextContent = newValue;
                break;
            case 'internal-html':
                this.internalHTML = newValue;
                break;
        }
    }
}
customElements.define('test-component', TestComponent);

================================================
FILE: test/basic.spec.js
================================================
import { querySelectorAllDeep, querySelectorDeep, collectAllElementsDeep } from '../src/querySelectorDeep.js';
import { createTestComponent, createNestedComponent, COMPONENT_NAME, createChildElements } from './createTestComponent.js';



describe("Basic Suite", function() {

    let parent;
    let baseComponent;

    function setup() {
        parent = document.createElement('div');
        document.body.appendChild(parent);
        baseComponent = createTestComponent(parent, {
            childClassName: 'base',
            childTextContent: 'Base Component'
        });
        baseComponent.classList.add('base-comp');
    }
    beforeEach(() => {
        setup();
    });

    afterEach(() => {
        parent.remove();
    });

    it("exports querySelectorAllDeep function", function() {
        expect(querySelectorAllDeep).toEqual(jasmine.any(Function));
    });

    it("exports querySelectorDeep function", function() {
        expect(querySelectorDeep).toEqual(jasmine.any(Function));
    });

    it("exports collectAllElementsDeep function", function() {
        expect(collectAllElementsDeep).toEqual(jasmine.any(Function));
    });

    it("querySelectorDeep returns null when not found", function() {
        expect(querySelectorDeep('whatever')).toBeNull();
    });

    it("querySelectorAllDeep returns empty array when not found", function() {
        const foundElements = querySelectorAllDeep('whatever');
        expect(foundElements).toEqual(jasmine.any(Array));
        expect(foundElements.length).toEqual(0);
    });


    describe("querySelectorDeep", function() {
        it('can access an element in the light dom', function() {
            createTestComponent(parent);
            const testComponent = querySelectorDeep(COMPONENT_NAME);
            expect(testComponent).toBeTruthy();
        });

        it('can access direct shadow dom child of the root', function() {
            const rootComponent = createTestComponent(parent, {
                childClassName: "child-class",
            });
            const child = querySelectorDeep('.child-class', rootComponent);
            expect(child).toBeTruthy();
        });


        it('can access an element in the shadow dom', function() {
            createTestComponent(parent, {
                childTextContent: 'Child Content'
            });
            const testSubComponent = querySelectorDeep('.test-child');
            expect(testSubComponent).toBeTruthy();
            expect(testSubComponent.textContent).toEqual('Child Content')
        });

        it('can access an element nested in many shadow dom', function() {
            createNestedComponent(baseComponent, 20);
            const testSubComponent = querySelectorDeep('.desc-20');
            expect(testSubComponent).toBeTruthy();
            expect(testSubComponent.textContent).toEqual('Descendant 20')
        });


        it('can use find the nth-child inside of a shadow root', function() {
            createNestedComponent(baseComponent, 10, {
                createChildren: createChildElements
            });
            const testSubComponent = querySelectorDeep('.desc-5 div:nth-child(2)');
            expect(testSubComponent).toBeTruthy();
            expect(testSubComponent.textContent).toEqual('Child 2')
        });

        it('selector list only returns the first element', function() {
            createNestedComponent(baseComponent, 10, {
                createChildren: createChildElements
            });
            const testSubComponent = querySelectorDeep('.desc-5 div:nth-child(2), .desc-1');
            expect(testSubComponent).toBeTruthy();
            expect(testSubComponent.textContent).toEqual('Child 2')
        });

        it('returns null when reaching the document node and no matching parent was found', function() {
            const rootComponent = createTestComponent(parent, {
                childClassName: 'child',
            })
            const testSubComponent = querySelectorDeep('.parent .child', rootComponent);
            expect(testSubComponent).toBeNull();
        });
    });


    describe("querySelectorAll", function() {

        it('can locate all instances of components across even in shadow dom (except base)', function() {
            createNestedComponent(baseComponent, 10);
            const testComponents = querySelectorAllDeep(`test-component:not(.base-comp)`);
            expect(testComponents.length).toEqual(10);

        });

        it('can get elements matching or selector', function() {
            createTestComponent(parent, {
                childClassName: 'header-1'
            });
            const element = createTestComponent(parent, {
                childClassName: 'header-2'
            });
            element.classList.add('parent')
            const testComponents = querySelectorAllDeep(`.header-1, .header-2, .parent`);
            expect(testComponents.length).toEqual(3);

        });

        it('host property on non shadowRoot element is ignored', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2"><div class="find-me"></div></div>'
            });
            testComponent.shadowRoot.querySelector('.header-2').host = "test.com";
            testComponent.classList.add('container');
            const testComponents = querySelectorAllDeep(`.container .find-me`);
            expect(testComponents.length).toEqual(1);
        });

        it('can see inside the shadowRoot with ">" in selector', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2"><div class="find-me"></div></div>'
            });
            testComponent.shadowRoot.querySelector('.header-2').host = "test.com";
            testComponent.classList.add('container');
            const testComponents = querySelectorAllDeep(`.container > div > .header-2 > .find-me`);
            expect(testComponents.length).toEqual(1);
            expect(testComponents[0].classList.contains('find-me')).toEqual(true);
        });

        it('handles descendant selector > that dooes not match child', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2"><div class="find-me"></div></div>'
            });
            testComponent.shadowRoot.querySelector('.header-2').host = "test.com";
            testComponent.classList.add('container');
            const testComponents = querySelectorAllDeep(`.container > div > .header-2 > .doesnt-exist`);
            expect(testComponents.length).toEqual(0);
        });

        it('handles descendant selector where child exists but parent does not', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2"><div class="find-me"></div></div>'
            });
            testComponent.shadowRoot.querySelector('.header-2').host = "test.com";
            testComponent.classList.add('container');
            const testComponents = querySelectorAllDeep(`.container > div > .doesnt-exist > .find-me`);
            expect(testComponents.length).toEqual(0);
        });


        it('can handle extra white space in selectors', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1      .header-2`);
            expect(testComponents.length).toEqual(2);

        });

        it('can handle attribute selector value', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div data-test="Hello-World" class="header-2">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            testComponent.setAttribute('data-test', '123')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [data-test="Hello-World"]`);
            expect(testComponents.length).toEqual(1);
            expect(testComponents[0].classList.contains('header-2')).toBeTruthy();
        });


        it('can handle extra white space in attribute value', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div data-test="Hello World" class="header-2">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            // this should not match as matching children
            testComponent.setAttribute('data-test', 'Hello World')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [data-test="Hello World"]`);
            expect(testComponents.length).toEqual(1);
        });


        it('can handle comma in attribute values', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            const test2 = createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            test2.setAttribute('data-test', 'Hello, World')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [data-test="Hello, World"], .header-2, .header-1`);
            expect(testComponents.length).toEqual(5);
        });

        it('can handle spacing around attribute values', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            const test2 = createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            test2.setAttribute('data-test', 'Hello, World')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [ data-test = "Hello, World" ], .header-2, .header-1`);
            expect(testComponents.length).toEqual(5);
        });

        it('can handle spacing around attribute values with [ in attribute', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            const test2 = createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            test2.setAttribute('data-braces-test', ' [ Hello, World ] ')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [ data-braces-test = " [ Hello, World ] " ], .header-2, .header-1`);
            expect(testComponents.length).toEqual(5);
        });

        it('can escaped comma in attribute values', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            const test2 = createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            test2.setAttribute('data-test', 'Hello\, World')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [data-test="Hello\, World"]`);
            expect(testComponents.length).toEqual(1);
        });


        it('can handle escaped data in attributes', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            const test2 = createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            test2.setAttribute('data-test', 'Hello" World')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1 [data-test="Hello\\" World"]`);
            expect(testComponents.length).toEqual(1);
        });

        it('can handle extra white space in single quoted attribute value', function() {
            const testComponent = createTestComponent(parent, {
                childClassName: 'header-1',
                internalHTML: '<div class="header-2">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-2'
            });
            testComponent.setAttribute('data-test', 'Hello " \'World\'')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1[data-test='Hello \\" \\'World\\'']`);
            expect(testComponents.length).toEqual(1);
        });

        it('split correctly on selector list', function() {
            const testComponent = createTestComponent(parent, {
                internalHTML: '<span class="header-2"></span><div data-test="Hello" World" class="header-3">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-4'
            });
            testComponent.setAttribute('data-test', '123')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`.header-1,.header-2 + .header-3`);
            expect(testComponents.length).toEqual(2);
            expect(testComponents[1].classList.contains('header-3')).toBeTruthy();
        });

        it('split correctly on selector list (ignore white space)', function() {
            const testComponent = createTestComponent(parent, {
                internalHTML: '<span class="header-2"></span><div data-test="Hello World" class="header-3">Content</div>'
            });
            createTestComponent(testComponent, {
                childClassName: 'header-4'
            });
            testComponent.setAttribute('data-test', '123')
            testComponent.classList.add('header-1');
            const testComponents = querySelectorAllDeep(`    .header-1,     .header-2 + .header-3`);
            expect(testComponents.length).toEqual(2);
            expect(testComponents[1].classList.contains('header-3')).toBeTruthy();
        });

        it('can provide an alternative node', function() {
            const root = document.createElement('div');
            parent.appendChild(root);

            createTestComponent(root, {
                childClassName: 'inner-content'
            });

            createTestComponent(parent, {
                childClassName: 'inner-content'
            });
            const testComponent = querySelectorAllDeep('.inner-content', root);
            expect(testComponent.length).toEqual(1);

        });

        it('can cache collected elements with collectAllElementsDeep', function() {
            const root = document.createElement('div');
            parent.appendChild(root);

            createTestComponent(root, {
                childClassName: 'inner-content'
            });

            createTestComponent(parent, {
                childClassName: 'inner-content'
            });
            const collectedElements = collectAllElementsDeep('*', root)
            expect(collectedElements.length).toEqual(4);

            const testComponents = querySelectorAllDeep('.inner-content', root, collectedElements);
            expect(testComponents.length).toEqual(1);

            // remove element from dom
            testComponents[0].remove()

            // not found in dom
            const testComponents2 = querySelectorAllDeep('.inner-content', root);
            expect(testComponents2.length).toEqual(0);

            // still there with cached collectedElements
            const testComponents3 = querySelectorAllDeep('.inner-content', root, collectedElements);
            expect(testComponents3.length).toEqual(1);
        });

        it('empty collectAllElementsDeep find all elements', function() {
            const root = document.createElement('div');
            parent.appendChild(root);

            createTestComponent(root, {
                childClassName: 'inner-content'
            });

            createTestComponent(parent, {
                childClassName: 'inner-content'
            });
            const collectedElements = collectAllElementsDeep('', root);
            expect(collectedElements.length).toEqual(4);
        });

        it('can query nodes in an iframe', function(done) {

            const innerframe = `<p class='child'>Content</p>`;
            createTestComponent(parent, {
                internalHTML: `<iframe id="frame" srcdoc="${innerframe}"></iframe>`
            });
            setTimeout(() => {
                const iframe = querySelectorDeep('#frame');
                const testComponents = querySelectorAllDeep('.child', iframe.contentDocument);
                expect(testComponents.length).toEqual(1);
                expect(testComponents[0].textContent).toEqual("Content");
                done();
            }, 150);


        });



        // describe(".perf", function() {

        //     function generateQuerySelectorAllTest(count) {
        //         it(`can create ${count} shadow roots and search for all instances that match`, async function() {
        //             for (let i = 0; i < count; i++) {
        //                 createTestComponent(baseComponent, count);
        //             }
        //             const testComponents = querySelectorAllDeep('test-component [class=test-child]');
        //             expect(testComponents.length).toEqual(count);
        //         });
        //     }

        //     generateQuerySelectorAllTest(200000);

        // });

    });


});


================================================
FILE: test/codeceptjs/README.md
================================================
CodeceptJS functions to test:

appendField
attachFile
checkOption
clearField
click
clickLin
dontSee
dontSeeCheckboxIsChecked
dontSeeCookie
dontSeeCurrentUrlEquals
dontSeeElement
dontSeeElementInDOM
dontSeeInField
doubleClick
dragAndDrop
dragSlider
fillField
forceClick
grabAttributeFrom
grabCssPropertyFrom
grabDataFromPerformanceTiming
grabElementBoundingRect
grabHTMLFrom
grabNumberOfVisibleElements
grabPageScrollPosition
grabTextFrom
grabValueFrom
moveCursorTo
openNewTab
pressKey
pressKeyDown
pressKeyUp
rightClick
scrollPageToBottom
scrollPageToTop
scrollTo
see
seeAttributesOnElements
seeCheckboxIsChecked
seeCssPropertiesOnElements
seeElement
seeElementInDO
seeInField
seeNumberOfElements
seeNumberOfVisibleElements
seeTextEquals
selectOptio
uncheckOptio
waitForClickable
waitForDetached
waitForElement
waitForEnabled
waitForInvisible
waitForText
waitForValue
waitForVisible
waitNumberOfVisibleElements
waitToHide
waitUntil

================================================
FILE: test/codeceptjs/codecept.conf.js
================================================
exports.config = {
  tests: 'test/codeceptjs/*.test.js',
  output: './output',
  helpers: {
    Playwright: {
      url: 'http://localhost',
      show: true,
      browser: 'chromium'
    }
  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'codeceptjs',
  plugins: {
    retryFailedStep: {
      enabled: true
    },
    screenshotOnFail: {
      enabled: true
    }
  }
}

================================================
FILE: test/codeceptjs/components.test.js
================================================
Feature('components');

Scenario('test something', (I) => {

});


================================================
FILE: test/codeceptjs/jsconfig.json
================================================
{
  "compilerOptions": {
    "allowJs": true
  }
}

================================================
FILE: test/codeceptjs/steps.d.ts
================================================
/// <reference types='codeceptjs' />
type steps_file = typeof import('./steps_file.js');

declare namespace CodeceptJS {
  interface SupportObject { I: CodeceptJS.I }
  interface CallbackOrder { [0]: CodeceptJS.I }
  interface Methods extends CodeceptJS.Playwright {}
  interface I extends ReturnType<steps_file> {}
  namespace Translation {
    interface Actions {}
  }
}


================================================
FILE: test/codeceptjs/steps_file.js
================================================
// in this file you can append custom step methods to 'I' object

module.exports = function() {
  return actor({

    // Define custom steps here, use 'this' to access default methods of I.
    // It is recommended to place a general 'login' function here.

  });
}


================================================
FILE: test/createTestComponent.js
================================================
import { TestComponent } from './TestComponent.js';
export function createTestComponent(parent, options = {}) {
    return parent.appendChild(new TestComponent(options));
}

export const createChildElements = (component) => {
    for (let i = 0; i < 3; i++) {
        const child = document.createElement('div');
        child.textContent = `Child ${i+1}`;
        component.addNested(child);
    }
};

export function createNestedComponent(parent, count = 1, { createChildren = () => {}, childClass = (count) => `desc-${count}`, childContent = (count) => `Descendant ${count}` } = {}) {
    if (count > 2000) {
        const split = count / 2;
        for (let i = 0; i < count; i += split) {
            createNestedComponent(parent, split, {
                createChildren,
                childClass,
                childContent
            });
        }
    } else {
        if (count === 0) {
            return;
        }
        const component = new TestComponent({
            childClassName: childClass(count),
            childTextContent: childContent(count)
        });
        parent.add(component);
        createChildren(component);
        count = count - 1;
        createNestedComponent(component, count, { createChildren, childClass, childContent });
    }



}

export const COMPONENT_NAME = 'test-component';

================================================
FILE: test/index.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Testing</title>
</head>

<body>
    <script>
        (async () => {
            const {
                createTestComponent,
                createNestedComponent,
                createChildElements
            } = await
                    import('./createTestComponent.js');
            const {
                querySelectorAllDeep,
                querySelectorDeep
            } = await
                    import('../src/querySelectorDeep.js');
            const parent = document.createElement('div');
            document.body.appendChild(parent);
            const innerframe = `<p class='child'>Content</p>`;
            createTestComponent(parent, {
                internalHTML: `<button class="btn-in-shadow-dom">Shadow dom button</button><span class="output">Not clicked</span>`
            });

            createTestComponent(parent, {
                internalHTML: `<input id="type-to-input" type="text"/>`
            });

            querySelectorDeep(".btn-in-shadow-dom").onclick = (e) => querySelectorDeep(".output", e.target.parentElement).innerText = "clicked the btn"
            setTimeout(() => {
                const iframe = querySelectorDeep('#frame');
                const testComponents = querySelectorAllDeep('.child', iframe.contentDocument);
                expect(testComponents.length).toEqual(1);
                expect(testComponents[0].textContent).toEqual("Content");
                done();
            }, 300);
        })();
    </script>
</body>

</html>

================================================
FILE: test/nopolyfills.spec.js
================================================
import { querySelectorAllDeep, querySelectorDeep } from '../src/querySelectorDeep.js';

describe("No Polyfills Suite", function() {

    let parent;

    function setup() {
        parent = document.createElement('div');
        document.body.appendChild(parent);
    }
    beforeEach(() => {
        spyOnProperty(document, 'head', 'get').and.returnValue({
            attachShadow: undefined,
            createShadowRoot: undefined
        });
        setup();
    });

    afterEach(() => {
        parent.remove();
    });


    it('can fallback to query selector when no support and no polyfills', function() {
        const element = document.createElement('a');
        element.classList.add('testing');
        parent.appendChild(element);
        expect(querySelectorDeep('.testing')).toBeTruthy();
    });

    it('can fallback to query selector all when no support and no polyfills', function() {
        const element = document.createElement('a');
        const element2 = document.createElement('a');
        element.classList.add('testing');
        element2.classList.add('testing');
        parent.appendChild(element);
        parent.appendChild(element2);
        expect(querySelectorAllDeep('.testing').length).toEqual(2);
    });

    it('can fallback to query selector when no support and no polyfills with alternative root', function() {
        const root = document.createElement('div');
        parent.appendChild(root);
        const element = document.createElement('a');
        const element2 = document.createElement('a');
        element.classList.add('testing');
        element2.classList.add('testing');
        root.appendChild(element);
        parent.appendChild(element2);

        expect(querySelectorAllDeep('.testing', root).length).toEqual(1);
    });



});

================================================
FILE: test/protractor-locator.e2e.js
================================================
describe('query-selector-shadow-dom', () => {

    beforeEach(async () => {
        await browser.get(pageFromTemplate(`
            var firstParent = document.createElement('div');
            firstParent.id = 'first';
            firstParent.classList.add('parent');            
            document.body.appendChild(firstParent);
            
            var secondParent = document.createElement('div');
            secondParent.classList.add('parent');            
            document.body.appendChild(secondParent);
            
            var firstShadowChild = firstParent.attachShadow({ mode: 'open' });
            firstShadowChild.innerHTML = '<p class="shadow-child"><span class="name">First child</span> with Shadow DOM</p>';        
            
            var secondShadowChild = secondParent.attachShadow({ mode: 'open' });
            secondShadowChild.innerHTML = '<p class="shadow-child"><span class="name">Second child</span> with Shadow DOM</p>';
        `));
    });

    it(`identifies a single element matching the selector`, async () => {
        const text = await element(by.shadowDomCss('#first.parent .shadow-child .name')).getText();

        expect(text).toEqual('First child');
    });

    it(`identifies all elements matching the selector`, async () => {
        const text = await element.all(by.shadowDomCss('.parent .shadow-child .name')).getText();

        expect(text).toEqual(['First child', 'Second child']);
    });
});

/**
 * Turns a HTML template into a data URL Protractor can navigate to without having to use a web server
 *
 * @param {string} template
 * @returns {string}
 */
function pageFromTemplate(template /* string */) /* string */ {
    return `data:text/html;charset=utf-8,<!DOCTYPE html>
        <html>
            <body>
                <script>${ template }</script>
            </body>
        </html>`.replace(/[\s\n]+/s, ' ');
}
Download .txt
gitextract_u8vckhgp/

├── .babelrc
├── .eslintrc.js
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── LICENSE
├── README.md
├── codecept.conf.js
├── demo.js
├── examples/
│   ├── playwright/
│   │   └── custom-engine.js
│   ├── puppeteer/
│   │   ├── clicking-elements.js
│   │   ├── custom-engine.js
│   │   ├── multiple-elements.js
│   │   └── typing-to-elements.js
│   └── webdriverio/
│       ├── deeply-nested.js
│       ├── multiple-elements.js
│       └── typing-to-elements.js
├── jsconfig.json
├── karma.common.js
├── karma.conf.js
├── package.json
├── plugins/
│   ├── codeceptjs/
│   │   ├── README.md
│   │   └── index.js
│   ├── playwright/
│   │   └── index.js
│   ├── protractor/
│   │   ├── index.d.ts
│   │   └── index.js
│   ├── puppeteer/
│   │   └── index.js
│   └── webdriverio/
│       └── index.js
├── protractor.conf.js
├── puppeteer-es5.js
├── rollup.config.js
├── src/
│   ├── normalize.js
│   └── querySelectorDeep.js
├── steps.d.ts
├── steps_file.js
└── test/
    ├── TestComponent.js
    ├── basic.spec.js
    ├── codeceptjs/
    │   ├── README.md
    │   ├── codecept.conf.js
    │   ├── components.test.js
    │   ├── jsconfig.json
    │   ├── steps.d.ts
    │   └── steps_file.js
    ├── createTestComponent.js
    ├── index.html
    ├── nopolyfills.spec.js
    └── protractor-locator.e2e.js
Download .txt
SYMBOL INDEX (35 symbols across 10 files)

FILE: plugins/protractor/index.d.ts
  type ProtractorBy (line 4) | interface ProtractorBy {

FILE: src/normalize.js
  function normalizeSelector (line 15) | function normalizeSelector(sel) {

FILE: src/querySelectorDeep.js
  function querySelectorAllDeep (line 17) | function querySelectorAllDeep(selector, root = document, allElements = n...
  function querySelectorDeep (line 21) | function querySelectorDeep(selector, root = document, allElements = null) {
  function _querySelectorDeep (line 25) | function _querySelectorDeep(selector, findMany, root, allElements = null) {
  function findMatchingElement (line 77) | function findMatchingElement(splitSelector, possibleElementsIndex, root) {
  function splitByCharacterUnlessQuoted (line 114) | function splitByCharacterUnlessQuoted(selector, character) {
  function isDocumentNode (line 137) | function isDocumentNode(node) {
  function findParentOrHost (line 141) | function findParentOrHost(element, root) {
  function collectAllElementsDeep (line 153) | function collectAllElementsDeep(selector = null, root, cachedElements = ...

FILE: steps.d.ts
  type steps_file (line 2) | type steps_file = typeof import('./steps_file.js');
  type SupportObject (line 5) | interface SupportObject { I: CodeceptJS.I }
  type CallbackOrder (line 6) | interface CallbackOrder { [0]: CodeceptJS.I }
  type Methods (line 7) | interface Methods extends CodeceptJS.Playwright {}
  type I (line 8) | interface I extends ReturnType<steps_file> {}
  type Actions (line 10) | interface Actions {}

FILE: test/TestComponent.js
  class TestComponent (line 1) | class TestComponent extends HTMLElement {
    method constructor (line 3) | constructor({ childClassName = 'test-child', childTextContent = 'Child...
    method connectedCallback (line 12) | connectedCallback() {
    method add (line 17) | add(child) {
    method addNested (line 21) | addNested(child) {
    method observedAttributes (line 25) | static get observedAttributes() {
    method attributeChangedCallback (line 29) | attributeChangedCallback(name, oldValue, newValue) {

FILE: test/basic.spec.js
  function setup (line 11) | function setup() {

FILE: test/codeceptjs/steps.d.ts
  type steps_file (line 2) | type steps_file = typeof import('./steps_file.js');
  type SupportObject (line 5) | interface SupportObject { I: CodeceptJS.I }
  type CallbackOrder (line 6) | interface CallbackOrder { [0]: CodeceptJS.I }
  type Methods (line 7) | interface Methods extends CodeceptJS.Playwright {}
  type I (line 8) | interface I extends ReturnType<steps_file> {}
  type Actions (line 10) | interface Actions {}

FILE: test/createTestComponent.js
  function createTestComponent (line 2) | function createTestComponent(parent, options = {}) {
  function createNestedComponent (line 14) | function createNestedComponent(parent, count = 1, { createChildren = () ...
  constant COMPONENT_NAME (line 42) | const COMPONENT_NAME = 'test-component';

FILE: test/nopolyfills.spec.js
  function setup (line 7) | function setup() {

FILE: test/protractor-locator.e2e.js
  function pageFromTemplate (line 41) | function pageFromTemplate(template /* string */) /* string */ {
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (81K chars).
[
  {
    "path": ".babelrc",
    "chars": 169,
    "preview": "{\n    \"presets\": [\n      [\"@babel/env\", {\n        \"targets\": {\n          \"browsers\": [\"last 2 versions\", \"IE >= 11\"]\n   "
  },
  {
    "path": ".eslintrc.js",
    "chars": 455,
    "preview": "module.exports = {\n    \"env\": {\n        \"browser\": true,\n        \"es2021\": true,\n        \"node\": true,\n    },\n    \"exten"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 681,
    "preview": "# These are supported funding model platforms\nko_fi: griffadev\ngithub: # Replace with up to 4 GitHub Sponsors-enabled us"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 722,
    "preview": "---\nname: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    stra"
  },
  {
    "path": ".gitignore",
    "chars": 90,
    "preview": "# IDEs\n.vscode\n.idea\n\n# Build artifacts\nnode_modules\ndist\ncoverage\n.DS_Store\n**/output/**\n"
  },
  {
    "path": ".npmignore",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".nvmrc",
    "chars": 9,
    "preview": "v14.21.3\n"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2020 George Griffiths\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 9719,
    "preview": "[![Build Status](https://travis-ci.org/Georgegriff/query-selector-shadow-dom.svg?branch=main)](https://travis-ci.org/Geo"
  },
  {
    "path": "codecept.conf.js",
    "chars": 641,
    "preview": "const { setHeadlessWhen } = require('@codeceptjs/configure');\n\n// turn on headless mode when running with HEADLESS=true "
  },
  {
    "path": "demo.js",
    "chars": 291,
    "preview": "// Paste this code into chrome://downloads console to try it out :)\n\nconsole.log(querySelectorDeep('downloads-item:nth-c"
  },
  {
    "path": "examples/playwright/custom-engine.js",
    "chars": 648,
    "preview": "const { selectorEngine } = require(\"query-selector-shadow-dom/plugins/playwright\");\nconst playwright = require('playwrig"
  },
  {
    "path": "examples/puppeteer/clicking-elements.js",
    "chars": 1021,
    "preview": "const puppeteer = require('puppeteer');\nconst {  QueryHandler } = require(\"query-selector-shadow-dom/plugins/puppeteer\")"
  },
  {
    "path": "examples/puppeteer/custom-engine.js",
    "chars": 746,
    "preview": "const {  QueryHandler } = require(\"query-selector-shadow-dom/plugins/puppeteer\");\nconst puppeteer = require('puppeteer')"
  },
  {
    "path": "examples/puppeteer/multiple-elements.js",
    "chars": 882,
    "preview": "const puppeteer = require('puppeteer');\nconst {  QueryHandler } = require(\"query-selector-shadow-dom/plugins/puppeteer\")"
  },
  {
    "path": "examples/puppeteer/typing-to-elements.js",
    "chars": 770,
    "preview": "const puppeteer = require('puppeteer');\nconst {  QueryHandler } = require(\"query-selector-shadow-dom/plugins/puppeteer\")"
  },
  {
    "path": "examples/webdriverio/deeply-nested.js",
    "chars": 772,
    "preview": "const { remote } = require('webdriverio')\nconst { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriv"
  },
  {
    "path": "examples/webdriverio/multiple-elements.js",
    "chars": 855,
    "preview": "const { remote } = require('webdriverio')\nconst { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriv"
  },
  {
    "path": "examples/webdriverio/typing-to-elements.js",
    "chars": 774,
    "preview": "const { remote } = require('webdriverio')\nconst { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriv"
  },
  {
    "path": "jsconfig.json",
    "chars": 50,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowJs\": true\n  }\n}"
  },
  {
    "path": "karma.common.js",
    "chars": 3821,
    "preview": "const babel = require('rollup-plugin-babel');\nconst babelrc = require('babelrc-rollup').default;\nconst istanbul = requir"
  },
  {
    "path": "karma.conf.js",
    "chars": 182,
    "preview": "const KarmaConfig = require('./karma.common.js');\nmodule.exports = function(config) {\n\n    config.set(KarmaConfig({\n    "
  },
  {
    "path": "package.json",
    "chars": 2083,
    "preview": "{\n  \"name\": \"query-selector-shadow-dom\",\n  \"version\": \"1.0.1\",\n  \"description\": \"use querySelector syntax to search for "
  },
  {
    "path": "plugins/codeceptjs/README.md",
    "chars": 2001,
    "preview": "# Support for Shadow DOM in CodeceptJS\n\n- Supported CodeceptJS Helpers: *Playwright* only.\n- CodeceptJS 2.6.0 released w"
  },
  {
    "path": "plugins/codeceptjs/index.js",
    "chars": 1169,
    "preview": "const { selectorEngine } = require(\"../playwright\");\nconst supportedHelpers = [\n    'Playwright'\n]\nconst playwright = re"
  },
  {
    "path": "plugins/playwright/index.js",
    "chars": 928,
    "preview": "\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\n// load the library in UMD format which self executes and adds"
  },
  {
    "path": "plugins/protractor/index.d.ts",
    "chars": 332,
    "preview": "declare module 'protractor' {\n    import { Locator } from 'protractor';\n\n    export interface ProtractorBy {\n        /**"
  },
  {
    "path": "plugins/protractor/index.js",
    "chars": 649,
    "preview": "const fs = require('fs');\nconst path = require('path');\nconst querySelectorAllDeep = fs.readFileSync(path.resolve(__dirn"
  },
  {
    "path": "plugins/puppeteer/index.js",
    "chars": 586,
    "preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst querySelectorShadowDomUMD = fs.readFileSync(path.resolve("
  },
  {
    "path": "plugins/webdriverio/index.js",
    "chars": 380,
    "preview": "const fs = require('fs');\nconst path = require('path');\nconst querySelectorAllDeep = fs.readFileSync(path.resolve(__dirn"
  },
  {
    "path": "protractor.conf.js",
    "chars": 952,
    "preview": "exports.config = {\n    baseUrl: 'http://localhost:3000',\n\n    chromeDriver: process.env.CHROMEDRIVER_FILEPATH || require"
  },
  {
    "path": "puppeteer-es5.js",
    "chars": 840,
    "preview": "const puppeteer = require('puppeteer');\nconst path = require('path');\nconst fs = require('fs');\n(async() => {\n    try {\n"
  },
  {
    "path": "rollup.config.js",
    "chars": 727,
    "preview": "import babel from 'rollup-plugin-babel';\nimport babelrc from 'babelrc-rollup';\nimport { terser } from 'rollup-plugin-ter"
  },
  {
    "path": "src/normalize.js",
    "chars": 4386,
    "preview": "/* istanbul ignore file */\n\n\n// normalize-selector-rev-02.js\n/*\n  author: kyle simpson (@getify)\n  original source: http"
  },
  {
    "path": "src/querySelectorDeep.js",
    "chars": 6572,
    "preview": "import { normalizeSelector } from './normalize';\n\n/**\n* Finds first matching elements on the page that may be in a shado"
  },
  {
    "path": "steps.d.ts",
    "chars": 373,
    "preview": "/// <reference types='codeceptjs' />\ntype steps_file = typeof import('./steps_file.js');\n\ndeclare namespace CodeceptJS {"
  },
  {
    "path": "steps_file.js",
    "chars": 266,
    "preview": "// in this file you can append custom step methods to 'I' object\n\nmodule.exports = function() {\n  return actor({\n\n    //"
  },
  {
    "path": "test/TestComponent.js",
    "chars": 1381,
    "preview": "export class TestComponent extends HTMLElement {\n\n    constructor({ childClassName = 'test-child', childTextContent = 'C"
  },
  {
    "path": "test/basic.spec.js",
    "chars": 18577,
    "preview": "import { querySelectorAllDeep, querySelectorDeep, collectAllElementsDeep } from '../src/querySelectorDeep.js';\nimport { "
  },
  {
    "path": "test/codeceptjs/README.md",
    "chars": 931,
    "preview": "CodeceptJS functions to test:\n\nappendField\nattachFile\ncheckOption\nclearField\nclick\nclickLin\ndontSee\ndontSeeCheckboxIsChe"
  },
  {
    "path": "test/codeceptjs/codecept.conf.js",
    "chars": 411,
    "preview": "exports.config = {\n  tests: 'test/codeceptjs/*.test.js',\n  output: './output',\n  helpers: {\n    Playwright: {\n      url:"
  },
  {
    "path": "test/codeceptjs/components.test.js",
    "chars": 65,
    "preview": "Feature('components');\n\nScenario('test something', (I) => {\n\n});\n"
  },
  {
    "path": "test/codeceptjs/jsconfig.json",
    "chars": 50,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowJs\": true\n  }\n}"
  },
  {
    "path": "test/codeceptjs/steps.d.ts",
    "chars": 373,
    "preview": "/// <reference types='codeceptjs' />\ntype steps_file = typeof import('./steps_file.js');\n\ndeclare namespace CodeceptJS {"
  },
  {
    "path": "test/codeceptjs/steps_file.js",
    "chars": 266,
    "preview": "// in this file you can append custom step methods to 'I' object\n\nmodule.exports = function() {\n  return actor({\n\n    //"
  },
  {
    "path": "test/createTestComponent.js",
    "chars": 1332,
    "preview": "import { TestComponent } from './TestComponent.js';\nexport function createTestComponent(parent, options = {}) {\n    retu"
  },
  {
    "path": "test/index.html",
    "chars": 1712,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-widt"
  },
  {
    "path": "test/nopolyfills.spec.js",
    "chars": 1802,
    "preview": "import { querySelectorAllDeep, querySelectorDeep } from '../src/querySelectorDeep.js';\n\ndescribe(\"No Polyfills Suite\", f"
  },
  {
    "path": "test/protractor-locator.e2e.js",
    "chars": 1897,
    "preview": "describe('query-selector-shadow-dom', () => {\n\n    beforeEach(async () => {\n        await browser.get(pageFromTemplate(`"
  }
]

About this extraction

This page contains the full source code of the Georgegriff/query-selector-shadow-dom GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (73.6 KB), approximately 18.0k tokens, and a symbol index with 35 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!