Skip to content

Commit 7171c9d

Browse files
authored
refactor: reorganize tests for key event behavior (#863)
1 parent 5ea38e8 commit 7171c9d

File tree

13 files changed

+766
-737
lines changed

13 files changed

+766
-737
lines changed

tests/dom/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {setup} from '#testHelpers'
2+
3+
test('edit number input', async () => {
4+
const {element, user} = setup(`<input type="number" value="1"/>`)
5+
6+
await user.type(
7+
element,
8+
'e-5[ArrowLeft][Delete]6[ArrowLeft][Backspace][Backspace]',
9+
)
10+
11+
expect(element).toHaveValue(16)
12+
})

tests/event/behavior/keydown.ts

Lines changed: 296 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,301 @@
1-
import {setup} from '#testHelpers'
1+
import cases from 'jest-in-case'
2+
import {getUISelection} from '#src/document'
3+
import {dispatchUIEvent} from '#src/event'
4+
import {createConfig} from '#src/setup/setup'
5+
import {render} from '#testHelpers'
26

3-
test.each(['Backspace', 'Delete', 'End', 'Home', 'PageUp', 'PageDown'])(
4-
'implement no keydown behavior for [%s] outside of editable context',
5-
async code => {
6-
const {getEvents, user} = setup(`<div tabIndex="1"></div>`)
7+
describe('restrict certain keydown behavior to editable context', () => {
8+
;['Backspace', 'Delete', 'End', 'Home', 'PageUp', 'PageDown'].forEach(key => {
9+
test(key, () => {
10+
const {element, getEvents} = render(`<div tabIndex="1"></div>`)
711

8-
await user.keyboard(`[${code}>]`)
12+
dispatchUIEvent(createConfig(), element, 'keydown', {key})
913

10-
expect(getEvents().map(e => e.type)).toEqual(['keydown'])
14+
expect(getEvents().map(e => e.type)).toEqual(['keydown'])
15+
})
16+
})
17+
})
18+
19+
cases(
20+
'alter input selection',
21+
({html, selection, key, expectedSelection}) => {
22+
const {element} = render<HTMLInputElement>(html, {
23+
selection,
24+
})
25+
26+
dispatchUIEvent(createConfig(), element, 'keydown', {
27+
key,
28+
})
29+
30+
expect(getUISelection(element)).toEqual(
31+
expect.objectContaining(expectedSelection),
32+
)
33+
},
34+
{
35+
'collapse selection per [ArrowLeft]': {
36+
html: `<input value="foobar"/>`,
37+
selection: {anchorOffset: 2, focusOffset: 4},
38+
key: 'ArrowLeft',
39+
expectedSelection: {anchorOffset: 2, focusOffset: 2},
40+
},
41+
'collapse selection per [ArrowRight]': {
42+
html: `<input value="foobar"/>`,
43+
selection: {anchorOffset: 2, focusOffset: 4},
44+
key: 'ArrowRight',
45+
expectedSelection: {anchorOffset: 4, focusOffset: 4},
46+
},
47+
'move cursor per [ArrowLeft]': {
48+
html: `<input value="foobar"/>`,
49+
selection: {anchorOffset: 2, focusOffset: 2},
50+
key: 'ArrowLeft',
51+
expectedSelection: {anchorOffset: 1, focusOffset: 1},
52+
},
53+
'move cursor per [ArrowRight]': {
54+
html: `<input value="foobar"/>`,
55+
selection: {anchorOffset: 2, focusOffset: 2},
56+
key: 'ArrowRight',
57+
expectedSelection: {anchorOffset: 3, focusOffset: 3},
58+
},
59+
'set cursor per [Home]': {
60+
html: `<input value="foobar"/>`,
61+
selection: {anchorOffset: 2, focusOffset: 4},
62+
key: 'Home',
63+
expectedSelection: {anchorOffset: 0, focusOffset: 0},
64+
},
65+
'set cursor per [End]': {
66+
html: `<input value="foobar"/>`,
67+
selection: {anchorOffset: 2, focusOffset: 4},
68+
key: 'End',
69+
expectedSelection: {anchorOffset: 6, focusOffset: 6},
70+
},
71+
'set cursor per [PageUp]': {
72+
html: `<input value="foobar"/>`,
73+
selection: {anchorOffset: 2, focusOffset: 4},
74+
key: 'PageUp',
75+
expectedSelection: {anchorOffset: 0, focusOffset: 0},
76+
},
77+
'set cursor per [PageDown]': {
78+
html: `<input value="foobar"/>`,
79+
selection: {anchorOffset: 2, focusOffset: 4},
80+
key: 'PageDown',
81+
expectedSelection: {anchorOffset: 6, focusOffset: 6},
82+
},
83+
},
84+
)
85+
86+
cases(
87+
'alter document selection',
88+
({selection, key, expected}) => {
89+
const {
90+
elements: [, div],
91+
xpathNode,
92+
} = render(
93+
`<span>abc</span><div contenteditable><span>foo</span><span>bar</span></div><span>def</span>`,
94+
{selection},
95+
)
96+
97+
expected.forEach(expectedSelection => {
98+
dispatchUIEvent(createConfig(), div, 'keydown', {key})
99+
100+
const {focusNode, focusOffset} = document.getSelection() as Selection
101+
expect({focusNode, focusOffset}).toEqual({
102+
focusNode: xpathNode(expectedSelection.focusNode),
103+
focusOffset: expectedSelection.focusOffset,
104+
})
105+
})
106+
},
107+
{
108+
'collapse selection per [ArrowLeft]': {
109+
selection: {
110+
anchorNode: 'div/span[1]/text()',
111+
anchorOffset: 2,
112+
focusNode: 'div/span[2]/text()',
113+
focusOffset: 1,
114+
},
115+
key: 'ArrowLeft',
116+
expected: [{focusNode: 'div/span[1]/text()', focusOffset: 2}],
117+
},
118+
'collapse selection per [ArrowRight]': {
119+
selection: {
120+
anchorNode: 'div/span[1]/text()',
121+
anchorOffset: 2,
122+
focusNode: 'div/span[2]/text()',
123+
focusOffset: 1,
124+
},
125+
key: 'ArrowRight',
126+
expected: [{focusNode: 'div/span[2]/text()', focusOffset: 1}],
127+
},
128+
'move cursor per [ArrowLeft]': {
129+
selection: {
130+
focusNode: 'div/span[2]/text()',
131+
focusOffset: 1,
132+
},
133+
key: 'ArrowLeft',
134+
expected: [
135+
{focusNode: 'div/span[2]/text()', focusOffset: 0},
136+
{focusNode: 'div/span[1]/text()', focusOffset: 2},
137+
{focusNode: 'div/span[1]/text()', focusOffset: 1},
138+
{focusNode: 'div/span[1]/text()', focusOffset: 0},
139+
{focusNode: 'div/span[1]/text()', focusOffset: 0},
140+
],
141+
},
142+
'move cursor per [ArrowRight]': {
143+
selection: {
144+
focusNode: 'div/span[1]/text()',
145+
focusOffset: 2,
146+
},
147+
key: 'ArrowRight',
148+
expected: [
149+
{focusNode: 'div/span[1]/text()', focusOffset: 3},
150+
{focusNode: 'div/span[2]/text()', focusOffset: 1},
151+
{focusNode: 'div/span[2]/text()', focusOffset: 2},
152+
{focusNode: 'div/span[2]/text()', focusOffset: 3},
153+
{focusNode: 'div/span[2]/text()', focusOffset: 3},
154+
],
155+
},
156+
// TODO: implement [Home] and [End] on contenteditable with element children
157+
// 'set cursor per [Home]': {
158+
// selection: {
159+
// anchorNode: 'div/span[1]/text()',
160+
// anchorOffset: 2,
161+
// focusNode: 'div/span[2]/text()',
162+
// focusOffset: 1,
163+
// },
164+
// key: 'Home',
165+
// expected: [
166+
// { focusNode: 'div/span[1]/text()', focusOffset: 0 },
167+
// ],
168+
// },
169+
// 'set cursor per [End]': {
170+
// selection: {
171+
// anchorNode: 'div/span[1]/text()',
172+
// anchorOffset: 2,
173+
// focusNode: 'div/span[2]/text()',
174+
// focusOffset: 1,
175+
// },
176+
// key: 'End',
177+
// expected: [
178+
// { focusNode: 'div/span[2]/text()', focusOffset: 3 },
179+
// ],
180+
// },
181+
},
182+
)
183+
184+
cases(
185+
'set cursor on contenteditable with single text node',
186+
({key, expectedOffset}) => {
187+
const {element, xpathNode} = render(`<div contenteditable>foobar</div>`, {
188+
selection: {focusNode: 'div/text()', anchorOffset: 2, focusOffset: 4},
189+
})
190+
191+
dispatchUIEvent(createConfig(), element, 'keydown', {key})
192+
193+
const {focusNode, focusOffset} = document.getSelection() as Selection
194+
expect({focusNode, focusOffset}).toEqual({
195+
focusNode: xpathNode('div/text()'),
196+
focusOffset: expectedOffset,
197+
})
198+
},
199+
{
200+
Home: {
201+
key: 'Home',
202+
expectedOffset: 0,
203+
},
204+
End: {
205+
key: 'End',
206+
expectedOffset: 6,
207+
},
208+
},
209+
)
210+
211+
test('select input per `Control+A`', async () => {
212+
const {element} = render<HTMLInputElement>(`<input value="foo bar baz"/>`, {
213+
selection: {focusOffset: 5},
214+
})
215+
216+
const config = createConfig()
217+
config.keyboardState.modifiers.Control = true
218+
219+
dispatchUIEvent(config, element, 'keydown', {code: 'KeyA'})
220+
221+
expect(element).toHaveProperty('selectionStart', 0)
222+
expect(element).toHaveProperty('selectionEnd', 11)
223+
})
224+
225+
cases(
226+
'trigger deleteContent',
227+
({key, inputType, expectedValue}) => {
228+
const {element, getEvents} = render(`<input value="abcd"/>`, {
229+
selection: {focusOffset: 2},
230+
})
231+
232+
dispatchUIEvent(createConfig(), element, 'keydown', {key})
233+
234+
expect(getEvents('input')[0]).toHaveProperty('inputType', inputType)
235+
expect(element).toHaveValue(expectedValue)
236+
},
237+
{
238+
Backspace: {
239+
key: 'Backspace',
240+
inputType: 'deleteContentBackward',
241+
expectedValue: 'acd',
242+
},
243+
Delete: {
244+
key: 'Delete',
245+
inputType: 'deleteContentForward',
246+
expectedValue: 'abd',
247+
},
248+
},
249+
)
250+
251+
cases(
252+
'tab through elements',
253+
({focus, shiftKey = false, expectedFocus, expectedSelection}) => {
254+
const {xpathNode} = render(
255+
`<input value="abc"/><input type="number" value="1e23"/><button/>`,
256+
{
257+
focus,
258+
},
259+
)
260+
261+
const config = createConfig()
262+
config.keyboardState.modifiers.Shift = shiftKey
263+
264+
dispatchUIEvent(config, document.activeElement as Element, 'keydown', {
265+
key: 'Tab',
266+
})
267+
268+
expect(xpathNode(expectedFocus)).toHaveFocus()
269+
if (expectedSelection) {
270+
expect(getUISelection(xpathNode(expectedFocus))).toEqual(
271+
expect.objectContaining(expectedSelection),
272+
)
273+
}
274+
},
275+
{
276+
'tab to input': {
277+
focus: '//body',
278+
expectedFocus: 'input[1]',
279+
expectedSelection: {startOffset: 0, endOffset: 3},
280+
},
281+
'tab to number input': {
282+
focus: 'input[1]',
283+
expectedFocus: 'input[2]',
284+
expectedSelection: {startOffset: 0, endOffset: 4},
285+
},
286+
'tab forward to body': {
287+
focus: 'button',
288+
expectedFocus: '//body',
289+
},
290+
'tab backward to body': {
291+
focus: '*[1]',
292+
shiftKey: true,
293+
expectedFocus: '//body',
294+
},
295+
'tab backward around the corner': {
296+
focus: '//body',
297+
shiftKey: true,
298+
expectedFocus: 'button',
299+
},
11300
},
12301
)

0 commit comments

Comments
 (0)