UNPKG

18.5 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.type = type;
7
8var _dom = require("@testing-library/dom");
9
10var _utils = require("./utils");
11
12var _click = require("./click");
13
14// TODO: wrap in asyncWrapper
15const modifierCallbackMap = { ...createModifierCallbackEntries({
16 name: 'shift',
17 key: 'Shift',
18 keyCode: 16,
19 modifierProperty: 'shiftKey'
20 }),
21 ...createModifierCallbackEntries({
22 name: 'ctrl',
23 key: 'Control',
24 keyCode: 17,
25 modifierProperty: 'ctrlKey'
26 }),
27 ...createModifierCallbackEntries({
28 name: 'alt',
29 key: 'Alt',
30 keyCode: 18,
31 modifierProperty: 'altKey'
32 }),
33 ...createModifierCallbackEntries({
34 name: 'meta',
35 key: 'Meta',
36 keyCode: 93,
37 modifierProperty: 'metaKey'
38 })
39};
40const specialCharCallbackMap = {
41 '{enter}': handleEnter,
42 '{esc}': handleEsc,
43 '{del}': handleDel,
44 '{backspace}': handleBackspace,
45 '{selectall}': handleSelectall,
46 '{space}': handleSpace,
47 ' ': handleSpace
48};
49
50function wait(time) {
51 return new Promise(resolve => setTimeout(() => resolve(), time));
52} // this needs to be wrapped in the event/asyncWrapper for React's act and angular's change detection
53// depending on whether it will be async.
54
55
56async function type(element, text, {
57 delay = 0,
58 ...options
59} = {}) {
60 // we do not want to wrap in the asyncWrapper if we're not
61 // going to actually be doing anything async, so we only wrap
62 // if the delay is greater than 0
63 let result;
64
65 if (delay > 0) {
66 await (0, _dom.getConfig)().asyncWrapper(async () => {
67 result = await typeImpl(element, text, {
68 delay,
69 ...options
70 });
71 });
72 } else {
73 result = typeImpl(element, text, {
74 delay,
75 ...options
76 });
77 }
78
79 return result;
80}
81
82async function typeImpl(element, text, {
83 delay,
84 skipClick = false,
85 skipAutoClose = false,
86 initialSelectionStart,
87 initialSelectionEnd
88}) {
89 if (element.disabled) return;
90 if (!skipClick) (0, _click.click)(element);
91
92 if ((0, _utils.isContentEditable)(element) && document.getSelection().rangeCount === 0) {
93 const range = document.createRange();
94 range.setStart(element, 0);
95 range.setEnd(element, 0);
96 document.getSelection().addRange(range);
97 } // The focused element could change between each event, so get the currently active element each time
98
99
100 const currentElement = () => (0, _utils.getActiveElement)(element.ownerDocument); // by default, a new element has it's selection start and end at 0
101 // but most of the time when people call "type", they expect it to type
102 // at the end of the current input value. So, if the selection start
103 // and end are both the default of 0, then we'll go ahead and change
104 // them to the length of the current value.
105 // the only time it would make sense to pass the initialSelectionStart or
106 // initialSelectionEnd is if you have an input with a value and want to
107 // explicitely start typing with the cursor at 0. Not super common.
108
109
110 const value = (0, _utils.getValue)(currentElement());
111 const {
112 selectionStart,
113 selectionEnd
114 } = (0, _utils.getSelectionRange)(element);
115
116 if (value != null && selectionStart === 0 && selectionEnd === 0) {
117 (0, _utils.setSelectionRangeIfNecessary)(currentElement(), initialSelectionStart != null ? initialSelectionStart : value.length, initialSelectionEnd != null ? initialSelectionEnd : value.length);
118 }
119
120 const eventCallbacks = function () {
121 const callbacks = [];
122 let remainingString = text;
123
124 while (remainingString) {
125 const {
126 callback,
127 remainingString: newRemainingString
128 } = getNextCallback(remainingString, skipAutoClose);
129 callbacks.push(callback);
130 remainingString = newRemainingString;
131 }
132
133 return callbacks;
134 }();
135
136 await async function (callbacks) {
137 const eventOverrides = {};
138 let prevWasMinus, prevWasPeriod, prevValue, typedValue;
139
140 for (const callback of callbacks) {
141 if (delay > 0) await wait(delay);
142
143 if (!currentElement().disabled) {
144 const returnValue = callback({
145 currentElement,
146 prevWasMinus,
147 prevWasPeriod,
148 prevValue,
149 eventOverrides,
150 typedValue
151 });
152 Object.assign(eventOverrides, returnValue == null ? void 0 : returnValue.eventOverrides);
153 prevWasMinus = returnValue == null ? void 0 : returnValue.prevWasMinus;
154 prevWasPeriod = returnValue == null ? void 0 : returnValue.prevWasPeriod;
155 prevValue = returnValue == null ? void 0 : returnValue.prevValue;
156 typedValue = returnValue == null ? void 0 : returnValue.typedValue;
157 }
158 }
159 }(eventCallbacks);
160}
161
162function getNextCallback(remainingString, skipAutoClose) {
163 const modifierCallback = getModifierCallback(remainingString, skipAutoClose);
164
165 if (modifierCallback) {
166 return modifierCallback;
167 }
168
169 const specialCharCallback = getSpecialCharCallback(remainingString);
170
171 if (specialCharCallback) {
172 return specialCharCallback;
173 }
174
175 return getTypeCallback(remainingString);
176}
177
178function getModifierCallback(remainingString, skipAutoClose) {
179 const modifierKey = Object.keys(modifierCallbackMap).find(key => remainingString.startsWith(key));
180
181 if (!modifierKey) {
182 return null;
183 }
184
185 const callback = modifierCallbackMap[modifierKey]; // if this modifier has an associated "close" callback and the developer
186 // doesn't close it themselves, then we close it for them automatically
187 // Effectively if they send in: '{alt}a' then we type: '{alt}a{/alt}'
188
189 if (!skipAutoClose && callback.closeName && !remainingString.includes(callback.closeName)) {
190 remainingString += callback.closeName;
191 }
192
193 remainingString = remainingString.slice(modifierKey.length);
194 return {
195 callback,
196 remainingString
197 };
198}
199
200function getSpecialCharCallback(remainingString) {
201 const specialChar = Object.keys(specialCharCallbackMap).find(key => remainingString.startsWith(key));
202
203 if (!specialChar) {
204 return null;
205 }
206
207 return {
208 callback: specialCharCallbackMap[specialChar],
209 remainingString: remainingString.slice(specialChar.length)
210 };
211}
212
213function getTypeCallback(remainingString) {
214 const character = remainingString[0];
215 return {
216 callback: context => typeCharacter(character, context),
217 remainingString: remainingString.slice(1)
218 };
219}
220
221function setSelectionRange({
222 currentElement,
223 newValue,
224 newSelectionStart
225}) {
226 // if we *can* change the selection start, then we will if the new value
227 // is the same as the current value (so it wasn't programatically changed
228 // when the fireEvent.input was triggered).
229 // The reason we have to do this at all is because it actually *is*
230 // programmatically changed by fireEvent.input, so we have to simulate the
231 // browser's default behavior
232 const value = (0, _utils.getValue)(currentElement());
233
234 if (value === newValue) {
235 (0, _utils.setSelectionRangeIfNecessary)(currentElement(), newSelectionStart, newSelectionStart);
236 } else {
237 // If the currentValue is different than the expected newValue and we *can*
238 // change the selection range, than we should set it to the length of the
239 // currentValue to ensure that the browser behavior is mimicked.
240 (0, _utils.setSelectionRangeIfNecessary)(currentElement(), value.length, value.length);
241 }
242}
243
244function fireInputEventIfNeeded({
245 currentElement,
246 newValue,
247 newSelectionStart,
248 eventOverrides
249}) {
250 const prevValue = (0, _utils.getValue)(currentElement());
251
252 if (!currentElement().readOnly && !(0, _utils.isClickable)(currentElement()) && newValue !== prevValue) {
253 if ((0, _utils.isContentEditable)(currentElement())) {
254 _dom.fireEvent.input(currentElement(), {
255 target: {
256 textContent: newValue
257 },
258 ...eventOverrides
259 });
260 } else {
261 _dom.fireEvent.input(currentElement(), {
262 target: {
263 value: newValue
264 },
265 ...eventOverrides
266 });
267 }
268
269 setSelectionRange({
270 currentElement,
271 newValue,
272 newSelectionStart
273 });
274 }
275
276 return {
277 prevValue
278 };
279}
280
281function typeCharacter(char, {
282 currentElement,
283 prevWasMinus = false,
284 prevWasPeriod = false,
285 prevValue = '',
286 typedValue = '',
287 eventOverrides
288}) {
289 const key = char; // TODO: check if this also valid for characters with diacritic markers e.g. úé etc
290
291 const keyCode = char.charCodeAt(0);
292 let nextPrevWasMinus, nextPrevWasPeriod;
293 const textToBeTyped = typedValue + char;
294
295 const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
296 key,
297 keyCode,
298 which: keyCode,
299 ...eventOverrides
300 });
301
302 if (keyDownDefaultNotPrevented) {
303 const keyPressDefaultNotPrevented = _dom.fireEvent.keyPress(currentElement(), {
304 key,
305 keyCode,
306 charCode: keyCode,
307 ...eventOverrides
308 });
309
310 if ((0, _utils.getValue)(currentElement()) != null && keyPressDefaultNotPrevented) {
311 let newEntry = char;
312
313 if (prevWasMinus) {
314 newEntry = `-${char}`;
315 } else if (prevWasPeriod) {
316 newEntry = `${prevValue}.${char}`;
317 }
318
319 if ((0, _utils.isValidDateValue)(currentElement(), textToBeTyped)) {
320 newEntry = textToBeTyped;
321 }
322
323 const inputEvent = fireInputEventIfNeeded({ ...(0, _utils.calculateNewValue)(newEntry, currentElement()),
324 eventOverrides: {
325 data: key,
326 inputType: 'insertText',
327 ...eventOverrides
328 },
329 currentElement
330 });
331 prevValue = inputEvent.prevValue;
332
333 if ((0, _utils.isValidDateValue)(currentElement(), textToBeTyped)) {
334 _dom.fireEvent.change(currentElement(), {
335 target: {
336 value: textToBeTyped
337 }
338 });
339 } // typing "-" into a number input will not actually update the value
340 // so for the next character we type, the value should be set to
341 // `-${newEntry}`
342 // we also preserve the prevWasMinus when the value is unchanged due
343 // to typing an invalid character (typing "-a3" results in "-3")
344 // same applies for the decimal character.
345
346
347 if (currentElement().type === 'number') {
348 const newValue = (0, _utils.getValue)(currentElement());
349
350 if (newValue === prevValue && newEntry !== '-') {
351 nextPrevWasMinus = prevWasMinus;
352 } else {
353 nextPrevWasMinus = newEntry === '-';
354 }
355
356 if (newValue === prevValue && newEntry !== '.') {
357 nextPrevWasPeriod = prevWasPeriod;
358 } else {
359 nextPrevWasPeriod = newEntry === '.';
360 }
361 }
362 }
363 }
364
365 _dom.fireEvent.keyUp(currentElement(), {
366 key,
367 keyCode,
368 which: keyCode,
369 ...eventOverrides
370 });
371
372 return {
373 prevWasMinus: nextPrevWasMinus,
374 prevWasPeriod: nextPrevWasPeriod,
375 prevValue,
376 typedValue: textToBeTyped
377 };
378} // yes, calculateNewBackspaceValue and calculateNewValue look extremely similar
379// and you may be tempted to create a shared abstraction.
380// If you, brave soul, decide to so endevor, please increment this count
381// when you inevitably fail: 1
382
383
384function calculateNewBackspaceValue(element) {
385 const {
386 selectionStart,
387 selectionEnd
388 } = (0, _utils.getSelectionRange)(element);
389 const value = (0, _utils.getValue)(element);
390 let newValue, newSelectionStart;
391
392 if (selectionStart === null) {
393 // at the end of an input type that does not support selection ranges
394 // https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
395 newValue = value.slice(0, value.length - 1);
396 newSelectionStart = selectionStart - 1;
397 } else if (selectionStart === selectionEnd) {
398 if (selectionStart === 0) {
399 // at the beginning of the input
400 newValue = value;
401 newSelectionStart = selectionStart;
402 } else if (selectionStart === value.length) {
403 // at the end of the input
404 newValue = value.slice(0, value.length - 1);
405 newSelectionStart = selectionStart - 1;
406 } else {
407 // in the middle of the input
408 newValue = value.slice(0, selectionStart - 1) + value.slice(selectionEnd);
409 newSelectionStart = selectionStart - 1;
410 }
411 } else {
412 // we have something selected
413 const firstPart = value.slice(0, selectionStart);
414 newValue = firstPart + value.slice(selectionEnd);
415 newSelectionStart = firstPart.length;
416 }
417
418 return {
419 newValue,
420 newSelectionStart
421 };
422}
423
424function calculateNewDeleteValue(element) {
425 const {
426 selectionStart,
427 selectionEnd
428 } = (0, _utils.getSelectionRange)(element);
429 const value = (0, _utils.getValue)(element);
430 let newValue;
431
432 if (selectionStart === null) {
433 // at the end of an input type that does not support selection ranges
434 // https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
435 newValue = value;
436 } else if (selectionStart === selectionEnd) {
437 if (selectionStart === 0) {
438 // at the beginning of the input
439 newValue = value.slice(1);
440 } else if (selectionStart === value.length) {
441 // at the end of the input
442 newValue = value;
443 } else {
444 // in the middle of the input
445 newValue = value.slice(0, selectionStart) + value.slice(selectionEnd + 1);
446 }
447 } else {
448 // we have something selected
449 const firstPart = value.slice(0, selectionStart);
450 newValue = firstPart + value.slice(selectionEnd);
451 }
452
453 return {
454 newValue,
455 newSelectionStart: selectionStart
456 };
457}
458
459function createModifierCallbackEntries({
460 name,
461 key,
462 keyCode,
463 modifierProperty
464}) {
465 const closeName = `{/${name}}`;
466
467 function open({
468 currentElement,
469 eventOverrides
470 }) {
471 const newEventOverrides = {
472 [modifierProperty]: true
473 };
474
475 _dom.fireEvent.keyDown(currentElement(), {
476 key,
477 keyCode,
478 which: keyCode,
479 ...eventOverrides,
480 ...newEventOverrides
481 });
482
483 return {
484 eventOverrides: newEventOverrides
485 };
486 }
487
488 open.closeName = closeName;
489 return {
490 [`{${name}}`]: open,
491 [closeName]: function ({
492 currentElement,
493 eventOverrides
494 }) {
495 const newEventOverrides = {
496 [modifierProperty]: false
497 };
498
499 _dom.fireEvent.keyUp(currentElement(), {
500 key,
501 keyCode,
502 which: keyCode,
503 ...eventOverrides,
504 ...newEventOverrides
505 });
506
507 return {
508 eventOverrides: newEventOverrides
509 };
510 }
511 };
512}
513
514function handleEnter({
515 currentElement,
516 eventOverrides
517}) {
518 const key = 'Enter';
519 const keyCode = 13;
520
521 const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
522 key,
523 keyCode,
524 which: keyCode,
525 ...eventOverrides
526 });
527
528 if (keyDownDefaultNotPrevented) {
529 _dom.fireEvent.keyPress(currentElement(), {
530 key,
531 keyCode,
532 charCode: keyCode,
533 ...eventOverrides
534 });
535
536 if ((0, _utils.isClickable)(currentElement())) {
537 _dom.fireEvent.click(currentElement(), { ...eventOverrides
538 });
539 }
540 }
541
542 if (currentElement().tagName === 'TEXTAREA') {
543 const {
544 newValue,
545 newSelectionStart
546 } = (0, _utils.calculateNewValue)('\n', currentElement());
547
548 _dom.fireEvent.input(currentElement(), {
549 target: {
550 value: newValue
551 },
552 inputType: 'insertLineBreak',
553 ...eventOverrides
554 });
555
556 setSelectionRange({
557 currentElement,
558 newValue,
559 newSelectionStart
560 });
561 }
562
563 if (currentElement().tagName === 'INPUT' && currentElement().form && (currentElement().form.querySelectorAll('input').length === 1 || currentElement().form.querySelector('input[type="submit"]') || currentElement().form.querySelector('button[type="submit"]'))) {
564 _dom.fireEvent.submit(currentElement().form);
565 }
566
567 _dom.fireEvent.keyUp(currentElement(), {
568 key,
569 keyCode,
570 which: keyCode,
571 ...eventOverrides
572 });
573}
574
575function handleEsc({
576 currentElement,
577 eventOverrides
578}) {
579 const key = 'Escape';
580 const keyCode = 27;
581
582 _dom.fireEvent.keyDown(currentElement(), {
583 key,
584 keyCode,
585 which: keyCode,
586 ...eventOverrides
587 }); // NOTE: Browsers do not fire a keypress on meta key presses
588
589
590 _dom.fireEvent.keyUp(currentElement(), {
591 key,
592 keyCode,
593 which: keyCode,
594 ...eventOverrides
595 });
596}
597
598function handleDel({
599 currentElement,
600 eventOverrides
601}) {
602 const key = 'Delete';
603 const keyCode = 46;
604
605 const keyPressDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
606 key,
607 keyCode,
608 which: keyCode,
609 ...eventOverrides
610 });
611
612 if (keyPressDefaultNotPrevented) {
613 fireInputEventIfNeeded({ ...calculateNewDeleteValue(currentElement()),
614 eventOverrides: {
615 inputType: 'deleteContentForward',
616 ...eventOverrides
617 },
618 currentElement
619 });
620 }
621
622 _dom.fireEvent.keyUp(currentElement(), {
623 key,
624 keyCode,
625 which: keyCode,
626 ...eventOverrides
627 });
628}
629
630function handleBackspace({
631 currentElement,
632 eventOverrides
633}) {
634 const key = 'Backspace';
635 const keyCode = 8;
636
637 const keyPressDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
638 key,
639 keyCode,
640 which: keyCode,
641 ...eventOverrides
642 });
643
644 if (keyPressDefaultNotPrevented) {
645 fireInputEventIfNeeded({ ...calculateNewBackspaceValue(currentElement()),
646 eventOverrides: {
647 inputType: 'deleteContentBackward',
648 ...eventOverrides
649 },
650 currentElement
651 });
652 }
653
654 _dom.fireEvent.keyUp(currentElement(), {
655 key,
656 keyCode,
657 which: keyCode,
658 ...eventOverrides
659 });
660}
661
662function handleSelectall({
663 currentElement
664}) {
665 currentElement().setSelectionRange(0, (0, _utils.getValue)(currentElement()).length);
666}
667
668function handleSpace(context) {
669 if ((0, _utils.isClickable)(context.currentElement())) {
670 handleSpaceOnClickable(context);
671 return;
672 }
673
674 typeCharacter(' ', context);
675}
676
677function handleSpaceOnClickable({
678 currentElement,
679 eventOverrides
680}) {
681 const key = ' ';
682 const keyCode = 32;
683
684 const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
685 key,
686 keyCode,
687 which: keyCode,
688 ...eventOverrides
689 });
690
691 if (keyDownDefaultNotPrevented) {
692 _dom.fireEvent.keyPress(currentElement(), {
693 key,
694 keyCode,
695 charCode: keyCode,
696 ...eventOverrides
697 });
698 }
699
700 const keyUpDefaultNotPrevented = _dom.fireEvent.keyUp(currentElement(), {
701 key,
702 keyCode,
703 which: keyCode,
704 ...eventOverrides
705 });
706
707 if (keyDownDefaultNotPrevented && keyUpDefaultNotPrevented) {
708 _dom.fireEvent.click(currentElement(), { ...eventOverrides
709 });
710 }
711}
\No newline at end of file