diff --git a/.size-snapshot.json b/.size-snapshot.json index a5e568a4..363648a9 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,7 +1,7 @@ { "dist/dom-testing-library.umd.js": { - "bundled": 122519, - "minified": 52052, - "gzipped": 15632 + "bundled": 146890, + "minified": 53947, + "gzipped": 15849 } } diff --git a/jest.config.js b/jest.config.js index 4160d66d..435c4715 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,20 @@ -const jestConfig = require('kcd-scripts/jest') +const baseConfig = require('kcd-scripts/jest') -module.exports = Object.assign(jestConfig, { - testEnvironment: 'jest-environment-jsdom', -}) +module.exports = { + collectCoverageFrom: baseConfig.collectCoverageFrom, + coverageThreshold: baseConfig.coverageThreshold, + projects: [ + { + ...baseConfig, + displayName: 'jsdom', + testEnvironment: 'jest-environment-jsdom', + }, + { + ...baseConfig, + displayName: 'node', + testEnvironment: 'jest-environment-node', + }, + ], + // this is for eslint + modulePaths: baseConfig.modulePaths, +} diff --git a/package.json b/package.json index 27221312..b82e6473 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,15 @@ "typings" ], "dependencies": { - "mutationobserver-shim": "^0.3.2", + "@sheerun/mutationobserver-shim": "^0.3.2", "pretty-format": "^23.6.0", "wait-for-expect": "^1.0.0" }, "devDependencies": { "dtslint": "^0.3.0", - "jest-dom": "^1.7.0", + "jest-dom": "^2.0.4", "jest-in-case": "^1.0.2", + "jsdom": "^12.2.0", "kcd-scripts": "^0.41.0", "microbundle": "^0.4.4" }, diff --git a/src/__tests__/__snapshots__/wait-for-element.js.snap b/src/__tests__/__snapshots__/wait-for-element.js.snap index 2ffcf686..07f5e46d 100644 --- a/src/__tests__/__snapshots__/wait-for-element.js.snap +++ b/src/__tests__/__snapshots__/wait-for-element.js.snap @@ -79,3 +79,4 @@ exports[`it waits for the callback to return a value and only reacts to DOM muta /> `; + diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 47d71609..d49d62c8 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -1,8 +1,9 @@ import 'jest-dom/extend-expect' import {render} from './helpers/test-utils' +import document from './helpers/document' beforeEach(() => { - window.Cypress = null + document.defaultView.Cypress = null }) test('query can return null', () => { @@ -169,9 +170,7 @@ test('get element by its alt text', () => { finding nemo poster , `) - expect(getByAltText(/fin.*nem.*poster$/i).src).toBe( - 'http://localhost/finding-nemo.png', - ) + expect(getByAltText(/fin.*nem.*poster$/i).src).toContain('/finding-nemo.png') }) test('query/get element by its title', () => { @@ -510,7 +509,7 @@ test('test the debug helper prints the dom state here', () => { }) test('get throws a useful error message without DOM in Cypress', () => { - window.Cypress = {} + document.defaultView.Cypress = {} const { getByLabelText, getBySelectText, diff --git a/src/__tests__/events.js b/src/__tests__/events.js index cb3e7483..10b15f69 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,4 +1,5 @@ import {fireEvent} from '..' +import document from './helpers/document' const eventTypes = [ { @@ -171,7 +172,7 @@ test('assigning a value to a target that cannot have a value throws an error', ( test('assigning the files property on an input', () => { const node = document.createElement('input') - const file = new File(['(⌐□_□)'], 'chucknorris.png', { + const file = new document.defaultView.File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png', }) fireEvent.change(node, {target: {files: [file]}}) diff --git a/src/__tests__/example.js b/src/__tests__/example.js index da0bb8ba..f6a96dd6 100644 --- a/src/__tests__/example.js +++ b/src/__tests__/example.js @@ -2,6 +2,7 @@ import {getByLabelText, getByText, getByTestId, queryByTestId, wait} from '../' // adds special assertions like toHaveTextContent import 'jest-dom/extend-expect' +import document from './helpers/document' function getExampleDOM() { // This is just a raw example of setting up some DOM diff --git a/src/__tests__/get-queries-for-element.js b/src/__tests__/get-queries-for-element.js index 1a6afba8..44d2e43d 100644 --- a/src/__tests__/get-queries-for-element.js +++ b/src/__tests__/get-queries-for-element.js @@ -1,5 +1,6 @@ import {getQueriesForElement} from '../get-queries-for-element' import {queries} from '..' +import document from './helpers/document' test('uses default queries', () => { const container = document.createElement('div') diff --git a/src/__tests__/helpers/document.js b/src/__tests__/helpers/document.js new file mode 100644 index 00000000..1df46c07 --- /dev/null +++ b/src/__tests__/helpers/document.js @@ -0,0 +1,9 @@ +let testWindow = typeof window === 'undefined' ? undefined : window + +if (typeof window === 'undefined') { + const {JSDOM} = require('jsdom') + const dom = new JSDOM() + testWindow = dom.window +} + +module.exports = testWindow.document diff --git a/src/__tests__/helpers/test-utils.js b/src/__tests__/helpers/test-utils.js index a21a316b..46d270d9 100644 --- a/src/__tests__/helpers/test-utils.js +++ b/src/__tests__/helpers/test-utils.js @@ -1,4 +1,5 @@ import {getQueriesForElement} from '../../get-queries-for-element' +import document from './document' function render(html) { const container = document.createElement('div') diff --git a/src/__tests__/pretty-dom.js b/src/__tests__/pretty-dom.js index 312cf78b..b2fe3d02 100644 --- a/src/__tests__/pretty-dom.js +++ b/src/__tests__/pretty-dom.js @@ -1,5 +1,6 @@ import {prettyDOM} from '../pretty-dom' import {render} from './helpers/test-utils' +import document from './helpers/document' test('it prints out the given DOM element tree', () => { const {container} = render('
Hello World!
') diff --git a/src/__tests__/wait-for-dom-change.js b/src/__tests__/wait-for-dom-change.js index 0034f9bb..4d43d2df 100644 --- a/src/__tests__/wait-for-dom-change.js +++ b/src/__tests__/wait-for-dom-change.js @@ -2,6 +2,7 @@ import {waitForDomChange} from '../' // adds special assertions like toBeTruthy import 'jest-dom/extend-expect' import {render} from './helpers/test-utils' +import document from './helpers/document' const skipSomeTime = delayMs => new Promise(resolve => setTimeout(resolve, delayMs)) @@ -15,7 +16,11 @@ test('it waits for the next DOM mutation', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - waitForDomChange().then(successHandler, errorHandler) + if (typeof window === 'undefined') { + waitForDomChange({container: document}).then(successHandler, errorHandler) + } else { + waitForDomChange().then(successHandler, errorHandler) + } // Promise callbacks are always asynchronous. expect(successHandler).toHaveBeenCalledTimes(0) @@ -83,7 +88,14 @@ test('it throws if timeout is exceeded', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - waitForDomChange({timeout: 70}).then(successHandler, errorHandler) + if (typeof window === 'undefined') { + waitForDomChange({container: document, timeout: 70}).then( + successHandler, + errorHandler, + ) + } else { + waitForDomChange({timeout: 70}).then(successHandler, errorHandler) + } await skipSomeTimeForMutationObserver(100) diff --git a/src/__tests__/wait-for-element.js b/src/__tests__/wait-for-element.js index d8883d06..aa9a8f9f 100644 --- a/src/__tests__/wait-for-element.js +++ b/src/__tests__/wait-for-element.js @@ -2,6 +2,7 @@ import {waitForElement, wait} from '../' // adds special assertions like toBeTruthy import 'jest-dom/extend-expect' import {render} from './helpers/test-utils' +import document from './helpers/document' const skipSomeTime = delayMs => new Promise(resolve => setTimeout(resolve, delayMs)) @@ -320,7 +321,14 @@ test('works if a container is not defined', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - waitForElement(callback).then(successHandler, errorHandler) + if (typeof window === 'undefined') { + waitForElement(callback, {container: document}).then( + successHandler, + errorHandler, + ) + } else { + waitForElement(callback).then(successHandler, errorHandler) + } await skipSomeTimeForMutationObserver() @@ -342,7 +350,14 @@ test('throws an error if callback is not a function', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - waitForElement().then(successHandler, errorHandler) + if (typeof window === 'undefined') { + waitForElement(undefined, {container: document}).then( + successHandler, + errorHandler, + ) + } else { + waitForElement().then(successHandler, errorHandler) + } await skipSomeTimeForMutationObserver() diff --git a/src/__tests__/wait.js b/src/__tests__/wait.js index 0efbc93d..95e1ace0 100644 --- a/src/__tests__/wait.js +++ b/src/__tests__/wait.js @@ -20,6 +20,6 @@ test('can just be used for a next tick thing', async () => { const after = Date.now() // if it's greater than 50 then `wait` used a timeout // but it should have resolved sooner. It really should be 0 - expect(after - before).not.toBeGreaterThan(50) + expect(after - before).not.toBeGreaterThan(60) expect(spy).toHaveBeenCalledTimes(1) }) diff --git a/src/events.js b/src/events.js index 51073c5b..7eedfb49 100644 --- a/src/events.js +++ b/src/events.js @@ -1,218 +1,201 @@ -const { - AnimationEvent, - ClipboardEvent, - CompositionEvent, - DragEvent, - Event, - FocusEvent, - InputEvent, - KeyboardEvent, - MouseEvent, - ProgressEvent, - TouchEvent, - TransitionEvent, - UIEvent, - WheelEvent, -} = typeof window === 'undefined' ? /* istanbul ignore next */ global : window - const eventMap = { // Clipboard Events copy: { - EventType: ClipboardEvent, + EventType: 'ClipboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, cut: { - EventType: ClipboardEvent, + EventType: 'ClipboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, paste: { - EventType: ClipboardEvent, + EventType: 'ClipboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, // Composition Events compositionEnd: { - EventType: CompositionEvent, + EventType: 'CompositionEvent', defaultInit: {bubbles: true, cancelable: true}, }, compositionStart: { - EventType: CompositionEvent, + EventType: 'CompositionEvent', defaultInit: {bubbles: true, cancelable: true}, }, compositionUpdate: { - EventType: CompositionEvent, + EventType: 'CompositionEvent', defaultInit: {bubbles: true, cancelable: false}, }, // Keyboard Events keyDown: { - EventType: KeyboardEvent, + EventType: 'KeyboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, keyPress: { - EventType: KeyboardEvent, + EventType: 'KeyboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, keyUp: { - EventType: KeyboardEvent, + EventType: 'KeyboardEvent', defaultInit: {bubbles: true, cancelable: true}, }, // Focus Events focus: { - EventType: FocusEvent, + EventType: 'FocusEvent', defaultInit: {bubbles: false, cancelable: false}, }, blur: { - EventType: FocusEvent, + EventType: 'FocusEvent', defaultInit: {bubbles: false, cancelable: false}, }, // Form Events change: { - EventType: InputEvent, + EventType: 'InputEvent', defaultInit: {bubbles: true, cancelable: true}, }, input: { - EventType: InputEvent, + EventType: 'InputEvent', defaultInit: {bubbles: true, cancelable: true}, }, invalid: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: true}, }, submit: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: true, cancelable: true}, }, // Mouse Events click: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true, button: 0}, }, contextMenu: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, dblClick: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, drag: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: true}, }, dragEnd: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: false}, }, dragEnter: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: true}, }, dragExit: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: false}, }, dragLeave: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: false}, }, dragOver: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: true}, }, dragStart: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: true}, }, drop: { - EventType: DragEvent, + EventType: 'DragEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseDown: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseEnter: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseLeave: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseMove: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseOut: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseOver: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, mouseUp: { - EventType: MouseEvent, + EventType: 'MouseEvent', defaultInit: {bubbles: true, cancelable: true}, }, // Selection Events select: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: true, cancelable: false}, }, // Touch Events touchCancel: { - EventType: TouchEvent, + EventType: 'TouchEvent', defaultInit: {bubbles: true, cancelable: false}, }, touchEnd: { - EventType: TouchEvent, + EventType: 'TouchEvent', defaultInit: {bubbles: true, cancelable: true}, }, touchMove: { - EventType: TouchEvent, + EventType: 'TouchEvent', defaultInit: {bubbles: true, cancelable: true}, }, touchStart: { - EventType: TouchEvent, + EventType: 'TouchEvent', defaultInit: {bubbles: true, cancelable: true}, }, // UI Events scroll: { - EventType: UIEvent, + EventType: 'UIEvent', defaultInit: {bubbles: false, cancelable: false}, }, // Wheel Events wheel: { - EventType: WheelEvent, + EventType: 'WheelEvent', defaultInit: {bubbles: true, cancelable: true}, }, // Media Events abort: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, canPlay: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, canPlayThrough: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, durationChange: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, emptied: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, encrypted: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, ended: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, // error: { @@ -220,90 +203,90 @@ const eventMap = { // defaultInit: {bubbles: false, cancelable: false}, // }, loadedData: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, loadedMetadata: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, loadStart: { - EventType: ProgressEvent, + EventType: 'ProgressEvent', defaultInit: {bubbles: false, cancelable: false}, }, pause: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, play: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, playing: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, progress: { - EventType: ProgressEvent, + EventType: 'ProgressEvent', defaultInit: {bubbles: false, cancelable: false}, }, rateChange: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, seeked: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, seeking: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, stalled: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, suspend: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, timeUpdate: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, volumeChange: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, waiting: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, // Image Events load: { - EventType: UIEvent, + EventType: 'UIEvent', defaultInit: {bubbles: false, cancelable: false}, }, error: { - EventType: Event, + EventType: 'Event', defaultInit: {bubbles: false, cancelable: false}, }, // Animation Events animationStart: { - EventType: AnimationEvent, + EventType: 'AnimationEvent', defaultInit: {bubbles: true, cancelable: false}, }, animationEnd: { - EventType: AnimationEvent, + EventType: 'AnimationEvent', defaultInit: {bubbles: true, cancelable: false}, }, animationIteration: { - EventType: AnimationEvent, + EventType: 'AnimationEvent', defaultInit: {bubbles: true, cancelable: false}, }, // Transition Events transitionEnd: { - EventType: TransitionEvent, + EventType: 'TransitionEvent', defaultInit: {bubbles: true, cancelable: true}, }, } @@ -316,7 +299,7 @@ function fireEvent(element, event) { return element.dispatchEvent(event) } -Object.entries(eventMap).forEach(([key, {EventType = Event, defaultInit}]) => { +Object.entries(eventMap).forEach(([key, {EventType, defaultInit}]) => { const eventName = key.toLowerCase() fireEvent[key] = (node, init) => { @@ -334,7 +317,9 @@ Object.entries(eventMap).forEach(([key, {EventType = Event, defaultInit}]) => { value: files, }) } - const event = new EventType(eventName, eventInit) + const window = node.ownerDocument.defaultView + const EventConstructor = window[EventType] || window.Event + const event = new EventConstructor(eventName, eventInit) return fireEvent(node, event) } }) diff --git a/src/get-node-text.js b/src/get-node-text.js index cace4734..1f7c3ecd 100644 --- a/src/get-node-text.js +++ b/src/get-node-text.js @@ -1,4 +1,6 @@ function getNodeText(node) { + const window = node.ownerDocument.defaultView + return Array.from(node.childNodes) .filter( child => diff --git a/src/query-helpers.js b/src/query-helpers.js index 8a8e7751..3f3e7e56 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -8,6 +8,7 @@ function debugDOM(htmlElement) { typeof process !== 'undefined' && process.versions !== undefined && process.versions.node !== undefined + const window = htmlElement.ownerDocument.defaultView const inCypress = typeof window !== 'undefined' && window.Cypress /* istanbul ignore else */ if (inCypress) { diff --git a/src/wait-for-dom-change.js b/src/wait-for-dom-change.js index 3a9069f6..ead39b2c 100644 --- a/src/wait-for-dom-change.js +++ b/src/wait-for-dom-change.js @@ -1,7 +1,7 @@ -import 'mutationobserver-shim' +import MutationObserver from '@sheerun/mutationobserver-shim' function waitForDomChange({ - container = document, + container = getDocument(), timeout = 4500, mutationObserverOptions = { subtree: true, @@ -12,7 +12,13 @@ function waitForDomChange({ } = {}) { return new Promise((resolve, reject) => { const timer = setTimeout(onTimeout, timeout) - const observer = new window.MutationObserver(onMutation) + /* istanbul ignore next */ + const MutationObserverConstructor = + typeof window !== 'undefined' && + typeof window.MutationObserver !== 'undefined' + ? window.MutationObserver + : MutationObserver + const observer = new MutationObserverConstructor(onMutation) observer.observe(container, mutationObserverOptions) function onDone(error, result) { @@ -33,4 +39,12 @@ function waitForDomChange({ }) } +function getDocument() { + /* istanbul ignore if */ + if (typeof window === 'undefined') { + throw new Error('Could not find default container') + } + return window.document +} + export {waitForDomChange} diff --git a/src/wait-for-element.js b/src/wait-for-element.js index a632020d..01195445 100644 --- a/src/wait-for-element.js +++ b/src/wait-for-element.js @@ -1,9 +1,9 @@ -import 'mutationobserver-shim' +import MutationObserver from '@sheerun/mutationobserver-shim' function waitForElement( callback, { - container = document, + container = getDocument(), timeout = 4500, mutationObserverOptions = { subtree: true, @@ -19,7 +19,13 @@ function waitForElement( } let lastError const timer = setTimeout(onTimeout, timeout) - const observer = new window.MutationObserver(onMutation) + /* istanbul ignore next */ + const MutationObserverConstructor = + typeof window !== 'undefined' && + typeof window.MutationObserver !== 'undefined' + ? window.MutationObserver + : MutationObserver + const observer = new MutationObserverConstructor(onMutation) observer.observe(container, mutationObserverOptions) function onDone(error, result) { clearTimeout(timer) @@ -50,4 +56,12 @@ function waitForElement( }) } +function getDocument() { + /* istanbul ignore if */ + if (typeof window === 'undefined') { + throw new Error('Could not find default container') + } + return window.document +} + export {waitForElement}