Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: replace isInstanceOfElement #617

Merged
merged 1 commit into from Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/__tests__/helpers/utils.js
@@ -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
@@ -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
@@ -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
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
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
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
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
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
@@ -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
@@ -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})
}