diff --git a/src/__tests__/click.js b/src/__tests__/click.js index 810a25d7..5630c230 100644 --- a/src/__tests__/click.js +++ b/src/__tests__/click.js @@ -1,95 +1,64 @@ import React from 'react' import {render, screen} from '@testing-library/react' -import '@testing-library/jest-dom/extend-expect' import userEvent from '..' +import {setup} from './helpers/utils' + +test('click in input', () => { + const {element, getEventCalls} = setup('input') + userEvent.click(element) + expect(getEventCalls()).toMatchInlineSnapshot(` + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + focus + mouseup: Left (0) + click: Left (0) + `) +}) -test.each(['input', 'textarea'])( - 'should fire the correct events for <%s>', - type => { - const events = [] - const eventsHandler = jest.fn(evt => events.push(evt.type)) - render( - React.createElement(type, { - 'data-testid': 'element', - onMouseOver: eventsHandler, - onMouseMove: eventsHandler, - onMouseDown: eventsHandler, - onFocus: eventsHandler, - onMouseUp: eventsHandler, - onClick: eventsHandler, - }), - ) - - userEvent.click(screen.getByTestId('element')) - - expect(events).toEqual([ - 'mouseover', - 'mousemove', - 'mousedown', - 'focus', - 'mouseup', - 'click', - ]) - }, -) +test('click in textarea', () => { + const {element, getEventCalls} = setup('textarea') + userEvent.click(element) + expect(getEventCalls()).toMatchInlineSnapshot(` + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + focus + mouseup: Left (0) + click: Left (0) + `) +}) it('should fire the correct events for ', () => { - const events = [] - const eventsHandler = jest.fn(evt => events.push(evt.type)) - render( - , - ) - - userEvent.click(screen.getByTestId('element')) - - expect(events).toEqual([ - 'mouseover', - 'mousemove', - 'mousedown', - 'focus', - 'mouseup', - 'click', - 'change', - ]) - - expect(screen.getByTestId('element')).toHaveProperty('checked', true) + const {element, getEventCalls} = setup('input', {type: 'checkbox'}) + expect(element).not.toBeChecked() + userEvent.click(element) + expect(getEventCalls()).toMatchInlineSnapshot(` + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + focus + mouseup: Left (0) + click: unchecked -> checked + input: checked + change + `) }) it('should fire the correct events for ', () => { - const events = [] - const eventsHandler = jest.fn(evt => events.push(evt.type)) - render( - , - ) - - userEvent.click(screen.getByTestId('element')) - - expect(events).toEqual([]) - - expect(screen.getByTestId('element')).toHaveProperty('checked', false) + const {element, getEventCalls} = setup('input', { + type: 'checkbox', + disabled: true, + }) + userEvent.click(element) + expect(element).toBeDisabled() + // no event calls is expected here: + expect(getEventCalls()).toMatchInlineSnapshot(``) + expect(element).toBeDisabled() }) +// TODO: Update all these tests to use the setup util... + it('should fire the correct events for ', () => { const events = [] const eventsHandler = jest.fn(evt => events.push(evt.type)) @@ -434,12 +403,14 @@ test.each(['input', 'textarea'])( it('should fire mouse events with the correct properties', () => { const events = [] - const eventsHandler = jest.fn(evt => events.push({ - type: evt.type, - button: evt.button, - buttons: evt.buttons, - detail: evt.detail - })) + const eventsHandler = jest.fn(evt => + events.push({ + type: evt.type, + button: evt.button, + buttons: evt.buttons, + detail: evt.detail, + }), + ) render(
{ type: 'mouseover', button: 0, buttons: 0, - detail: 0 + detail: 0, }, { type: 'mousemove', button: 0, buttons: 0, - detail: 0 + detail: 0, }, { type: 'mousedown', button: 0, buttons: 1, - detail: 1 + detail: 1, }, { type: 'mouseup', button: 0, buttons: 1, - detail: 1 + detail: 1, }, { type: 'click', button: 0, buttons: 1, - detail: 1 + detail: 1, }, ]) }) it('should fire mouse events with custom button property', () => { const events = [] - const eventsHandler = jest.fn(evt => events.push({ - type: evt.type, - button: evt.button, - buttons: evt.buttons, - detail: evt.detail, - altKey: evt.altKey - })) + const eventsHandler = jest.fn(evt => + events.push({ + type: evt.type, + button: evt.button, + buttons: evt.buttons, + detail: evt.detail, + altKey: evt.altKey, + }), + ) render(
{ userEvent.click(screen.getByTestId('div'), { button: 1, - altKey: true + altKey: true, }) expect(events).toEqual([ @@ -519,47 +492,49 @@ it('should fire mouse events with custom button property', () => { button: 0, buttons: 0, detail: 0, - altKey: true + altKey: true, }, { type: 'mousemove', button: 0, buttons: 0, detail: 0, - altKey: true + altKey: true, }, { type: 'mousedown', button: 1, buttons: 4, detail: 1, - altKey: true + altKey: true, }, { type: 'mouseup', button: 1, buttons: 4, detail: 1, - altKey: true + altKey: true, }, { type: 'click', button: 1, buttons: 4, detail: 1, - altKey: true + altKey: true, }, ]) }) it('should fire mouse events with custom buttons property', () => { const events = [] - const eventsHandler = jest.fn(evt => events.push({ - type: evt.type, - button: evt.button, - buttons: evt.buttons, - detail: evt.detail - })) + const eventsHandler = jest.fn(evt => + events.push({ + type: evt.type, + button: evt.button, + buttons: evt.buttons, + detail: evt.detail, + }), + ) render(
{ ) userEvent.click(screen.getByTestId('div'), { - buttons: 4 + buttons: 4, }) expect(events).toEqual([ @@ -581,31 +556,31 @@ it('should fire mouse events with custom buttons property', () => { type: 'mouseover', button: 0, buttons: 0, - detail: 0 + detail: 0, }, { type: 'mousemove', button: 0, buttons: 0, - detail: 0 + detail: 0, }, { type: 'mousedown', button: 1, buttons: 4, - detail: 1 + detail: 1, }, { type: 'mouseup', button: 1, buttons: 4, - detail: 1 + detail: 1, }, { type: 'click', button: 1, buttons: 4, - detail: 1 + detail: 1, }, ]) }) diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js new file mode 100644 index 00000000..2381fddf --- /dev/null +++ b/src/__tests__/helpers/utils.js @@ -0,0 +1,182 @@ +import React from 'react' +import {render} from '@testing-library/react' + +// all of the stuff below is complex magic that makes the simpler tests work +// sorrynotsorry... + +const unstringSnapshotSerializer = { + test: val => typeof val === 'string', + print: val => val, +} + +expect.addSnapshotSerializer(unstringSnapshotSerializer) + +let eventListeners = [] + +function getTestData(element) { + return { + value: element.value, + selectionStart: element.selectionStart, + selectionEnd: element.selectionEnd, + checked: element.checked, + } +} + +function addEventListener(el, type, listener, options) { + const hijackedListener = e => { + e.testData = {previous: e.target.previousTestData} + const retVal = listener(e) + const next = getTestData(e.target) + e.testData.next = next + e.target.previousTestData = next + return retVal + } + eventListeners.push({el, type, listener: hijackedListener}) + el.addEventListener(type, hijackedListener, options) +} + +function setup(elementType, props, ...children) { + const { + container: {firstChild: element}, + } = render(React.createElement(elementType, props, ...children)) + element.previousTestData = getTestData(element) + + const getEventCalls = addListeners(element) + return {element, getEventCalls} +} + +function addListeners(element) { + const generalListener = jest.fn().mockName('eventListener') + const listeners = [ + 'keydown', + 'keyup', + 'keypress', + 'input', + 'change', + 'blur', + 'focus', + 'click', + 'mouseover', + 'mousemove', + 'mouseenter', + 'mouseleave', + 'mouseup', + 'mousedown', + ] + + for (const name of listeners) { + addEventListener(element, name, generalListener) + } + function getEventCalls() { + return generalListener.mock.calls + .map(([event]) => { + const window = event.target.ownerDocument.defaultView + const modifiers = ['altKey', 'shiftKey', 'metaKey', 'ctrlKey'] + .filter(key => event[key]) + .map(k => `{${k.replace('Key', '')}}`) + .join('') + + if ( + event.type === 'click' && + event.hasOwnProperty('testData') && + (element.type === 'checkbox' || element.type === 'radio') + ) { + return getCheckboxOrRadioClickedLine(event) + } + + if (event.type === 'input' && event.hasOwnProperty('testData')) { + return getInputLine(element, event) + } + + if (event instanceof window.KeyboardEvent) { + return getKeyboardEventLine(event, modifiers) + } + + if (event instanceof window.MouseEvent) { + return getMouseEventLine(event, modifiers) + } + + return [event.type, modifiers].join(' ').trim() + }) + .join('\n') + } + return getEventCalls +} + +// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button +const mouseButtonMap = { + 0: 'Left', + 1: 'Middle', + 2: 'Right', + 3: 'Browser Back', + 4: 'Browser Forward', +} +function getMouseEventLine(event, modifiers) { + return [ + `${event.type}:`, + mouseButtonMap[event.button], + `(${event.button})`, + modifiers, + ] + .join(' ') + .trim() +} + +function getKeyboardEventLine(event, modifiers) { + return [ + `${event.type}:`, + event.key, + typeof event.keyCode === 'undefined' ? null : `(${event.keyCode})`, + modifiers, + ] + .join(' ') + .trim() +} + +function getCheckboxOrRadioClickedLine(event) { + const {previous, next} = event.testData + + return `${event.type}: ${previous.checked ? '' : 'un'}checked -> ${ + next.checked ? '' : 'un' + }checked` +} + +function getInputLine(element, event) { + if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + const {previous, next} = event.testData + + if (element.type === 'checkbox' || element.type === 'radio') { + return `${event.type}: ${next.checked ? '' : 'un'}checked` + } else { + const prevVal = [ + previous.value.slice(0, previous.selectionStart), + ...(previous.selectionStart === previous.selectionEnd + ? ['{CURSOR}'] + : [ + '{SELECTION}', + previous.value.slice( + previous.selectionStart, + previous.selectionEnd, + ), + '{/SELECTION}', + ]), + previous.value.slice(previous.selectionEnd), + ].join('') + return `${event.type}: "${prevVal}" -> "${next.value}"` + } + } else { + throw new Error( + `fired ${event.type} event on a ${element.tagName} which probably doesn't make sense. Fix that, or handle it in the setup function`, + ) + } +} + +// eslint-disable-next-line jest/prefer-hooks-on-top +afterEach(() => { + for (const {el, type, listener} of eventListeners) { + el.removeEventListener(type, listener) + } + eventListeners = [] +}) + +export {setup, addEventListener} diff --git a/src/__tests__/type-modifiers.js b/src/__tests__/type-modifiers.js index a6909782..47704470 100644 --- a/src/__tests__/type-modifiers.js +++ b/src/__tests__/type-modifiers.js @@ -1,6 +1,5 @@ -import React from 'react' -import {render} from '@testing-library/react' -import userEvent from '../../src' +import userEvent from '..' +import {setup, addEventListener} from './helpers/utils' // Note, use the setup function at the bottom of the file... // but don't hurt yourself trying to read it 😅 @@ -84,7 +83,7 @@ test('{alt}a{/alt}', async () => { keydown: a (97) {alt} keypress: a (97) {alt} input: "{CURSOR}" -> "a" - keyup: a (97) + keyup: a (97) {alt} keyup: Alt (18) `) }) @@ -100,7 +99,7 @@ test('{meta}a{/meta}', async () => { keydown: a (97) {meta} keypress: a (97) {meta} input: "{CURSOR}" -> "a" - keyup: a (97) + keyup: a (97) {meta} keyup: Meta (93) `) }) @@ -116,7 +115,7 @@ test('{ctrl}a{/ctrl}', async () => { keydown: a (97) {ctrl} keypress: a (97) {ctrl} input: "{CURSOR}" -> "a" - keyup: a (97) + keyup: a (97) {ctrl} keyup: Control (17) `) }) @@ -132,7 +131,7 @@ test('{shift}a{/shift}', async () => { keydown: a (97) {shift} keypress: a (97) {shift} input: "{CURSOR}" -> "a" - keyup: a (97) + keyup: a (97) {shift} keyup: Shift (16) `) }) @@ -176,7 +175,7 @@ test('{enter} on a button', async () => { focus keydown: Enter (13) keypress: Enter (13) - click + click: Left (0) keyup: Enter (13) `) }) @@ -206,116 +205,28 @@ test('{meta}{enter}{/meta} on a button', async () => { keydown: Meta (93) {meta} keydown: Enter (13) {meta} keypress: Enter (13) {meta} - click {meta} + click: Left (0) {meta} keyup: Enter (13) {meta} keyup: Meta (93) `) }) -// all of the stuff below is complex magic that makes the simpler tests above work -// sorrynotsorry... - -const unstringSnapshotSerializer = { - test: val => typeof val === 'string', - print: val => val, -} - -expect.addSnapshotSerializer(unstringSnapshotSerializer) - -let eventListeners = [] - -function addEventListener(el, type, listener, options) { - const hijackedListener = e => { - e.testData = { - previousValue: e.target.previousValue, - nextValue: e.target.value, - selectionStart: e.target.previousSelectionStart, - selectionEnd: e.target.previousSelectionEnd, - } - e.target.previousValue = e.target.value - e.target.previousSelectionStart = e.target.selectionStart - e.target.previousSelectionEnd = e.target.selectionEnd - return listener(e) - } - eventListeners.push({el, type, listener: hijackedListener}) - el.addEventListener(type, hijackedListener, options) -} - -function setup(elementType) { - const { - container: {firstChild: element}, - } = render(React.createElement(elementType)) - const getEventCalls = addListeners(element) - return {element, getEventCalls} -} - -function addListeners(element) { - const generalListener = jest.fn().mockName('eventListener') - const listeners = [ - 'keydown', - 'keyup', - 'keypress', - 'input', - 'change', - 'blur', - 'focus', - 'click', - ] - - for (const name of listeners) { - addEventListener(element, name, generalListener) - } - function getEventCalls() { - return generalListener.mock.calls - .map(([event]) => { - const modifiers = ['altKey', 'shiftKey', 'metaKey', 'ctrlKey'] - .filter(key => event[key]) - .map(k => `{${k.replace('Key', '')}}`) - .join('') - if (event.type === 'input' && event.hasOwnProperty('testData')) { - const { - previousValue, - nextValue, - selectionStart, - selectionEnd, - } = event.testData - const prevVal = [ - previousValue.slice(0, selectionStart), - ...(selectionStart === selectionEnd - ? ['{CURSOR}'] - : [ - '{SELECTION}', - previousValue.slice(selectionStart, selectionEnd), - '{/SELECTION}', - ]), - previousValue.slice(selectionEnd), - ].join('') - return `input: "${prevVal}" -> "${nextValue}"` - } - if ( - event instanceof event.target.ownerDocument.defaultView.KeyboardEvent - ) { - return [ - `${event.type}:`, - event.key, - typeof event.keyCode === 'undefined' ? null : `(${event.keyCode})`, - modifiers, - ] - .join(' ') - .trim() - } else { - return [event.type, modifiers].join(' ').trim() - } - }) - .join('\n') - } - return getEventCalls -} - -// eslint-disable-next-line jest/prefer-hooks-on-top -afterEach(() => { - for (const {el, type, listener} of eventListeners) { - el.removeEventListener(type, listener) - } - eventListeners = [] +test('{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}', async () => { + const {element: input, getEventCalls} = setup('input') + + await userEvent.type(input, '{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}') + + expect(getEventCalls()).toMatchInlineSnapshot(` + focus + keydown: Meta (93) {meta} + keydown: Alt (18) {alt}{meta} + keydown: Control (17) {alt}{meta}{ctrl} + keydown: a (97) {alt}{meta}{ctrl} + keypress: a (97) {alt}{meta}{ctrl} + input: "{CURSOR}" -> "a" + keyup: a (97) {alt}{meta}{ctrl} + keyup: Control (17) {alt}{meta} + keyup: Alt (18) {meta} + keyup: Meta (93) + `) }) diff --git a/src/__tests__/type.js b/src/__tests__/type.js index 5f8255cf..8dc95614 100644 --- a/src/__tests__/type.js +++ b/src/__tests__/type.js @@ -1,39 +1,58 @@ import React, {Fragment} from 'react' import {render, screen} from '@testing-library/react' -import userEvent from '../../src' - -test.each(['input', 'textarea'])('should type text in <%s>', async type => { - const onChange = jest.fn() - render( - React.createElement(type, { - 'data-testid': 'input', - onChange, - }), - ) - const text = 'Hello, world!' - await userEvent.type(screen.getByTestId('input'), text) - expect(onChange).toHaveBeenCalledTimes(text.length) - expect(screen.getByTestId('input')).toHaveProperty('value', text) +import userEvent from '..' +import {setup} from './helpers/utils' + +it('types text in input', async () => { + const {element, getEventCalls} = setup('input') + await userEvent.type(element, 'Sup') + expect(getEventCalls()).toMatchInlineSnapshot(` + focus + keydown: S (83) + keypress: S (83) + input: "{CURSOR}" -> "S" + keyup: S (83) + keydown: u (117) + keypress: u (117) + input: "S{CURSOR}" -> "Su" + keyup: u (117) + keydown: p (112) + keypress: p (112) + input: "Su{CURSOR}" -> "Sup" + keyup: p (112) + `) }) -test('should append text one by one', async () => { - const onChange = jest.fn() - render() - await userEvent.type(screen.getByTestId('input'), 'hello') - await userEvent.type(screen.getByTestId('input'), ' world') - expect(onChange).toHaveBeenCalledTimes('hello world'.length) - expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world') +it('types text in textarea', async () => { + const {element, getEventCalls} = setup('textarea') + await userEvent.type(element, 'Sup') + expect(getEventCalls()).toMatchInlineSnapshot(` + focus + keydown: S (83) + keypress: S (83) + input: "{CURSOR}" -> "S" + keyup: S (83) + keydown: u (117) + keypress: u (117) + input: "S{CURSOR}" -> "Su" + keyup: u (117) + keydown: p (112) + keypress: p (112) + input: "Su{CURSOR}" -> "Sup" + keyup: p (112) + `) }) test('should append text all at once', async () => { - const onChange = jest.fn() - render() - await userEvent.type(screen.getByTestId('input'), 'hello', {allAtOnce: true}) - await userEvent.type(screen.getByTestId('input'), ' world', {allAtOnce: true}) - expect(onChange).toHaveBeenCalledTimes(2) - expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world') + const {element, getEventCalls} = setup('input') + await userEvent.type(element, 'Sup', {allAtOnce: true}) + expect(getEventCalls()).toMatchInlineSnapshot(` + focus + input: "{CURSOR}" -> "Sup" + `) }) +// TODO: Let's migrate these tests to use the setup util test('should not type when event.preventDefault() is called', async () => { const onChange = jest.fn() const onKeydown = jest diff --git a/src/type.js b/src/type.js index 41cdf9bf..de87434f 100644 --- a/src/type.js +++ b/src/type.js @@ -177,12 +177,12 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { remainingString = remainingString.slice(1) } } - let eventOverrides = {} + const eventOverrides = {} for (const callback of eventCallbacks) { if (delay > 0) await wait(delay) if (!currentElement().disabled) { const returnValue = await callback({eventOverrides}) - eventOverrides = returnValue?.eventOverrides ?? eventOverrides + Object.assign(eventOverrides, returnValue?.eventOverrides) } } } @@ -267,6 +267,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { key, keyCode, which: keyCode, + ...eventOverrides, }) }