diff --git a/jest.config.js b/jest.config.js
index 7a7a2b50..d4ea4309 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -17,6 +17,7 @@ module.exports = {
...watchPlugins,
require.resolve('jest-watch-select-projects'),
],
+ transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],
projects: [
require.resolve('./tests/jest.config.dom.js'),
require.resolve('./tests/jest.config.node.js'),
diff --git a/package.json b/package.json
index 02d084af..47dd6524 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,8 @@
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4",
- "pretty-format": "^27.0.2"
+ "pretty-format": "^27.0.2",
+ "query-selector-shadow-dom": "^1.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.6",
diff --git a/src/__node_tests__/index.js b/src/__node_tests__/index.js
index 6c663360..bdbf91de 100644
--- a/src/__node_tests__/index.js
+++ b/src/__node_tests__/index.js
@@ -1,6 +1,13 @@
import {JSDOM} from 'jsdom'
import * as dtl from '../'
+beforeEach(() => {
+ const dom = new JSDOM()
+ global.document = dom.window.document
+ global.window = dom.window
+ global.Node = dom.window.Node
+})
+
test('works without a global dom', async () => {
const container = new JSDOM(`
@@ -77,6 +84,60 @@ test('works without a browser context on a dom node (JSDOM Fragment)', () => {
`)
})
+test('works with a custom configured element query for shadow dom elements', async () => {
+ const window = new JSDOM(`
+
+
+
+
+
+ `).window
+ const document = window.document
+ const container = document.body
+
+ // Given I have defined a component with shadow dom
+ window.customElements.define(
+ 'example-input',
+ class extends window.HTMLElement {
+ constructor() {
+ super()
+ const shadow = this.attachShadow({mode: 'open'})
+
+ const div = document.createElement('div')
+ const label = document.createElement('label')
+ label.setAttribute('for', 'invisible-from-outer-dom')
+ label.innerHTML =
+ 'Visible in browser, invisible for traditional queries'
+ const input = document.createElement('input')
+ input.setAttribute('id', 'invisible-from-outer-dom')
+ div.appendChild(label)
+ div.appendChild(input)
+ shadow.appendChild(div)
+ }
+ },
+ )
+
+ // Then it is part of the document
+ expect(
+ dtl.queryByLabelText(
+ container,
+ /Visible in browser, invisible for traditional queries/i,
+ ),
+ ).toBeInTheDocument()
+
+ // And it returns the expected item
+ expect(
+ dtl.getByLabelText(
+ container,
+ /Visible in browser, invisible for traditional queries/i,
+ ),
+ ).toMatchInlineSnapshot(`
+
+ `)
+})
+
test('byRole works without a global DOM', () => {
const {
window: {
diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js
index 1ceb210d..eab7a936 100644
--- a/src/__tests__/ariaAttributes.js
+++ b/src/__tests__/ariaAttributes.js
@@ -131,15 +131,15 @@ test('`selected: true` matches `aria-selected="true"` on supported roles', () =>
expect(
getAllByRole('columnheader', {selected: true}).map(({id}) => id),
- ).toEqual(['selected-native-columnheader', 'selected-columnheader'])
+ ).toEqual(['selected-columnheader', 'selected-native-columnheader'])
expect(getAllByRole('gridcell', {selected: true}).map(({id}) => id)).toEqual([
'selected-gridcell',
])
expect(getAllByRole('option', {selected: true}).map(({id}) => id)).toEqual([
- 'selected-native-option',
'selected-listbox-option',
+ 'selected-native-option',
])
expect(getAllByRole('rowheader', {selected: true}).map(({id}) => id)).toEqual(
@@ -217,8 +217,8 @@ test('`level` matches elements with `heading` role', () => {
])
expect(getAllByRole('heading', {level: 2}).map(({id}) => id)).toEqual([
- 'first-heading-two',
'second-heading-two',
+ 'first-heading-two',
])
expect(getAllByRole('heading', {level: 3}).map(({id}) => id)).toEqual([
diff --git a/src/label-helpers.ts b/src/label-helpers.ts
index 4e1a3fed..ad6f223c 100644
--- a/src/label-helpers.ts
+++ b/src/label-helpers.ts
@@ -1,3 +1,4 @@
+import {querySelector, querySelectorAll} from './queries/all-utils'
import {TEXT_NODE} from './helpers'
const labelledNodeNames = [
@@ -43,7 +44,7 @@ function getRealLabels(element: Element) {
if (!isLabelable(element)) return []
- const labels = element.ownerDocument.querySelectorAll('label')
+ const labels = querySelectorAll(element.ownerDocument, 'label')
return Array.from(labels).filter(label => label.control === element)
}
@@ -63,7 +64,8 @@ function getLabels(
const labelsId = ariaLabelledBy ? ariaLabelledBy.split(' ') : []
return labelsId.length
? labelsId.map(labelId => {
- const labellingElement = container.querySelector(
+ const labellingElement = querySelector(
+ container,
`[id="${labelId}"]`,
)
return labellingElement
diff --git a/src/queries/display-value.ts b/src/queries/display-value.ts
index 7177a84a..1e373b2d 100644
--- a/src/queries/display-value.ts
+++ b/src/queries/display-value.ts
@@ -7,6 +7,7 @@ import {
MatcherOptions,
} from '../../types'
import {
+ querySelectorAll,
getNodeText,
matches,
fuzzyMatches,
@@ -23,7 +24,10 @@ const queryAllByDisplayValue: AllByBoundAttribute = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
- container.querySelectorAll(`input,textarea,select`),
+ querySelectorAll(
+ container,
+ `input,textarea,select`,
+ ),
).filter(node => {
if (node.tagName === 'SELECT') {
const selectedOptions = Array.from(
diff --git a/src/queries/label-text.ts b/src/queries/label-text.ts
index 39e766d5..a56f67f1 100644
--- a/src/queries/label-text.ts
+++ b/src/queries/label-text.ts
@@ -17,12 +17,16 @@ import {
makeSingleQuery,
wrapAllByQueryWithSuggestion,
wrapSingleQueryWithSuggestion,
+ querySelectorAll,
+ querySelector,
} from './all-utils'
function queryAllLabels(
container: HTMLElement,
): {textToMatch: string | null; node: HTMLElement}[] {
- return Array.from(container.querySelectorAll('label,input'))
+ return Array.from(
+ querySelectorAll(container, 'label,input'),
+ )
.map(node => {
return {node, textToMatch: getLabelContent(node)}
})
@@ -56,7 +60,7 @@ const queryAllByLabelText: AllByText = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
const matchingLabelledElements = Array.from(
- container.querySelectorAll('*'),
+ querySelectorAll(container, '*'),
)
.filter(element => {
return (
@@ -169,7 +173,10 @@ function getTagNameOfElementAssociatedWithLabelViaFor(
return null
}
- const element = container.querySelector(`[id="${htmlFor}"]`)
+ const element = querySelector(
+ container,
+ `[id="${htmlFor}"]`,
+ )
return element ? element.tagName.toLowerCase() : null
}
diff --git a/src/queries/role.js b/src/queries/role.js
index 826edd43..69c8bca7 100644
--- a/src/queries/role.js
+++ b/src/queries/role.js
@@ -20,6 +20,7 @@ import {
getConfig,
makeNormalizer,
matches,
+ querySelectorAll,
} from './all-utils'
function queryAllByRole(
@@ -100,7 +101,8 @@ function queryAllByRole(
}
return Array.from(
- container.querySelectorAll(
+ querySelectorAll(
+ container,
// Only query elements that can be matched by the following filters
makeRoleSelector(role, exact, normalizer ? matchNormalizer : undefined),
),
diff --git a/src/queries/text.ts b/src/queries/text.ts
index 0e6ac3f7..4d3e2644 100644
--- a/src/queries/text.ts
+++ b/src/queries/text.ts
@@ -3,6 +3,7 @@ import {checkContainerType} from '../helpers'
import {DEFAULT_IGNORE_TAGS} from '../shared'
import {AllByText, GetErrorFunction} from '../../types'
import {
+ querySelectorAll,
fuzzyMatches,
matches,
makeNormalizer,
@@ -32,7 +33,9 @@ const queryAllByText: AllByText = (
return (
[
...baseArray,
- ...Array.from(container.querySelectorAll(selector)),
+ ...Array.from(
+ querySelectorAll(container, selector),
+ ),
]
// TODO: `matches` according lib.dom.d.ts can get only `string` but according our code it can handle also boolean :)
.filter(node => !ignore || !node.matches(ignore as string))
diff --git a/src/queries/title.ts b/src/queries/title.ts
index 7366855f..1dd315f2 100644
--- a/src/queries/title.ts
+++ b/src/queries/title.ts
@@ -7,6 +7,7 @@ import {
MatcherOptions,
} from '../../types'
import {
+ querySelectorAll,
fuzzyMatches,
matches,
makeNormalizer,
@@ -27,7 +28,10 @@ const queryAllByTitle: AllByBoundAttribute = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
- container.querySelectorAll('[title], svg > title'),
+ querySelectorAll(
+ container,
+ '[title], svg > title',
+ ),
).filter(
node =>
matcher(node.getAttribute('title'), node, text, matchNormalizer) ||
diff --git a/src/query-helpers.ts b/src/query-helpers.ts
index 155210e1..5288ba9b 100644
--- a/src/query-helpers.ts
+++ b/src/query-helpers.ts
@@ -2,6 +2,8 @@ import type {
GetErrorFunction,
Matcher,
MatcherOptions,
+ QueryAllElements,
+ QueryElement,
QueryMethod,
Variant,
waitForOptions as WaitForOptions,
@@ -11,6 +13,16 @@ import {getSuggestedQuery} from './suggestions'
import {fuzzyMatches, matches, makeNormalizer} from './matches'
import {waitFor} from './wait-for'
import {getConfig} from './config'
+import * as querier from 'query-selector-shadow-dom'
+
+export const querySelector: QueryElement = (
+ element: T,
+ selector: string,
+) => querier.querySelectorDeep(selector, element)
+export const querySelectorAll: QueryAllElements = (
+ element: T,
+ selector: string,
+) => querier.querySelectorAllDeep(selector, element)
function getElementError(message: string | null, container: HTMLElement) {
return getConfig().getElementError(message, container)
@@ -35,7 +47,7 @@ function queryAllByAttribute(
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
- container.querySelectorAll(`[${attribute}]`),
+ querySelectorAll(container, `[${attribute}]`),
).filter(node =>
matcher(node.getAttribute(attribute), node, text, matchNormalizer),
)
diff --git a/tests/jest.config.dom.js b/tests/jest.config.dom.js
index 72b3b24b..ea330af4 100644
--- a/tests/jest.config.dom.js
+++ b/tests/jest.config.dom.js
@@ -10,5 +10,6 @@ module.exports = {
'/__tests__/',
'/__node_tests__/',
],
+ transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],
testEnvironment: 'jest-environment-jsdom',
}
diff --git a/tests/jest.config.node.js b/tests/jest.config.node.js
index bf37b60b..f72e6bfc 100644
--- a/tests/jest.config.node.js
+++ b/tests/jest.config.node.js
@@ -11,5 +11,6 @@ module.exports = {
'/__tests__/',
'/__node_tests__/',
],
+ transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],
testMatch: ['**/__node_tests__/**.js'],
}
diff --git a/types/query-helpers.d.ts b/types/query-helpers.d.ts
index 7b9904b1..8a9b150b 100644
--- a/types/query-helpers.d.ts
+++ b/types/query-helpers.d.ts
@@ -72,3 +72,27 @@ export function buildQueries(
getMultipleError: GetErrorFunction,
getMissingError: GetErrorFunction,
): BuiltQueryMethods
+
+export type QueryElement = {
+ (container: T, selectors: K):
+ | HTMLElementTagNameMap[K]
+ | null
+ (container: T, selectors: K):
+ | SVGElementTagNameMap[K]
+ | null
+ (container: T, selectors: string): E | null
+}
+export type QueryAllElements = {
+ (
+ container: T,
+ selectors: K,
+ ): NodeListOf
+ (
+ container: T,
+ selectors: K,
+ ): NodeListOf
+ (
+ container: T,
+ selectors: string,
+ ): NodeListOf
+}
diff --git a/types/query-selector-shadow-dom.d.ts b/types/query-selector-shadow-dom.d.ts
new file mode 100644
index 00000000..f92a50df
--- /dev/null
+++ b/types/query-selector-shadow-dom.d.ts
@@ -0,0 +1,4 @@
+declare module 'query-selector-shadow-dom' {
+ export const querySelectorAllDeep: QueryElement
+ export const querySelectorDeep: QueryAllElements
+}