DOM Selector
A CSS selector engine.
Install
npm i @asamuzakjp/dom-selector
Usage
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
const {
  closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
matches(selector, node, opt)
matches - equivalent to Element.matches()
Parameters
Returns boolean true if matched, false otherwise
closest(selector, node, opt)
closest - equivalent to Element.closest()
Parameters
Returns object? matched node
querySelector(selector, node, opt)
querySelector - equivalent to Document.querySelector(), DocumentFragment.querySelector() and Element.querySelector()
Parameters
selectorstring CSS selectornodeobject Document, DocumentFragment or Element nodeoptobject? options
Returns object? matched node
querySelectorAll(selector, node, opt)
querySelectorAll - equivalent to Document.querySelectorAll(), DocumentFragment.querySelectorAll() and Element.querySelectorAll()
NOTE: returns Array, not NodeList
Parameters
selectorstring CSS selectornodeobject Document, DocumentFragment or Element nodeoptobject? options
Returns Array<(object | undefined)> array of matched nodes
Monkey patch jsdom
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const dom = new JSDOM('', {
  runScripts: 'dangerously',
  url: 'http://localhost/',
  beforeParse: window => {
    const domSelector = new DOMSelector(window);
    const matches = domSelector.matches.bind(domSelector);
    window.Element.prototype.matches = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return matches(selector, this);
    };
    const closest = domSelector.closest.bind(domSelector);
    window.Element.prototype.closest = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return closest(selector, this);
    };
    const querySelector = domSelector.querySelector.bind(domSelector);
    window.Document.prototype.querySelector = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelector(selector, this);
    };
    window.DocumentFragment.prototype.querySelector = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelector(selector, this);
    };
    window.Element.prototype.querySelector = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelector(selector, this);
    };
    const querySelectorAll = domSelector.querySelectorAll.bind(domSelector);
    window.Document.prototype.querySelectorAll = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelectorAll(selector, this);
    };
    window.DocumentFragment.prototype.querySelectorAll = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelectorAll(selector, this);
    };
    window.Element.prototype.querySelectorAll = function (...args) {
      if (!args.length) {
        throw new window.TypeError('1 argument required, but only 0 present.');
      }
      const [selector] = args;
      return querySelectorAll(selector, this);
    };
  }
});
Supported CSS selectors
| Pattern | Supported | Note | ||
|---|---|---|---|---|
| * | ✓ | |||
| E | ✓ | |||
| ns\ | E | ✓ | ||
| *\ | E | ✓ | ||
| \ | E | ✓ | ||
| E F | ✓ | |||
| E > F | ✓ | |||
| E + F | ✓ | |||
| E ~ F | ✓ | |||
| F \ | \ | E | Unsupported | |
| E.warning | ✓ | |||
| E#myid | ✓ | |||
| E[foo] | ✓ | |||
| E[foo="bar"] | ✓ | |||
| E[foo="bar" i] | ✓ | |||
| E[foo="bar" s] | ✓ | |||
| E[foo~="bar"] | ✓ | |||
| E[foo^="bar"] | ✓ | |||
| E[foo$="bar"] | ✓ | |||
| E[foo*="bar"] | ✓ | |||
| E[foo\ | ="en"] | ✓ | ||
| E:is(s1, s2, …) | ✓ | |||
| E:not(s1, s2, …) | ✓ | |||
| E:where(s1, s2, …) | ✓ | |||
| E:has(rs1, rs2, …) | ✓ | |||
| E:defined | Partially supported | Matching with MathML is not yet supported. | ||
| E:dir(ltr) | ✓ | |||
| E:lang(en) | ✓ | |||
| E:any‑link | ✓ | |||
| E:link | ✓ | |||
| E:visited | ✓ | Returns false or null to prevent fingerprinting. | 
||
| E:local‑link | ✓ | |||
| E:target | ✓ | |||
| E:target‑within | ✓ | |||
| E:scope | ✓ | |||
| E:hover | ✓ | |||
| E:active | ✓ | |||
| E:focus | ✓ | |||
| E:focus‑visible | ✓ | |||
| E:focus‑within | ✓ | |||
| E:current | Unsupported | |||
| E:current(s) | Unsupported | |||
| E:past | Unsupported | |||
| E:future | Unsupported | |||
| E:open E:closed  | 
Partially supported | Matching with <select>, e.g. select:open, is not supported. | 
||
| E:popover-open | ✓ | |||
| E:enabled E:disabled  | 
✓ | |||
| E:read‑write E:read‑only  | 
✓ | |||
| E:placeholder‑shown | ✓ | |||
| E:default | ✓ | |||
| E:checked | ✓ | |||
| E:indeterminate | ✓ | |||
| E:blank | Unsupported | |||
| E:valid E:invalid  | 
✓ | |||
| E:in-range E:out-of-range  | 
✓ | |||
| E:required E:optional  | 
✓ | |||
| E:user‑valid E:user‑invalid  | 
Unsupported | |||
| E:root | ✓ | |||
| E:empty | ✓ | |||
| E:nth‑child(n [of S]?) | ✓ | |||
| E:nth‑last‑child(n [of S]?) | ✓ | |||
| E:first‑child | ✓ | |||
| E:last‑child | ✓ | |||
| E:only‑child | ✓ | |||
| E:nth‑of‑type(n) | ✓ | |||
| E:nth‑last‑of‑type(n) | ✓ | |||
| E:first‑of‑type | ✓ | |||
| E:last‑of‑type | ✓ | |||
| E:only‑of‑type | ✓ | |||
| E:nth‑col(n) | Unsupported | |||
| E:nth‑last‑col(n) | Unsupported | |||
| CE:state(v) | ✓ | *1 | ||
| :host | ✓ | |||
| :host(s) | ✓ | |||
| :host(:state(v)) | ✓ | *1 | ||
| :host:has(rs1, rs2, ...) | ✓ | |||
| :host(s):has(rs1, rs2, ...) | ✓ | |||
| :host‑context(s) | ✓ | |||
| :host‑context(s):has(rs1, rs2, ...) | ✓ | |||
| & | ✓ | Only supports outermost &, i.e. equivalent to :scope | 
*1: ElementInternals.states, i.e. CustomStateSet, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
class LabeledCheckbox extends window.HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    // patch CustomStateSet
    if (!this.#internals.states) {
      this.#internals.states = new Set();
    }
    this.addEventListener('click', this._onClick.bind(this));
  }
  get checked() {
    return this.#internals.states.has('checked');
  }
  set checked(flag) {
    if (flag) {
      this.#internals.states.add('checked');
    } else {
      this.#internals.states.delete('checked');
    }
  }
  _onClick(event) {
    this.checked = !this.checked;
  }
}
Performance
See benchmark for the latest results.
Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)