-
Notifications
You must be signed in to change notification settings - Fork 252
/
Copy pathsetup.ts
118 lines (108 loc) · 3.12 KB
/
setup.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import {addListeners, EventHandlers} from './listeners'
import userEvent from '#src'
import {Options} from '#src/options'
import {FOCUSABLE_SELECTOR, getActiveElementOrBody} from '#src/utils'
import {setSelection} from '#src/event/selection'
export function render<Elements extends Element | Element[] = HTMLElement>(
ui: string,
{
eventHandlers,
focus,
selection,
}: {
eventHandlers?: EventHandlers
focus?: string | false
selection?: {
focusNode?: string
anchorNode?: string
focusOffset?: number
anchorOffset?: number
}
} = {},
) {
const div = document.createElement('div')
div.innerHTML = ui.trim()
document.body.append(div)
if (typeof focus === 'string') {
;(assertSingleNodeFromXPath(focus, div) as HTMLElement).focus()
} else if (focus !== false) {
const element: HTMLElement | null = findFocusable(div)
element?.focus()
}
if (selection) {
const focusNode =
typeof selection.focusNode === 'string'
? assertSingleNodeFromXPath(selection.focusNode, div)
: getActiveElementOrBody(document)
const anchorNode =
typeof selection.anchorNode === 'string'
? assertSingleNodeFromXPath(selection.anchorNode, div)
: focusNode
const focusOffset = selection.focusOffset ?? 0
const anchorOffset = selection.anchorOffset ?? focusOffset
setSelection({
focusNode,
anchorNode,
focusOffset,
anchorOffset,
})
}
type ElementsArray = Elements extends Array<Element> ? Elements : [Elements]
// The HTMLCollection in lib.d.ts does not allow array access
type ElementsCollection = HTMLCollection &
ElementsArray & {
item<N extends number>(i: N): ElementsArray[N]
}
return {
element: div.firstChild as ElementsArray[0],
elements: div.children as ElementsCollection,
// for single elements add the listeners to the element for capturing non-bubbling events
...addListeners(
div.children.length === 1 ? (div.firstChild as Element) : div,
{
eventHandlers,
},
),
xpathNode: <NodeType extends Node = HTMLElement>(xpath: string) =>
assertSingleNodeFromXPath(xpath, div) as NodeType,
}
}
function assertSingleNodeFromXPath(xpath: string, context: Node) {
const node = document.evaluate(
xpath,
context,
undefined,
XPathResult.FIRST_ORDERED_NODE_TYPE,
).singleNodeValue
if (!node) {
throw new Error(`invalid XPath: "${xpath}"`)
}
return node
}
export function setup<Elements extends Element | Element[] = HTMLElement>(
ui: string,
{
eventHandlers,
focus,
selection,
...options
}: Parameters<typeof render>[1] & Options = {},
) {
return {
user: userEvent.setup(options),
...render<Elements>(ui, {eventHandlers, focus, selection}),
}
}
function findFocusable(container: Element | ShadowRoot): HTMLElement | null {
for (const el of Array.from(container.querySelectorAll('*'))) {
if (el.matches(FOCUSABLE_SELECTOR)) {
return el as HTMLElement
} else if (el.shadowRoot) {
const f = findFocusable(el.shadowRoot)
if (f) {
return f
}
}
}
return null
}