Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: feat(query-deep): Using a query selector that supports queries into the shadow dom of elements #1069

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions src/__node_tests__/index.js
Original file line number Diff line number Diff line change
@@ -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(`
<html>
Expand Down Expand Up @@ -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(`
<html>
<body>
<example-input></example-input>
</body>
</html>
`).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(`
<input
id=invisible-from-outer-dom
/>
`)
})

test('byRole works without a global DOM', () => {
const {
window: {
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/ariaAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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([
Expand Down
6 changes: 4 additions & 2 deletions src/label-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {querySelector, querySelectorAll} from './queries/all-utils'
import {TEXT_NODE} from './helpers'

const labelledNodeNames = [
Expand Down Expand Up @@ -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)
}

Expand All @@ -63,7 +64,8 @@ function getLabels(
const labelsId = ariaLabelledBy ? ariaLabelledBy.split(' ') : []
return labelsId.length
? labelsId.map(labelId => {
const labellingElement = container.querySelector<HTMLElement>(
const labellingElement = querySelector<Element, HTMLElement>(
container,
`[id="${labelId}"]`,
)
return labellingElement
Expand Down
6 changes: 5 additions & 1 deletion src/queries/display-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MatcherOptions,
} from '../../types'
import {
querySelectorAll,
getNodeText,
matches,
fuzzyMatches,
Expand All @@ -23,7 +24,10 @@ const queryAllByDisplayValue: AllByBoundAttribute = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
container.querySelectorAll<HTMLElement>(`input,textarea,select`),
querySelectorAll<HTMLElement, HTMLElement>(
container,
`input,textarea,select`,
),
).filter(node => {
if (node.tagName === 'SELECT') {
const selectedOptions = Array.from(
Expand Down
13 changes: 10 additions & 3 deletions src/queries/label-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>('label,input'))
return Array.from(
querySelectorAll<HTMLElement, HTMLElement>(container, 'label,input'),
)
.map(node => {
return {node, textToMatch: getLabelContent(node)}
})
Expand Down Expand Up @@ -56,7 +60,7 @@ const queryAllByLabelText: AllByText = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
const matchingLabelledElements = Array.from(
container.querySelectorAll<HTMLElement>('*'),
querySelectorAll<HTMLElement, HTMLElement>(container, '*'),
)
.filter(element => {
return (
Expand Down Expand Up @@ -169,7 +173,10 @@ function getTagNameOfElementAssociatedWithLabelViaFor(
return null
}

const element = container.querySelector(`[id="${htmlFor}"]`)
const element = querySelector<Element, HTMLElement>(
container,
`[id="${htmlFor}"]`,
)
return element ? element.tagName.toLowerCase() : null
}

Expand Down
4 changes: 3 additions & 1 deletion src/queries/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getConfig,
makeNormalizer,
matches,
querySelectorAll,
} from './all-utils'

function queryAllByRole(
Expand Down Expand Up @@ -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),
),
Expand Down
5 changes: 4 additions & 1 deletion src/queries/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {checkContainerType} from '../helpers'
import {DEFAULT_IGNORE_TAGS} from '../shared'
import {AllByText, GetErrorFunction} from '../../types'
import {
querySelectorAll,
fuzzyMatches,
matches,
makeNormalizer,
Expand Down Expand Up @@ -32,7 +33,9 @@ const queryAllByText: AllByText = (
return (
[
...baseArray,
...Array.from(container.querySelectorAll<HTMLElement>(selector)),
...Array.from(
querySelectorAll<HTMLElement, HTMLElement>(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))
Expand Down
6 changes: 5 additions & 1 deletion src/queries/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MatcherOptions,
} from '../../types'
import {
querySelectorAll,
fuzzyMatches,
matches,
makeNormalizer,
Expand All @@ -27,7 +28,10 @@ const queryAllByTitle: AllByBoundAttribute = (
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
container.querySelectorAll<HTMLElement>('[title], svg > title'),
querySelectorAll<HTMLElement, HTMLElement>(
container,
'[title], svg > title',
),
).filter(
node =>
matcher(node.getAttribute('title'), node, text, matchNormalizer) ||
Expand Down
14 changes: 13 additions & 1 deletion src/query-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {
GetErrorFunction,
Matcher,
MatcherOptions,
QueryAllElements,
QueryElement,
QueryMethod,
Variant,
waitForOptions as WaitForOptions,
Expand All @@ -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 = <T extends Element>(
element: T,
selector: string,
) => querier.querySelectorDeep(selector, element)
export const querySelectorAll: QueryAllElements = <T extends Element>(
element: T,
selector: string,
) => querier.querySelectorAllDeep(selector, element)

function getElementError(message: string | null, container: HTMLElement) {
return getConfig().getElementError(message, container)
Expand All @@ -35,7 +47,7 @@ function queryAllByAttribute(
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
return Array.from(
container.querySelectorAll<HTMLElement>(`[${attribute}]`),
querySelectorAll<HTMLElement, HTMLElement>(container, `[${attribute}]`),
).filter(node =>
matcher(node.getAttribute(attribute), node, text, matchNormalizer),
)
Expand Down
1 change: 1 addition & 0 deletions tests/jest.config.dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
'/__tests__/',
'/__node_tests__/',
],
transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],
testEnvironment: 'jest-environment-jsdom',
}
1 change: 1 addition & 0 deletions tests/jest.config.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ module.exports = {
'/__tests__/',
'/__node_tests__/',
],
transformIgnorePatterns: ['node_modules/(?!(query-selector-shadow-dom)/)'],
testMatch: ['**/__node_tests__/**.js'],
}
24 changes: 24 additions & 0 deletions types/query-helpers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,27 @@ export function buildQueries<Arguments extends any[]>(
getMultipleError: GetErrorFunction<Arguments>,
getMissingError: GetErrorFunction<Arguments>,
): BuiltQueryMethods<Arguments>

export type QueryElement = {
<T, K extends keyof HTMLElementTagNameMap>(container: T, selectors: K):
| HTMLElementTagNameMap[K]
| null
<T, K extends keyof SVGElementTagNameMap>(container: T, selectors: K):
| SVGElementTagNameMap[K]
| null
<T, E extends Element = Element>(container: T, selectors: string): E | null
}
export type QueryAllElements = {
<T, K extends keyof HTMLElementTagNameMap>(
container: T,
selectors: K,
): NodeListOf<HTMLElementTagNameMap[K]>
<T, K extends keyof SVGElementTagNameMap>(
container: T,
selectors: K,
): NodeListOf<SVGElementTagNameMap[K]>
<T, E extends Element = Element>(
container: T,
selectors: string,
): NodeListOf<E>
}
4 changes: 4 additions & 0 deletions types/query-selector-shadow-dom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'query-selector-shadow-dom' {
export const querySelectorAllDeep: QueryElement
export const querySelectorDeep: QueryAllElements
}