Skip to content

Commit fe05919

Browse files
feat(queryconfig): Allows configuring a different element query mechanism
Commit opens the possibility to configure the api to use more sophisticated queries on elements, for instance enabling shadow dom specific queries via libraries like https://github.com/Georgegriff/query-selector-shadow-dom
1 parent 8edfad0 commit fe05919

File tree

11 files changed

+146
-18
lines changed

11 files changed

+146
-18
lines changed

src/__node_tests__/index.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {configure} from '../config'
12
import {JSDOM} from 'jsdom'
23
import * as dtl from '../'
34

@@ -77,6 +78,52 @@ test('works without a browser context on a dom node (JSDOM Fragment)', () => {
7778
`)
7879
})
7980

81+
test('works with a custom configured element query', () => {
82+
const container = JSDOM.fragment(`
83+
<html>
84+
<body>
85+
<form id="login-form">
86+
<label for="username">Username</label>
87+
<input id="username" />
88+
<label for="password">Password</label>
89+
<input id="password" type="password" />
90+
<button type="submit">Submit</button>
91+
<div id="data-container"></div>
92+
</form>
93+
<form id="other">
94+
<label for="user_other">Username</label>
95+
<input id="user_other" />
96+
<label for="pass_other">Password</label>
97+
<input id="pass_other" type="password" />
98+
<button type="submit">Submit</button>
99+
<div id="data-container"></div>
100+
</form>
101+
</body>
102+
</html>
103+
`)
104+
105+
configure({
106+
queryAllElements: (element, query) =>
107+
element.querySelectorAll(`#other ${query}`),
108+
})
109+
110+
expect(dtl.getByLabelText(container, /username/i)).toMatchInlineSnapshot(`
111+
<input
112+
id=user_other
113+
/>
114+
`)
115+
expect(dtl.getByLabelText(container, /password/i)).toMatchInlineSnapshot(`
116+
<input
117+
id=pass_other
118+
type=password
119+
/>
120+
`)
121+
// reset back to original config
122+
configure({
123+
queryAllElements: (element, query) => element.querySelectorAll(query),
124+
})
125+
})
126+
80127
test('byRole works without a global DOM', () => {
81128
const {
82129
window: {

src/config.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
import {Config, ConfigFn} from '../types/config'
1+
import {Config, ConfigFn, QueryAllElements, QueryElement} from '../types/config'
22
import {prettyDOM} from './pretty-dom'
33

44
type Callback<T> = () => T
5+
6+
const queryElement: QueryElement = <T extends Element>(
7+
element: T,
8+
selector: string,
9+
) => element.querySelector(selector)
10+
const queryElementAll: QueryAllElements = <T extends Element>(
11+
element: T,
12+
selector: string,
13+
) => element.querySelectorAll(selector)
14+
515
interface InternalConfig extends Config {
616
_disableExpensiveErrorDiagnostics: boolean
17+
/**
18+
* Returns the first element that is a descendant of node that matches selectors.
19+
*/
20+
queryElement: QueryElement
21+
/**
22+
* Returns all element descendants of node that match selectors.
23+
*/
24+
queryAllElements: QueryAllElements
725
}
826

927
// It would be cleaner for this to live inside './queries', but
@@ -46,6 +64,8 @@ let config: InternalConfig = {
4664
},
4765
_disableExpensiveErrorDiagnostics: false,
4866
computedStyleSupportsPseudoElements: false,
67+
queryElement,
68+
queryAllElements: queryElementAll,
4969
}
5070

5171
export function runWithExpensiveErrorDiagnosticsDisabled<T>(

src/label-helpers.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {TEXT_NODE} from './helpers'
22

3+
import {getConfig} from './config'
4+
35
const labelledNodeNames = [
46
'button',
57
'meter',
@@ -43,7 +45,7 @@ function getRealLabels(element: Element) {
4345

4446
if (!isLabelable(element)) return []
4547

46-
const labels = element.ownerDocument.querySelectorAll('label')
48+
const labels = getConfig().queryAllElements(element.ownerDocument, 'label')
4749
return Array.from(labels).filter(label => label.control === element)
4850
}
4951

@@ -63,7 +65,8 @@ function getLabels(
6365
const labelsId = ariaLabelledBy ? ariaLabelledBy.split(' ') : []
6466
return labelsId.length
6567
? labelsId.map(labelId => {
66-
const labellingElement = container.querySelector<HTMLElement>(
68+
const labellingElement = getConfig().queryElement(
69+
container,
6770
`[id="${labelId}"]`,
6871
)
6972
return labellingElement
@@ -75,7 +78,10 @@ function getLabels(
7578
const formControlSelector =
7679
'button, input, meter, output, progress, select, textarea'
7780
const labelledFormControl = Array.from(
78-
label.querySelectorAll<HTMLElement>(formControlSelector),
81+
getConfig().queryAllElements<Element, HTMLElement>(
82+
label,
83+
formControlSelector,
84+
),
7985
).filter(formControlElement => formControlElement.matches(selector))[0]
8086
return {content: textToMatch, formControl: labelledFormControl}
8187
})

src/queries/display-value.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
fuzzyMatches,
1313
makeNormalizer,
1414
buildQueries,
15+
getConfig,
1516
} from './all-utils'
1617

1718
const queryAllByDisplayValue: AllByBoundAttribute = (
@@ -23,7 +24,10 @@ const queryAllByDisplayValue: AllByBoundAttribute = (
2324
const matcher = exact ? matches : fuzzyMatches
2425
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
2526
return Array.from(
26-
container.querySelectorAll<HTMLElement>(`input,textarea,select`),
27+
getConfig().queryAllElements<HTMLElement, HTMLElement>(
28+
container,
29+
`input,textarea,select`,
30+
),
2731
).filter(node => {
2832
if (node.tagName === 'SELECT') {
2933
const selectedOptions = Array.from(

src/queries/label-text.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {getConfig} from '../config'
21
import {checkContainerType} from '../helpers'
32
import {getLabels, getRealLabels, getLabelContent} from '../label-helpers'
43
import {
@@ -17,12 +16,18 @@ import {
1716
makeSingleQuery,
1817
wrapAllByQueryWithSuggestion,
1918
wrapSingleQueryWithSuggestion,
19+
getConfig,
2020
} from './all-utils'
2121

2222
function queryAllLabels(
2323
container: HTMLElement,
2424
): {textToMatch: string | null; node: HTMLElement}[] {
25-
return Array.from(container.querySelectorAll<HTMLElement>('label,input'))
25+
return Array.from(
26+
getConfig().queryAllElements<HTMLElement, HTMLElement>(
27+
container,
28+
'label,input',
29+
),
30+
)
2631
.map(node => {
2732
return {node, textToMatch: getLabelContent(node)}
2833
})
@@ -56,7 +61,7 @@ const queryAllByLabelText: AllByText = (
5661
const matcher = exact ? matches : fuzzyMatches
5762
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
5863
const matchingLabelledElements = Array.from(
59-
container.querySelectorAll<HTMLElement>('*'),
64+
getConfig().queryAllElements<HTMLElement, HTMLElement>(container, '*'),
6065
)
6166
.filter(element => {
6267
return (
@@ -169,7 +174,7 @@ function getTagNameOfElementAssociatedWithLabelViaFor(
169174
return null
170175
}
171176

172-
const element = container.querySelector(`[id="${htmlFor}"]`)
177+
const element = getConfig().queryElement(container, `[id="${htmlFor}"]`)
173178
return element ? element.tagName.toLowerCase() : null
174179
}
175180

src/queries/role.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ function queryAllByRole(
100100
}
101101

102102
return Array.from(
103-
container.querySelectorAll(
103+
getConfig().queryAllElements(
104+
container,
104105
// Only query elements that can be matched by the following filters
105106
makeRoleSelector(role, exact, normalizer ? matchNormalizer : undefined),
106107
),

src/queries/text.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {AllByText, GetErrorFunction} from '../../types'
55
import {
66
fuzzyMatches,
77
matches,
8+
getConfig,
89
makeNormalizer,
910
getNodeText,
1011
buildQueries,
@@ -32,7 +33,12 @@ const queryAllByText: AllByText = (
3233
return (
3334
[
3435
...baseArray,
35-
...Array.from(container.querySelectorAll<HTMLElement>(selector)),
36+
...Array.from(
37+
getConfig().queryAllElements<HTMLElement, HTMLElement>(
38+
container,
39+
selector,
40+
),
41+
),
3642
]
3743
// TODO: `matches` according lib.dom.d.ts can get only `string` but according our code it can handle also boolean :)
3844
.filter(node => !ignore || !node.matches(ignore as string))

src/queries/title.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import {
1010
fuzzyMatches,
1111
matches,
12+
getConfig,
1213
makeNormalizer,
1314
getNodeText,
1415
buildQueries,
@@ -27,7 +28,10 @@ const queryAllByTitle: AllByBoundAttribute = (
2728
const matcher = exact ? matches : fuzzyMatches
2829
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
2930
return Array.from(
30-
container.querySelectorAll<HTMLElement>('[title], svg > title'),
31+
getConfig().queryAllElements<HTMLElement, HTMLElement>(
32+
container,
33+
'[title], svg > title',
34+
),
3135
).filter(
3236
node =>
3337
matcher(node.getAttribute('title'), node, text, matchNormalizer) ||

src/query-helpers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ function queryAllByAttribute(
3535
const matcher = exact ? matches : fuzzyMatches
3636
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
3737
return Array.from(
38-
container.querySelectorAll<HTMLElement>(`[${attribute}]`),
38+
getConfig().queryAllElements<HTMLElement, HTMLElement>(
39+
container,
40+
`[${attribute}]`,
41+
),
3942
).filter(node =>
4043
matcher(node.getAttribute(attribute), node, text, matchNormalizer),
4144
)

types/__tests__/type-tests.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ export async function testQueries() {
4040

4141
// screen queries
4242
screen.getByText('foo')
43-
screen.getByText<HTMLDivElement>('foo')
43+
screen.getByText('foo')
4444
screen.queryByText('foo')
4545
await screen.findByText('foo')
4646
await screen.findByText('foo', undefined, {timeout: 10})
4747
screen.debug(screen.getAllByText('bar'))
4848
screen.queryAllByText('bar')
4949
await screen.findAllByText('bar')
50-
await screen.findAllByRole<HTMLButtonElement>('button', {name: 'submit'})
50+
await screen.findAllByRole('button', {name: 'submit'})
5151
await screen.findAllByText('bar', undefined, {timeout: 10})
5252
}
5353

@@ -249,11 +249,11 @@ export async function testWithin() {
249249
container.queryAllByLabelText('Some label')
250250

251251
container.getByText('Click me')
252-
container.getByText<HTMLButtonElement>('Click me')
253-
container.getAllByText<HTMLButtonElement>('Click me')
252+
container.getByText('Click me')
253+
container.getAllByText('Click me')
254254

255255
await container.findByRole('button', {name: /click me/i})
256-
container.getByRole<HTMLButtonElement>('button', {name: /click me/i})
256+
container.getByRole('button', {name: /click me/i})
257257
}
258258

259259
/*

types/config.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
export type QueryElement = {
2+
<T, K extends keyof HTMLElementTagNameMap>(container: T, selectors: K):
3+
| HTMLElementTagNameMap[K]
4+
| null
5+
<T, K extends keyof SVGElementTagNameMap>(container: T, selectors: K):
6+
| SVGElementTagNameMap[K]
7+
| null
8+
<T, E extends Element = Element>(container: T, selectors: string): E | null
9+
}
10+
export type QueryAllElements = {
11+
<T, K extends keyof HTMLElementTagNameMap>(
12+
container: T,
13+
selectors: K,
14+
): NodeListOf<HTMLElementTagNameMap[K]>
15+
<T, K extends keyof SVGElementTagNameMap>(
16+
container: T,
17+
selectors: K,
18+
): NodeListOf<SVGElementTagNameMap[K]>
19+
<T, E extends Element = Element>(
20+
container: T,
21+
selectors: string,
22+
): NodeListOf<E>
23+
}
24+
125
export interface Config {
226
testIdAttribute: string
327
/**
@@ -14,6 +38,14 @@ export interface Config {
1438
defaultHidden: boolean
1539
showOriginalStackTrace: boolean
1640
throwSuggestions: boolean
41+
/**
42+
* Returns the first element that is a descendant of node that matches selectors.
43+
*/
44+
queryElement?: QueryElement
45+
/**
46+
* Returns all element descendants of node that match selectors.
47+
*/
48+
queryAllElements?: QueryAllElements
1749
getElementError: (message: string | null, container: Element) => Error
1850
}
1951

0 commit comments

Comments
 (0)