From b4330c40cb8720a846c74187d781850618bb22a9 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Mon, 1 Feb 2021 22:01:12 +0100 Subject: [PATCH] fix: add isInstanceOfElement helper (#552) --- src/__tests__/utils.js | 73 ++++++++++++++++++++++++++++++++++++++++++ src/select-options.js | 3 +- src/utils.js | 35 +++++++++++++++++++- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/utils.js diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js new file mode 100644 index 00000000..bdd0d0e8 --- /dev/null +++ b/src/__tests__/utils.js @@ -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(``) + + 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(``) + + 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(``) + + 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() + }) +}) diff --git a/src/select-options.js b/src/select-options.js index 1ca6777d..9620814e 100644 --- a/src/select-options.js +++ b/src/select-options.js @@ -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' @@ -36,7 +37,7 @@ function selectOptionsBase(newValue, select, values, init) { if (select.disabled || !selectedOptions.length) return - if (select instanceof HTMLSelectElement) { + if (isInstanceOfElement(select, 'HTMLSelectElement')) { if (select.multiple) { for (const option of selectedOptions) { // events fired for multiple select are weird. Can't use hover... diff --git a/src/utils.js b/src/utils.js index 0dc0c9c9..96578020 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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 ( @@ -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)) ) } @@ -334,4 +366,5 @@ export { getValue, getSelectionRange, isContentEditable, + isInstanceOfElement, }