Skip to content

Commit

Permalink
refactor: replace isInstanceOfElement (#617)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche authored Mar 24, 2021
1 parent 391e513 commit 7ff9a9a
Show file tree
Hide file tree
Showing 18 changed files with 124 additions and 179 deletions.
9 changes: 5 additions & 4 deletions src/__tests__/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {eventMap} from '@testing-library/dom/dist/event-map'
import {isElementType} from '../../utils'
// this is pretty helpful:
// https://codesandbox.io/s/quizzical-worker-eo909

Expand Down Expand Up @@ -126,7 +127,7 @@ function addEventListener(el, type, listener, options) {
}

function getElementValue(element) {
if (element.tagName === 'SELECT' && element.multiple) {
if (isElementType(element, 'select') && element.multiple) {
return JSON.stringify(Array.from(element.selectedOptions).map(o => o.value))
} else if (element.getAttribute('role') === 'listbox') {
return JSON.stringify(
Expand All @@ -137,7 +138,7 @@ function getElementValue(element) {
} else if (
element.type === 'checkbox' ||
element.type === 'radio' ||
element.tagName === 'BUTTON'
isElementType(element, 'button')
) {
// handled separately
return null
Expand All @@ -156,7 +157,7 @@ function getElementDisplayName(element) {
element.htmlFor ? `[for="${element.htmlFor}"]` : null,
value ? `[value=${value}]` : null,
hasChecked ? `[checked=${element.checked}]` : null,
element.tagName === 'OPTION' ? `[selected=${element.selected}]` : null,
isElementType(element, 'option') ? `[selected=${element.selected}]` : null,
element.getAttribute('role') === 'option'
? `[aria-selected=${element.getAttribute('aria-selected')}]`
: null,
Expand Down Expand Up @@ -197,7 +198,7 @@ function addListeners(element, {eventHandlers = {}} = {}) {
})
}
// prevent default of submits in tests
if (element.tagName === 'FORM') {
if (isElementType(element, 'form')) {
addEventListener(element, 'submit', e => e.preventDefault())
}

Expand Down
103 changes: 36 additions & 67 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,44 @@
import { screen } from '@testing-library/dom'
import {isInstanceOfElement, isVisible} from '../utils'
import {screen} from '@testing-library/dom'
import {isElementType, isVisible} 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)
describe('check element type per namespace, tagname and props', () => {
test('check in HTML document', () => {
const {elements} = setup(`<input readonly="true"/><textarea/>`)

expect(isElementType(elements[0], 'input')).toBe(true)
expect(isElementType(elements[0], 'input', {readOnly: false})).toBe(false)
expect(isElementType(elements[1], 'input')).toBe(false)
expect(isElementType(elements[1], ['input', 'textarea'])).toBe(true)
expect(
isElementType(elements[1], ['input', 'textarea'], {readOnly: false}),
).toBe(true)
})

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

expect(element.ownerDocument.defaultView).toEqual(
expect.objectContaining({
HTMLSpanElement: expect.any(Function),
}),
test('check in XML document', () => {
// const {element} = setup(`<input readonly="true"/>`)
const dom = new DOMParser().parseFromString(
`
<root xmlns="http://example.com/foo">
<input readonly="true"/>
<input xmlns="http://www.w3.org/1999/xhtml" readonly="true"/>
</root>
`,
'application/xml',
)

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()
const xmlInput = dom.getElementsByTagNameNS(
'http://example.com/foo',
'input',
)[0]
const htmlInput = dom.getElementsByTagNameNS(
'http://www.w3.org/1999/xhtml',
'input',
)[0]

expect(isElementType(xmlInput, 'input')).toBe(false)
expect(isElementType(htmlInput, 'input')).toBe(true)
expect(isElementType(htmlInput, 'input', {readOnly: true})).toBe(true)
expect(isElementType(htmlInput, 'input', {readOnly: false})).toBe(false)
})
})

Expand Down
18 changes: 7 additions & 11 deletions src/clear.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import {isDisabled, isInstanceOfElement} from './utils'
import {isDisabled, isElementType} from './utils'
import {type} from './type'

function clear(element: Element) {
if (
!isInstanceOfElement(element, 'HTMLInputElement') &&
!isInstanceOfElement(element, 'HTMLTextAreaElement')
) {
if (!isElementType(element, ['input', 'textarea'])) {
// TODO: support contenteditable
throw new Error(
'clear currently only supports input and textarea elements.',
)
}
const el = element as HTMLInputElement | HTMLTextAreaElement

if (isDisabled(el)) {
if (isDisabled(element)) {
return
}

// TODO: track the selection range ourselves so we don't have to do this input "type" trickery
// just like cypress does: https://github.com/cypress-io/cypress/blob/8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683/packages/driver/src/dom/selection.ts#L16-L37

const elementType = el.type
const elementType = element.type

if (elementType !== 'textarea') {
// setSelectionRange is not supported on certain types of inputs, e.g. "number" or "email"
Expand All @@ -30,13 +26,13 @@ function clear(element: Element) {
type(element, '{selectall}{del}', {
delay: 0,
initialSelectionStart:
el.selectionStart ?? /* istanbul ignore next */ undefined,
element.selectionStart ?? /* istanbul ignore next */ undefined,
initialSelectionEnd:
el.selectionEnd ?? /* istanbul ignore next */ undefined,
element.selectionEnd ?? /* istanbul ignore next */ undefined,
})

if (elementType !== 'textarea') {
;(el as HTMLInputElement).type = elementType
;(element as HTMLInputElement).type = elementType
}
}

Expand Down
15 changes: 7 additions & 8 deletions src/click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
isLabelWithInternallyDisabledControl,
isFocusable,
isDisabled,
isInstanceOfElement,
isElementType,
} from './utils'
import {hover} from './hover'
import {blur} from './blur'
Expand Down Expand Up @@ -119,14 +119,13 @@ function click(
) {
if (!skipHover) hover(element, init)

if (isInstanceOfElement(element, 'HTMLLabelElement')) {
clickLabel(element as HTMLLabelElement, init, {clickCount})
} else if (isInstanceOfElement(element, 'HTMLInputElement')) {
const el = element as HTMLInputElement
if (el.type === 'checkbox' || el.type === 'radio') {
clickBooleanElement(el, init, {clickCount})
if (isElementType(element, 'label')) {
clickLabel(element, init, {clickCount})
} else if (isElementType(element, 'input')) {
if (element.type === 'checkbox' || element.type === 'radio') {
clickBooleanElement(element, init, {clickCount})
} else {
clickElement(el, init, {clickCount})
clickElement(element, init, {clickCount})
}
} else {
clickElement(element, init, {clickCount})
Expand Down
4 changes: 2 additions & 2 deletions src/keyboard/plugins/arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
*/

import {behaviorPlugin} from '../types'
import {isInstanceOfElement, setSelectionRangeIfNecessary} from '../../utils'
import {isElementType, setSelectionRangeIfNecessary} from '../../utils'

export const keydownBehavior: behaviorPlugin[] = [
{
// TODO: implement for textarea and contentEditable
matches: (keyDef, element) =>
(keyDef.key === 'ArrowLeft' || keyDef.key === 'ArrowRight') &&
isInstanceOfElement(element, 'HTMLInputElement'),
isElementType(element, 'input'),
handle: (keyDef, element) => {
const {selectionStart, selectionEnd} = element as HTMLInputElement

Expand Down
17 changes: 6 additions & 11 deletions src/keyboard/plugins/character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
calculateNewValue,
getValue,
isContentEditable,
isInstanceOfElement,
isElementType,
isValidDateValue,
isValidInputTimeValue,
} from '../../utils'
Expand All @@ -19,8 +19,7 @@ export const keypressBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key?.length === 1 &&
isInstanceOfElement(element, 'HTMLInputElement') &&
(element as HTMLInputElement).type === 'time',
isElementType(element, 'input', {type: 'time'}),
handle: (keyDef, element, options, state) => {
let newEntry = keyDef.key as string

Expand Down Expand Up @@ -62,8 +61,7 @@ export const keypressBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key?.length === 1 &&
isInstanceOfElement(element, 'HTMLInputElement') &&
(element as HTMLInputElement).type === 'date',
isElementType(element, 'input', {type: 'date'}),
handle: (keyDef, element, options, state) => {
let newEntry = keyDef.key as string

Expand Down Expand Up @@ -103,8 +101,7 @@ export const keypressBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key?.length === 1 &&
isInstanceOfElement(element, 'HTMLInputElement') &&
(element as HTMLInputElement).type === 'number',
isElementType(element, 'input', {type: 'number'}),
handle: (keyDef, element, options, state) => {
if (!/[\d.\-e]/.test(keyDef.key as string)) {
return
Expand Down Expand Up @@ -140,8 +137,7 @@ export const keypressBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key?.length === 1 &&
(isInstanceOfElement(element, 'HTMLInputElement') ||
isInstanceOfElement(element, 'HTMLTextAreaElement') ||
(isElementType(element, ['input', 'textarea']) ||
isContentEditable(element)),
handle: (keyDef, element) => {
const {newValue, newSelectionStart} = calculateNewValue(
Expand All @@ -163,8 +159,7 @@ export const keypressBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key === 'Enter' &&
(isInstanceOfElement(element, 'HTMLTextAreaElement') ||
isContentEditable(element)),
(isElementType(element, 'textarea') || isContentEditable(element)),
handle: (keyDef, element, options, state) => {
const {newValue, newSelectionStart} = calculateNewValue(
'\n',
Expand Down
5 changes: 2 additions & 3 deletions src/keyboard/plugins/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {behaviorPlugin} from '../types'
import {
getValue,
isContentEditable,
isInstanceOfElement,
isElementType,
setSelectionRangeIfNecessary,
} from '../../utils'
import {fireInputEventIfNeeded} from '../shared'
Expand All @@ -17,8 +17,7 @@ export const keydownBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
(keyDef.key === 'Home' || keyDef.key === 'End') &&
(isInstanceOfElement(element, 'HTMLInputElement') ||
isInstanceOfElement(element, 'HTMLTextAreaElement') ||
(isElementType(element, ['input', 'textarea']) ||
isContentEditable(element)),
handle: (keyDef, element) => {
// This could probably been improved by collapsing a selection range
Expand Down
8 changes: 3 additions & 5 deletions src/keyboard/plugins/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import {fireEvent} from '@testing-library/dom'
import {getValue, isClickableInput, isInstanceOfElement} from '../../utils'
import {getValue, isClickableInput, isElementType} from '../../utils'
import {getKeyEventProps, getMouseEventProps} from '../getEventProps'
import {fireInputEventIfNeeded} from '../shared'
import {behaviorPlugin} from '../types'
Expand Down Expand Up @@ -78,16 +78,14 @@ export const keypressBehavior: behaviorPlugin[] = [
keyDef.key === 'Enter' &&
(isClickableInput(element) ||
// Links with href defined should handle Enter the same as a click
(isInstanceOfElement(element, 'HTMLAnchorElement') &&
Boolean((element as HTMLAnchorElement).href))),
(isElementType(element, 'a') && Boolean(element.href))),
handle: (keyDef, element, options, state) => {
fireEvent.click(element, getMouseEventProps(state))
},
},
{
matches: (keyDef, element) =>
keyDef.key === 'Enter' &&
isInstanceOfElement(element, 'HTMLInputElement'),
keyDef.key === 'Enter' && isElementType(element, 'input'),
handle: (keyDef, element) => {
const form = (element as HTMLInputElement).form

Expand Down
5 changes: 2 additions & 3 deletions src/keyboard/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {behaviorPlugin} from '../types'
import {isInstanceOfElement} from '../../utils'
import {isElementType} from '../../utils'
import * as arrowKeys from './arrow'
import * as controlKeys from './control'
import * as characterKeys from './character'
Expand All @@ -9,8 +9,7 @@ export const replaceBehavior: behaviorPlugin[] = [
{
matches: (keyDef, element) =>
keyDef.key === 'selectall' &&
(isInstanceOfElement(element, 'HTMLInputElement') ||
isInstanceOfElement(element, 'HTMLTextAreaElement')),
isElementType(element, ['input', 'textarea']),
handle: (keyDef, element) => {
;(element as HTMLInputElement).select()
},
Expand Down
10 changes: 2 additions & 8 deletions src/keyboard/shared/fireInputEventIfNeeded.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {fireEvent} from '@testing-library/dom'
import {
isInstanceOfElement,
isElementType,
isClickableInput,
getValue,
isContentEditable,
Expand Down Expand Up @@ -53,11 +53,5 @@ export function fireInputEventIfNeeded({
}

function isReadonly(element: Element): boolean {
if (
!isInstanceOfElement(element, 'HTMLInputElement') &&
!isInstanceOfElement(element, 'HTMLTextAreaElement')
) {
return false
}
return (element as HTMLInputElement | HTMLTextAreaElement).readOnly
return isElementType(element, ['input', 'textarea'], {readOnly: true})
}
Loading

0 comments on commit 7ff9a9a

Please sign in to comment.