From 8797ea11ff3e23d9518ceb3c44f3b9b4e164cdb8 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Fri, 5 Jun 2020 21:25:16 +0800 Subject: [PATCH] feat: add toggleSelectOptions (#252) * implement without test * feat: support toggleOptions with tests * fix: type definition for toggleOptions * docs: add toggleOptions example * fix: test coverage * docs: fix README * fix: avoid testid in docs and test --- README.md | 33 +++++++ src/__tests__/dblclick.js | 103 ++++++++++---------- src/__tests__/helpers/utils.js | 2 +- src/__tests__/toggleselectoptions.js | 137 +++++++++++++++++++++++++++ src/index.js | 45 +++++++++ typings/index.d.ts | 5 + 6 files changed, 275 insertions(+), 50 deletions(-) create mode 100644 src/__tests__/toggleselectoptions.js diff --git a/README.md b/README.md index e82446b3..7edf05ba 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ change the state of the checkbox. - [`upload(element, file, [{ clickInit, changeInit }])`](#uploadelement-file--clickinit-changeinit-) - [`clear(element)`](#clearelement) - [`selectOptions(element, values)`](#selectoptionselement-values) + - [`toggleSelectOptions(element, values)`](#toggleselectoptionselement-values) - [`tab({shift, focusTrap})`](#tabshift-focustrap) - [Issues](#issues) - [🐛 Bugs](#-bugs) @@ -308,6 +309,38 @@ userEvent.selectOptions(screen.getByTestId('select-multiple'), [ ]) ``` +### `toggleSelectOptions(element, values)` + +Toggle the specified option(s) of a ` + + + + , + ) + + userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1', '3']) + + expect(screen.getByText('A').selected).toBe(true) + expect(screen.getByText('C').selected).toBe(true) + + userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1']) + + expect(screen.getByText('A').selected).toBe(false) +}) +``` + +The `values` parameter can be either an array of values or a singular scalar +value. + ### `tab({shift, focusTrap})` Fires a tab event changing the document.activeElement in the same way the diff --git a/src/__tests__/dblclick.js b/src/__tests__/dblclick.js index 087d3f02..43f86781 100644 --- a/src/__tests__/dblclick.js +++ b/src/__tests__/dblclick.js @@ -205,15 +205,16 @@ test('should not blur when mousedown prevents default', () => { ]) }) - 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, }, { type: 'mousedown', button: 0, buttons: 1, - detail: 2 + detail: 2, }, { type: 'mouseup', button: 0, buttons: 1, - detail: 2 + detail: 2, }, { type: 'click', button: 0, buttons: 1, - detail: 2 + detail: 2, }, { type: 'dblclick', button: 0, buttons: 1, - detail: 2 + detail: 2, }, ]) }) 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.dblClick(screen.getByTestId('div'), { button: 1, - altKey: true + altKey: true, }) expect(events).toEqual([ @@ -319,75 +322,77 @@ 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, }, { type: 'mousedown', button: 1, buttons: 4, detail: 2, - altKey: true + altKey: true, }, { type: 'mouseup', button: 1, buttons: 4, detail: 2, - altKey: true + altKey: true, }, { type: 'click', button: 1, buttons: 4, detail: 2, - altKey: true + altKey: true, }, { type: 'dblclick', button: 1, buttons: 4, detail: 2, - 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.dblClick(screen.getByTestId('div'), { - buttons: 4 + buttons: 4, }) expect(events).toEqual([ @@ -410,55 +415,55 @@ 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, }, { type: 'mousedown', button: 1, buttons: 4, - detail: 2 + detail: 2, }, { type: 'mouseup', button: 1, buttons: 4, - detail: 2 + detail: 2, }, { type: 'click', button: 1, buttons: 4, - detail: 2 + detail: 2, }, { type: 'dblclick', button: 1, buttons: 4, - detail: 2 + detail: 2, }, ]) }) diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js index e35da3e4..6dbfd1c8 100644 --- a/src/__tests__/helpers/utils.js +++ b/src/__tests__/helpers/utils.js @@ -181,4 +181,4 @@ afterEach(() => { eventListeners = [] }) -export {setup, addEventListener} +export {setup, addEventListener, addListeners} diff --git a/src/__tests__/toggleselectoptions.js b/src/__tests__/toggleselectoptions.js new file mode 100644 index 00000000..bb88507d --- /dev/null +++ b/src/__tests__/toggleselectoptions.js @@ -0,0 +1,137 @@ +import {render, screen} from '@testing-library/react' +import React from 'react' +import userEvent from '..' +import {addListeners, setup} from './helpers/utils' + +test('should fire the correct events for multiple select', () => { + const {element: select, getEventCalls} = setup( + , + ) + + userEvent.toggleSelectOptions(select, '1') + + expect(getEventCalls()).toMatchInlineSnapshot(` + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + focus + mouseup: Left (0) + click: Left (0) + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + mouseup: Left (0) + click: Left (0) + change + `) + + expect(screen.getByRole('listbox').value).toBe('1') +}) + +test('should fire the correct events for multiple select when focus is in other element', () => { + const {element: select, getEventCalls} = setup( + <> + + + , + ) + + const $otherBtn = screen.getByRole('button') + + const getButtonEvents = addListeners($otherBtn) + + $otherBtn.focus() + + userEvent.toggleSelectOptions(select, '1') + + expect(getButtonEvents()).toMatchInlineSnapshot(` + focus + mousemove: Left (0) + mouseleave: Left (0) + blur + `) + expect(getEventCalls()).toMatchInlineSnapshot(` + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + focus + mouseup: Left (0) + click: Left (0) + mouseover: Left (0) + mousemove: Left (0) + mousedown: Left (0) + mouseup: Left (0) + click: Left (0) + change + `) +}) + +test('toggle options as expected', () => { + const TestBed = () => { + const [selected, setSelected] = React.useState([]) + + return ( + <> + + {selected.join(', ')} + + ) + } + + render() + + // select one + userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1']) + expect(screen.getByRole('status')).toHaveTextContent('1') + + // unselect one and select two + userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1', '2']) + expect(screen.getByRole('status')).toHaveTextContent('2') + + // select one + userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1']) + expect(screen.getByRole('status')).toHaveTextContent('1, 2') +}) + +it('throws error when provided element is not a multiple select', () => { + const {element: select} = setup( + , + ) + + expect(() => { + userEvent.toggleSelectOptions(select) + }).toThrowErrorMatchingInlineSnapshot( + `Unable to toggleSelectOptions - please provide a select element with multiple=true`, + ) +}) diff --git a/src/index.js b/src/index.js index febda901..79694f1e 100644 --- a/src/index.js +++ b/src/index.js @@ -161,6 +161,19 @@ function selectOption(select, option, init) { fireEvent.change(select) } +function toggleSelectOption(select, option, init) { + fireEvent.mouseOver(option, getMouseEventOptions('mouseover', init)) + fireEvent.mouseMove(option, getMouseEventOptions('mousemove', init)) + fireEvent.mouseDown(option, getMouseEventOptions('mousedown', init)) + fireEvent.focus(option) + fireEvent.mouseUp(option, getMouseEventOptions('mouseup', init)) + fireEvent.click(option, getMouseEventOptions('click', init, 1)) + + option.selected = !option.selected + + fireEvent.change(select) +} + const Keys = { Backspace: {keyCode: 8, code: 'Backspace', key: 'Backspace'}, } @@ -310,6 +323,37 @@ function selectOptions(element, values, init) { } } +function toggleSelectOptions(element, values, init) { + if (!element || element.tagName !== 'SELECT' || !element.multiple) { + throw new Error( + `Unable to toggleSelectOptions - please provide a select element with multiple=true`, + ) + } + + const previouslyFocusedElement = getPreviouslyFocusedElement(element) + if (previouslyFocusedElement) { + fireEvent.mouseMove( + previouslyFocusedElement, + getMouseEventOptions('mousemove', init), + ) + fireEvent.mouseLeave( + previouslyFocusedElement, + getMouseEventOptions('mouseleave', init), + ) + } + + clickElement(element, previouslyFocusedElement, init) + + const valArray = Array.isArray(values) ? values : [values] + const selectedOptions = Array.from(element.querySelectorAll('option')).filter( + opt => valArray.includes(opt.value) || valArray.includes(opt), + ) + + if (selectedOptions.length > 0) { + selectedOptions.forEach(option => toggleSelectOption(element, option, init)) + } +} + function clear(element) { if (element.disabled) return @@ -387,6 +431,7 @@ const userEvent = { click, dblClick, selectOptions, + toggleSelectOptions, clear, type, upload, diff --git a/typings/index.d.ts b/typings/index.d.ts index e61d5725..735d4f84 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -27,6 +27,11 @@ declare const userEvent: { values: string | string[] | HTMLElement | HTMLElement[], init?: MouseEventInit, ) => void + toggleSelectOptions: ( + element: TargetElement, + values: string | string[] | HTMLElement | HTMLElement[], + init?: MouseEventInit, + ) => void upload: ( element: TargetElement, files: FilesArgument,