Skip to content

Commit

Permalink
fix: Shadow DOM support when root is not defined
Browse files Browse the repository at this point in the history
  • Loading branch information
fczbkk committed Jan 2, 2022
1 parent ebf880a commit 38f2a69
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 19 deletions.
9 changes: 9 additions & 0 deletions src/utilities-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const libraryName = 'CssSelectorGenerator'

/**
* Convenient wrapper for `console.warn` using consistent formatting.
*/
export function showWarning (id = 'unknown problem', ...args: unknown[]): void {
// eslint-disable-next-line no-console
console.warn(`${libraryName}: ${id}`, ...args)
}
43 changes: 30 additions & 13 deletions src/utilities-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import {
CssSelectorGeneratorOptions,
CssSelectorMatch,
CssSelectorType,
CssSelectorTypes
CssSelectorTypes,
} from './types'
import {isEnumValue} from './utilities-typescript'
import {showWarning} from './utilities-messages'

export const DEFAULT_OPTIONS = {
selectors: [
CssSelectorType.id,
CssSelectorType.class,
CssSelectorType.tag,
CssSelectorType.attribute
CssSelectorType.attribute,
] as CssSelectorTypes,
// if set to true, always include tag name
includeTag: false,
Expand All @@ -21,7 +22,7 @@ export const DEFAULT_OPTIONS = {
combineBetweenSelectors: true,
root: null,
maxCombinations: Number.POSITIVE_INFINITY,
maxCandidates: Number.POSITIVE_INFINITY
maxCandidates: Number.POSITIVE_INFINITY,
} as CssSelectorGeneratorOptions

/**
Expand Down Expand Up @@ -54,7 +55,7 @@ export function isCssSelectorMatch (input: unknown): input is CssSelectorMatch {
* Converts input to a list of valid values for whitelist or blacklist.
*/
export function sanitizeCssSelectorMatchList (
input: unknown
input: unknown,
): Array<CssSelectorMatch> {
if (!Array.isArray(input)) {
return []
Expand All @@ -75,19 +76,35 @@ export function isNode (input: unknown): input is Node {
export function isParentNode (input: unknown): input is ParentNode {
const validNodeTypes = [
Node.DOCUMENT_NODE,
Node.DOCUMENT_FRAGMENT_NODE,
Node.ELEMENT_NODE
Node.DOCUMENT_FRAGMENT_NODE, // this includes Shadow DOM root
Node.ELEMENT_NODE,
]
return isNode(input) && validNodeTypes.includes(input.nodeType)
}

/**
* Makes sure that the root node in options is valid.
*/
export function sanitizeRoot (input: unknown, element: Element): ParentNode {
return isParentNode(input)
? input
: element.ownerDocument.querySelector(':root')
export function sanitizeRoot (
input: unknown,
element: Element,
): ParentNode {
if (isParentNode(input)) {
if (!input.contains(element)) {
showWarning('element root mismatch', 'Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended.')
}
return input
}

const rootNode = element.getRootNode({composed: false})
if (isParentNode(rootNode)) {
if (rootNode !== document) {
showWarning('shadow root inferred', 'You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended.')
}
return rootNode
}

return element.ownerDocument.querySelector(':root')
}

/**
Expand All @@ -103,11 +120,11 @@ export function sanitizeMaxNumber (input?: unknown): number {
*/
export function sanitizeOptions (
element: Element,
custom_options = {}
custom_options = {},
): CssSelectorGeneratorOptions {
const options = {
...DEFAULT_OPTIONS,
...custom_options
...custom_options,
}

return {
Expand All @@ -119,6 +136,6 @@ export function sanitizeOptions (
combineBetweenSelectors: !!options.combineBetweenSelectors,
includeTag: !!options.includeTag,
maxCombinations: sanitizeMaxNumber(options.maxCombinations),
maxCandidates: sanitizeMaxNumber(options.maxCandidates)
maxCandidates: sanitizeMaxNumber(options.maxCandidates),
}
}
49 changes: 49 additions & 0 deletions test/sanitize-root.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {assert} from 'chai'
import {sanitizeRoot} from '../src/utilities-options.ts'

describe('utilities - sanitizeRoot', () => {
let root

beforeEach(() => {
root = document.body.appendChild(document.createElement('div'))
})

afterEach(() => {
root.parentNode.removeChild(root)
})

it('should return provided node if it is a document', () => {
const element = root.appendChild(document.createElement('div'))
const result = sanitizeRoot(document, element)
assert.equal(result, document)
})

it('should return provided node if it is an element', () => {
const element = root.appendChild(document.createElement('div'))
const result = sanitizeRoot(root, element)
assert.equal(result, root)
})

it('should return provided node if it is a fragment', () => {
const fragment = root.appendChild(document.createDocumentFragment())
const element = fragment.appendChild(document.createElement('div'))
const result = sanitizeRoot(fragment, element)
assert.equal(result, fragment)
})

it('should return document root if not provided', () => {
const element = root.appendChild(document.createElement('div'))
const result = sanitizeRoot(undefined, element)
assert.equal(result, document)
})

it('should return shadow root if element is part of shadow DOM', () => {
const shadowRoot = root.attachShadow({mode: 'open'})
const wrapper = shadowRoot.appendChild(document.createElement('div'))
wrapper.className = 'wrapper'
const element = wrapper.appendChild(document.createElement('div'))
element.className = 'element'
const result = sanitizeRoot(undefined, element)
assert.equal(result, shadowRoot)
})
})
6 changes: 0 additions & 6 deletions test/selector-id.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ describe('selector - ID', function () {
assert.deepEqual(getIdSelector([root.firstElementChild]), ['#aaa\\+bbb'])
})

it.skip('should escape UTF8 characters', function () {
// TODO
root.innerHTML = '<div id="aaa✓bbb"></div>'
assert.deepEqual(getIdSelector([root.firstElementChild]), ['aaa\\u2713bbb'])
})

it('should escape colon character', function () {
root.innerHTML = '<div id="aaa:bbb"></div>'
assert.deepEqual(getIdSelector([root.firstElementChild]), ['#aaa\\:bbb'])
Expand Down
29 changes: 29 additions & 0 deletions test/shadow-dom.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {assert} from 'chai'
import {getCssSelector} from '../src/index.ts'

describe('Shadow DOM', () => {
let root
let shadowRoot
let shadowElement

beforeEach(() => {
root = document.body.appendChild(document.createElement('div'))
shadowRoot = root.attachShadow({mode: 'open'})
shadowElement = shadowRoot.appendChild(document.createElement('div'))
shadowElement.className = 'shadowElement'
})

afterEach(() => {
root.parentNode.removeChild(root)
})

it('should match shadow element within shadow root', () => {
const result = getCssSelector(shadowElement, {root: shadowRoot})
assert.equal(result, '.shadowElement')
})

it('should match shadow element without specifying root', () => {
const result = getCssSelector(shadowElement)
assert.equal(result, '.shadowElement')
})
})

0 comments on commit 38f2a69

Please sign in to comment.