Skip to content

Commit

Permalink
Merge branch 'master' into pr/add-home-end-to-type
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Feb 9, 2021
2 parents b6f961a + b4330c4 commit 8f3cd01
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 73 deletions.
11 changes: 11 additions & 0 deletions src/__tests__/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ function setupListbox() {
document.body.append(wrapper)
const listbox = wrapper.querySelector('[role="listbox"]')
const options = Array.from(wrapper.querySelectorAll('[role="option"]'))

// the user is responsible for handling aria-selected on listbox options
options.forEach(el =>
el.addEventListener('click', e =>
e.target.setAttribute(
'aria-selected',
JSON.stringify(!JSON.parse(e.target.getAttribute('aria-selected'))),
),
),
)

return {
...addListeners(listbox),
listbox,
Expand Down
55 changes: 29 additions & 26 deletions src/__tests__/select-options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import userEvent from '../'
import {setupSelect, addListeners, setupListbox} from './helpers/utils'
import {setupSelect, addListeners, setupListbox, setup} from './helpers/utils'

test('fires correct events', () => {
const {select, options, getEventSnapshot} = setupSelect()
Expand All @@ -22,6 +22,13 @@ test('fires correct events', () => {
select[name="select"][value="1"] - click: Left (0)
select[name="select"][value="2"] - input
select[name="select"][value="2"] - change
select[name="select"][value="2"] - pointerover
select[name="select"][value="2"] - pointerenter
select[name="select"][value="2"] - mouseover: Left (0)
select[name="select"][value="2"] - mouseenter: Left (0)
select[name="select"][value="2"] - pointerup
select[name="select"][value="2"] - mouseup: Left (0)
select[name="select"][value="2"] - click: Left (0)
`)
const [o1, o2, o3] = options
expect(o1.selected).toBe(false)
Expand All @@ -35,33 +42,22 @@ test('fires correct events on listBox select', () => {
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: ul[value="2"]
ul - pointerover
li#2[value="2"][aria-selected=false] - pointerover
ul - pointerenter
ul - mouseover: Left (0)
li#2[value="2"][aria-selected=false] - mouseover: Left (0)
ul - mouseenter: Left (0)
ul - pointermove
ul - mousemove: Left (0)
ul - pointerdown
ul - mousedown: Left (0)
ul - pointerup
ul - mouseup: Left (0)
ul - click: Left (0)
li#2[value="2"][aria-selected=true] - pointerover
ul[value="2"] - pointerenter
li#2[value="2"][aria-selected=true] - mouseover: Left (0)
ul[value="2"] - mouseenter: Left (0)
li#2[value="2"][aria-selected=true] - pointermove
li#2[value="2"][aria-selected=true] - mousemove: Left (0)
li#2[value="2"][aria-selected=true] - pointerover
ul[value="2"] - pointerenter
li#2[value="2"][aria-selected=true] - mouseover: Left (0)
ul[value="2"] - mouseenter: Left (0)
li#2[value="2"][aria-selected=true] - pointermove
li#2[value="2"][aria-selected=true] - mousemove: Left (0)
li#2[value="2"][aria-selected=true] - pointerdown
li#2[value="2"][aria-selected=true] - mousedown: Left (0)
li#2[value="2"][aria-selected=true] - pointerup
li#2[value="2"][aria-selected=true] - mouseup: Left (0)
li#2[value="2"][aria-selected=false] - pointermove
li#2[value="2"][aria-selected=false] - mousemove: Left (0)
li#2[value="2"][aria-selected=false] - pointerover
ul - pointerenter
li#2[value="2"][aria-selected=false] - mouseover: Left (0)
ul - mouseenter: Left (0)
li#2[value="2"][aria-selected=false] - pointermove
li#2[value="2"][aria-selected=false] - mousemove: Left (0)
li#2[value="2"][aria-selected=false] - pointerdown
li#2[value="2"][aria-selected=false] - mousedown: Left (0)
li#2[value="2"][aria-selected=false] - pointerup
li#2[value="2"][aria-selected=false] - mouseup: Left (0)
li#2[value="2"][aria-selected=true] - click: Left (0)
li#2[value="2"][aria-selected=true] - pointermove
li#2[value="2"][aria-selected=true] - mousemove: Left (0)
Expand Down Expand Up @@ -150,6 +146,13 @@ test('a previously focused input gets blurred', () => {
`)
})

test('throws an error if elements is neither select nor listbox', () => {
const {element} = setup(`<ul><li role='option'>foo</li></ul>`)
expect(() => userEvent.selectOptions(element, ['foo'])).toThrowError(
/neither select nor listbox/i,
)
})

test('throws an error one selected option does not match', () => {
const {select} = setupSelect({multiple: true})
expect(() =>
Expand Down
73 changes: 73 additions & 0 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {isInstanceOfElement} from '../utils'
import {setup} from './helpers/utils'

// isInstanceOfElement can be removed once the peerDependency for @testing-library/dom is bumped to a version that includes https://github.com/testing-library/dom-testing-library/pull/885
describe('check element type per isInstanceOfElement', () => {
let defaultViewDescriptor, spanDescriptor
beforeAll(() => {
defaultViewDescriptor = Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(global.document),
'defaultView',
)
spanDescriptor = Object.getOwnPropertyDescriptor(
global.window,
'HTMLSpanElement',
)
})
afterEach(() => {
Object.defineProperty(
Object.getPrototypeOf(global.document),
'defaultView',
defaultViewDescriptor,
)
Object.defineProperty(global.window, 'HTMLSpanElement', spanDescriptor)
})

test('check in regular jest environment', () => {
const {element} = setup(`<span></span>`)

expect(element.ownerDocument.defaultView).toEqual(
expect.objectContaining({
HTMLSpanElement: expect.any(Function),
}),
)

expect(isInstanceOfElement(element, 'HTMLSpanElement')).toBe(true)
expect(isInstanceOfElement(element, 'HTMLDivElement')).toBe(false)
})

test('check in detached document', () => {
const {element} = setup(`<span></span>`)

Object.defineProperty(
Object.getPrototypeOf(element.ownerDocument),
'defaultView',
{value: null},
)

expect(element.ownerDocument.defaultView).toBe(null)

expect(isInstanceOfElement(element, 'HTMLSpanElement')).toBe(true)
expect(isInstanceOfElement(element, 'HTMLDivElement')).toBe(false)
})

test('check in environment not providing constructors on window', () => {
const {element} = setup(`<span></span>`)

delete global.window.HTMLSpanElement

expect(element.ownerDocument.defaultView.HTMLSpanElement).toBe(undefined)

expect(isInstanceOfElement(element, 'HTMLSpanElement')).toBe(true)
expect(isInstanceOfElement(element, 'HTMLDivElement')).toBe(false)
})

test('throw error if element is not created by HTML*Element constructor', () => {
const doc = new Document()

// constructor is global.Element
const element = doc.createElement('span')

expect(() => isInstanceOfElement(element, 'HTMLSpanElement')).toThrow()
})
})
92 changes: 55 additions & 37 deletions src/select-options.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {createEvent, getConfig, fireEvent} from '@testing-library/dom'
import {isInstanceOfElement} from './utils'
import {click} from './click'
import {focus} from './focus'
import {hover, unhover} from './hover'
Expand Down Expand Up @@ -36,53 +37,70 @@ function selectOptionsBase(newValue, select, values, init) {

if (select.disabled || !selectedOptions.length) return

if (select.multiple) {
for (const option of selectedOptions) {
// events fired for multiple select are weird. Can't use hover...
fireEvent.pointerOver(option, init)
if (isInstanceOfElement(select, 'HTMLSelectElement')) {
if (select.multiple) {
for (const option of selectedOptions) {
// events fired for multiple select are weird. Can't use hover...
fireEvent.pointerOver(option, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(option)
fireEvent.mouseEnter(select)
fireEvent.pointerMove(option, init)
fireEvent.mouseMove(option, init)
fireEvent.pointerDown(option, init)
fireEvent.mouseDown(option, init)
focus(select, init)
fireEvent.pointerUp(option, init)
fireEvent.mouseUp(option, init)
selectOption(option)
fireEvent.click(option, init)
}
} else if (selectedOptions.length === 1) {
// the click to open the select options
click(select, init)

selectOption(selectedOptions[0])

// the browser triggers another click event on the select for the click on the option
// this second click has no 'down' phase
fireEvent.pointerOver(select, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(option)
fireEvent.mouseOver(select)
fireEvent.mouseEnter(select)
fireEvent.pointerMove(option, init)
fireEvent.mouseMove(option, init)
fireEvent.pointerDown(option, init)
fireEvent.mouseDown(option, init)
focus(select, init)
fireEvent.pointerUp(option, init)
fireEvent.mouseUp(option, init)
selectOption(option)
fireEvent.click(option, init)
fireEvent.pointerUp(select, init)
fireEvent.mouseUp(select, init)
fireEvent.click(select, init)
} else {
throw getConfig().getElementError(
`Cannot select multiple options on a non-multiple select`,
select,
)
}
} else if (selectedOptions.length === 1) {
click(select, init)
selectOption(selectedOptions[0])
} else if (select.getAttribute('role') === 'listbox') {
selectedOptions.forEach(option => {
hover(option, init)
click(option, init)
unhover(option, init)
})
} else {
throw getConfig().getElementError(
`Cannot select multiple options on a non-multiple select`,
`Cannot select options on elements that are neither select nor listbox elements`,
select,
)
}

function selectOption(option) {
if (option.getAttribute('role') === 'option') {
option?.setAttribute?.('aria-selected', newValue)

hover(option, init)
click(option, init)
unhover(option, init)
} else {
option.selected = newValue
fireEvent(
select,
createEvent('input', select, {
bubbles: true,
cancelable: false,
composed: true,
...init,
}),
)
fireEvent.change(select, init)
}
option.selected = newValue
fireEvent(
select,
createEvent('input', select, {
bubbles: true,
cancelable: false,
composed: true,
...init,
}),
)
fireEvent.change(select, init)
}
}

Expand Down
16 changes: 7 additions & 9 deletions src/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ import {focus} from './focus'
function upload(element, fileOrFiles, init) {
if (element.disabled) return

let files
let input = element

click(element, init)
if (element.tagName === 'LABEL') {
files = element.control.multiple ? fileOrFiles : [fileOrFiles]
input = element.control
} else {
files = element.multiple ? fileOrFiles : [fileOrFiles]
}

const input = element.tagName === 'LABEL' ? element.control : element

const files = (Array.isArray(fileOrFiles)
? fileOrFiles
: [fileOrFiles]
).slice(0, input.multiple ? undefined : 1)

// blur fires when the file selector pops up
blur(element, init)
Expand Down
35 changes: 34 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
import {getConfig} from '@testing-library/dom'
import {getWindowFromNode} from '@testing-library/dom/dist/helpers'

// isInstanceOfElement can be removed once the peerDependency for @testing-library/dom is bumped to a version that includes https://github.com/testing-library/dom-testing-library/pull/885
/**
* Check if an element is of a given type.
*
* @param Element The element to test
* @param string Constructor name. E.g. 'HTMLSelectElement'
*/
function isInstanceOfElement(element, elementType) {
try {
const window = getWindowFromNode(element)
// Window usually has the element constructors as properties but is not required to do so per specs
if (typeof window[elementType] === 'function') {
return element instanceof window[elementType]
}
} catch (e) {
// The document might not be associated with a window
}

// Fall back to the constructor name as workaround for test environments that
// a) not associate the document with a window
// b) not provide the constructor as property of window
if (/^HTML(\w+)Element$/.test(element.constructor.name)) {
return element.constructor.name === elementType
}

// The user passed some node that is not created in a browser-like environment
throw new Error(
`Unable to verify if element is instance of ${elementType}. Please file an issue describing your test environment: https://github.com/testing-library/dom-testing-library/issues/new`,
)
}

function isMousePressEvent(event) {
return (
Expand Down Expand Up @@ -256,7 +288,7 @@ const CLICKABLE_INPUT_TYPES = [
function isClickable(element) {
return (
element.tagName === 'BUTTON' ||
(element instanceof element.ownerDocument.defaultView.HTMLInputElement &&
(isInstanceOfElement(element, 'HTMLInputElement') &&
CLICKABLE_INPUT_TYPES.includes(element.type))
)
}
Expand Down Expand Up @@ -334,4 +366,5 @@ export {
getValue,
getSelectionRange,
isContentEditable,
isInstanceOfElement,
}

0 comments on commit 8f3cd01

Please sign in to comment.