diff --git a/.size-snapshot.json b/.size-snapshot.json index 094d20cb..8b346d5f 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,7 +1,7 @@ { "dist/dom-testing-library.umd.js": { - "bundled": 115443, - "minified": 50960, - "gzipped": 15294 + "bundled": 144335, + "minified": 52790, + "gzipped": 15639 } } diff --git a/jest.config.js b/jest.config.js index 4160d66d..feca704e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ const jestConfig = require('kcd-scripts/jest') module.exports = Object.assign(jestConfig, { - testEnvironment: 'jest-environment-jsdom', + testEnvironment: 'jest-environment-node', }) diff --git a/package.json b/package.json index 27221312..b47bc1ee 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,9 @@ "build": "kcd-scripts build && kcd-scripts build --bundle umd --no-clean", "lint": "kcd-scripts lint", "test": "kcd-scripts test", + "test:all": "npm test && npm test -- --env jsdom", "test:update": "npm test -- --updateSnapshot --coverage", - "validate": "kcd-scripts validate", + "validate": "kcd-scripts validate build,lint,test:all", "setup": "npm install && npm run validate -s", "precommit": "kcd-scripts precommit", "dtslint": "dtslint typings" @@ -38,14 +39,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__/element-queries.js b/src/__tests__/element-queries.js index f0e0f6db..5374ec6b 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', () => { @@ -148,9 +149,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', () => { @@ -489,7 +488,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-element.js b/src/__tests__/wait-for-element.js index 1feaa619..247309e4 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' async function skipSomeTime(delayMs) { await new Promise(resolve => setTimeout(resolve, delayMs)) @@ -104,7 +105,16 @@ test('it waits for the next DOM mutation with default callback', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - const promise = waitForElement().then(successHandler, errorHandler) + let promise + + if (typeof window !== 'undefined' && typeof window.document !== 'undefined') { + promise = waitForElement().then(successHandler, errorHandler) + } else { + promise = waitForElement(undefined, {container: document}).then( + successHandler, + errorHandler, + ) + } // Promise callbacks are always asynchronous. expect(successHandler).toHaveBeenCalledTimes(0) diff --git a/src/events.js b/src/events.js index 51073c5b..c50fb59a 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,28 +299,32 @@ function fireEvent(element, event) { return element.dispatchEvent(event) } -Object.entries(eventMap).forEach(([key, {EventType = Event, defaultInit}]) => { - const eventName = key.toLowerCase() +Object.entries(eventMap).forEach( + ([key, {EventType = 'Event', defaultInit}]) => { + const eventName = key.toLowerCase() - fireEvent[key] = (node, init) => { - const eventInit = {...defaultInit, ...init} - const {target: {value, files, ...targetProperties} = {}} = eventInit - Object.assign(node, targetProperties) - if (value !== undefined) { - setNativeValue(node, value) - } - if (files !== undefined) { - // input.files is a read-only property so this is not allowed: - // input.files = [file] - // so we have to use this workaround to set the property - Object.defineProperty(node, 'files', { - value: files, - }) + fireEvent[key] = (node, init) => { + const eventInit = {...defaultInit, ...init} + const {target: {value, files, ...targetProperties} = {}} = eventInit + Object.assign(node, targetProperties) + if (value !== undefined) { + setNativeValue(node, value) + } + if (files !== undefined) { + // input.files is a read-only property so this is not allowed: + // input.files = [file] + // so we have to use this workaround to set the property + Object.defineProperty(node, 'files', { + value: files, + }) + } + const window = node.ownerDocument.defaultView + const EventConstructor = window[EventType] || window.Event + const event = new EventConstructor(eventName, eventInit) + return fireEvent(node, event) } - const event = new EventType(eventName, eventInit) - return fireEvent(node, event) - } -}) + }, +) // function written after some investigation here: // https://github.com/facebook/react/issues/10135#issuecomment-401496776 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-element.js b/src/wait-for-element.js index 1bed5511..e63d0e7c 100644 --- a/src/wait-for-element.js +++ b/src/wait-for-element.js @@ -1,9 +1,21 @@ -import 'mutationobserver-shim' +import MutationObserver from '@sheerun/mutationobserver-shim' + +function windowFor(container) { + if (container.defaultView) { + return container.defaultView + } + + if (container.ownerDocument) { + return container.ownerDocument.defaultView + } + + throw new Error('No window for container') +} function waitForElement( callback = undefined, { - container = document, + container = getDocument(), timeout = 4500, mutationObserverOptions = { subtree: true, @@ -46,7 +58,10 @@ function waitForElement( onDone(lastError || new Error('Timed out in waitForElement.'), null) } timer = setTimeout(onTimeout, timeout) - observer = new window.MutationObserver(onMutation) + const window = windowFor(container) + const MutationObserverConstructor = + window.MutationObserver || MutationObserver + observer = new MutationObserverConstructor(onMutation) observer.observe(container, mutationObserverOptions) if (callback !== undefined) { onMutation() @@ -54,4 +69,11 @@ function waitForElement( }) } +function getDocument() { + if (typeof document === 'undefined') { + throw new Error('Could not find default container') + } + return document +} + export {waitForElement}