-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cleanup): automatically cleanup if afterEach is detected #430
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// makes it so people can import from '@testing-library/react/pure' | ||
module.exports = require('./dist/pure') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react' | ||
|
||
let render | ||
beforeAll(() => { | ||
process.env.RTL_SKIP_AUTO_CLEANUP = 'true' | ||
const rtl = require('../') | ||
render = rtl.render | ||
}) | ||
|
||
// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set | ||
// then we DON'T auto-wire up the afterEach for folks | ||
test('first', () => { | ||
render(<div>hi</div>) | ||
}) | ||
|
||
test('second', () => { | ||
expect(document.body.innerHTML).toEqual('<div><div>hi</div></div>') | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react' | ||
import {render} from '../' | ||
|
||
// This just verifies that by importing RTL in an | ||
// environment which supports afterEach (like jest) | ||
// we'll get automatic cleanup between tests. | ||
test('first', () => { | ||
render(<div>hi</div>) | ||
}) | ||
|
||
test('second', () => { | ||
expect(document.body.innerHTML).toEqual('') | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,153 +1,16 @@ | ||
import React from 'react' | ||
import ReactDOM from 'react-dom' | ||
import { | ||
getQueriesForElement, | ||
prettyDOM, | ||
fireEvent as dtlFireEvent, | ||
configure as configureDTL, | ||
} from '@testing-library/dom' | ||
import act, {asyncAct} from './act-compat' | ||
|
||
configureDTL({ | ||
asyncWrapper: async cb => { | ||
let result | ||
await asyncAct(async () => { | ||
result = await cb() | ||
}) | ||
return result | ||
}, | ||
}) | ||
|
||
const mountedContainers = new Set() | ||
|
||
function render( | ||
ui, | ||
{ | ||
container, | ||
baseElement = container, | ||
queries, | ||
hydrate = false, | ||
wrapper: WrapperComponent, | ||
} = {}, | ||
) { | ||
if (!baseElement) { | ||
// default to document.body instead of documentElement to avoid output of potentially-large | ||
// head elements (such as JSS style blocks) in debug output | ||
baseElement = document.body | ||
} | ||
if (!container) { | ||
container = baseElement.appendChild(document.createElement('div')) | ||
} | ||
|
||
// we'll add it to the mounted containers regardless of whether it's actually | ||
// added to document.body so the cleanup method works regardless of whether | ||
// they're passing us a custom container or not. | ||
mountedContainers.add(container) | ||
|
||
const wrapUiIfNeeded = innerElement => | ||
WrapperComponent | ||
? React.createElement(WrapperComponent, null, innerElement) | ||
: innerElement | ||
|
||
act(() => { | ||
if (hydrate) { | ||
ReactDOM.hydrate(wrapUiIfNeeded(ui), container) | ||
} else { | ||
ReactDOM.render(wrapUiIfNeeded(ui), container) | ||
} | ||
}) | ||
|
||
return { | ||
container, | ||
baseElement, | ||
// eslint-disable-next-line no-console | ||
debug: (el = baseElement) => console.log(prettyDOM(el)), | ||
unmount: () => ReactDOM.unmountComponentAtNode(container), | ||
rerender: rerenderUi => { | ||
render(wrapUiIfNeeded(rerenderUi), {container, baseElement}) | ||
// Intentionally do not return anything to avoid unnecessarily complicating the API. | ||
// folks can use all the same utilities we return in the first place that are bound to the container | ||
}, | ||
asFragment: () => { | ||
/* istanbul ignore if (jsdom limitation) */ | ||
if (typeof document.createRange === 'function') { | ||
return document | ||
.createRange() | ||
.createContextualFragment(container.innerHTML) | ||
} | ||
|
||
const template = document.createElement('template') | ||
template.innerHTML = container.innerHTML | ||
return template.content | ||
}, | ||
...getQueriesForElement(baseElement, queries), | ||
} | ||
} | ||
|
||
function cleanup() { | ||
mountedContainers.forEach(cleanupAtContainer) | ||
} | ||
|
||
// maybe one day we'll expose this (perhaps even as a utility returned by render). | ||
// but let's wait until someone asks for it. | ||
function cleanupAtContainer(container) { | ||
ReactDOM.unmountComponentAtNode(container) | ||
if (container.parentNode === document.body) { | ||
document.body.removeChild(container) | ||
} | ||
mountedContainers.delete(container) | ||
} | ||
|
||
// react-testing-library's version of fireEvent will call | ||
// dom-testing-library's version of fireEvent wrapped inside | ||
// an "act" call so that after all event callbacks have been | ||
// been called, the resulting useEffect callbacks will also | ||
// be called. | ||
function fireEvent(...args) { | ||
let returnValue | ||
act(() => { | ||
returnValue = dtlFireEvent(...args) | ||
import {asyncAct} from './act-compat' | ||
import {cleanup} from './pure' | ||
|
||
// if we're running in a test runner that supports afterEach | ||
// then we'll automatically run cleanup afterEach test | ||
// this ensures that tests run in isolation from each other | ||
// if you don't like this then either import the `pure` module | ||
// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'. | ||
if (typeof afterEach === 'function' && !process.env.RTL_SKIP_AUTO_CLEANUP) { | ||
afterEach(async () => { | ||
await asyncAct(async () => {}) | ||
cleanup() | ||
}) | ||
return returnValue | ||
} | ||
|
||
Object.keys(dtlFireEvent).forEach(key => { | ||
fireEvent[key] = (...args) => { | ||
let returnValue | ||
act(() => { | ||
returnValue = dtlFireEvent[key](...args) | ||
}) | ||
return returnValue | ||
} | ||
}) | ||
|
||
// React event system tracks native mouseOver/mouseOut events for | ||
// running onMouseEnter/onMouseLeave handlers | ||
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31 | ||
fireEvent.mouseEnter = fireEvent.mouseOver | ||
fireEvent.mouseLeave = fireEvent.mouseOut | ||
|
||
fireEvent.select = (node, init) => { | ||
// React tracks this event only on focused inputs | ||
node.focus() | ||
|
||
// React creates this event when one of the following native events happens | ||
// - contextMenu | ||
// - mouseUp | ||
// - dragEnd | ||
// - keyUp | ||
// - keyDown | ||
// so we can use any here | ||
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224 | ||
fireEvent.keyUp(node, init) | ||
} | ||
|
||
// just re-export everything from dom-testing-library | ||
export * from '@testing-library/dom' | ||
export {render, cleanup, fireEvent, act} | ||
|
||
// NOTE: we're not going to export asyncAct because that's our own compatibility | ||
// thing for people using [email protected]. Anyone else doesn't need it and | ||
// people should just upgrade anyway. | ||
|
||
/* eslint func-name-matching:0 */ | ||
export * from './pure' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import React from 'react' | ||
import ReactDOM from 'react-dom' | ||
import { | ||
getQueriesForElement, | ||
prettyDOM, | ||
fireEvent as dtlFireEvent, | ||
configure as configureDTL, | ||
} from '@testing-library/dom' | ||
import act, {asyncAct} from './act-compat' | ||
|
||
configureDTL({ | ||
asyncWrapper: async cb => { | ||
let result | ||
await asyncAct(async () => { | ||
result = await cb() | ||
}) | ||
return result | ||
}, | ||
}) | ||
|
||
const mountedContainers = new Set() | ||
|
||
function render( | ||
ui, | ||
{ | ||
container, | ||
baseElement = container, | ||
queries, | ||
hydrate = false, | ||
wrapper: WrapperComponent, | ||
} = {}, | ||
) { | ||
if (!baseElement) { | ||
// default to document.body instead of documentElement to avoid output of potentially-large | ||
// head elements (such as JSS style blocks) in debug output | ||
baseElement = document.body | ||
} | ||
if (!container) { | ||
container = baseElement.appendChild(document.createElement('div')) | ||
} | ||
|
||
// we'll add it to the mounted containers regardless of whether it's actually | ||
// added to document.body so the cleanup method works regardless of whether | ||
// they're passing us a custom container or not. | ||
mountedContainers.add(container) | ||
|
||
const wrapUiIfNeeded = innerElement => | ||
WrapperComponent | ||
? React.createElement(WrapperComponent, null, innerElement) | ||
: innerElement | ||
|
||
act(() => { | ||
if (hydrate) { | ||
ReactDOM.hydrate(wrapUiIfNeeded(ui), container) | ||
} else { | ||
ReactDOM.render(wrapUiIfNeeded(ui), container) | ||
} | ||
}) | ||
|
||
return { | ||
container, | ||
baseElement, | ||
// eslint-disable-next-line no-console | ||
debug: (el = baseElement) => console.log(prettyDOM(el)), | ||
unmount: () => ReactDOM.unmountComponentAtNode(container), | ||
rerender: rerenderUi => { | ||
render(wrapUiIfNeeded(rerenderUi), {container, baseElement}) | ||
// Intentionally do not return anything to avoid unnecessarily complicating the API. | ||
// folks can use all the same utilities we return in the first place that are bound to the container | ||
}, | ||
asFragment: () => { | ||
/* istanbul ignore if (jsdom limitation) */ | ||
if (typeof document.createRange === 'function') { | ||
return document | ||
.createRange() | ||
.createContextualFragment(container.innerHTML) | ||
} | ||
|
||
const template = document.createElement('template') | ||
template.innerHTML = container.innerHTML | ||
return template.content | ||
}, | ||
...getQueriesForElement(baseElement, queries), | ||
} | ||
} | ||
|
||
function cleanup() { | ||
mountedContainers.forEach(cleanupAtContainer) | ||
} | ||
|
||
// maybe one day we'll expose this (perhaps even as a utility returned by render). | ||
// but let's wait until someone asks for it. | ||
function cleanupAtContainer(container) { | ||
ReactDOM.unmountComponentAtNode(container) | ||
if (container.parentNode === document.body) { | ||
document.body.removeChild(container) | ||
} | ||
mountedContainers.delete(container) | ||
} | ||
|
||
// react-testing-library's version of fireEvent will call | ||
// dom-testing-library's version of fireEvent wrapped inside | ||
// an "act" call so that after all event callbacks have been | ||
// been called, the resulting useEffect callbacks will also | ||
// be called. | ||
function fireEvent(...args) { | ||
let returnValue | ||
act(() => { | ||
returnValue = dtlFireEvent(...args) | ||
}) | ||
return returnValue | ||
} | ||
|
||
Object.keys(dtlFireEvent).forEach(key => { | ||
fireEvent[key] = (...args) => { | ||
let returnValue | ||
act(() => { | ||
returnValue = dtlFireEvent[key](...args) | ||
}) | ||
return returnValue | ||
} | ||
}) | ||
|
||
// React event system tracks native mouseOver/mouseOut events for | ||
// running onMouseEnter/onMouseLeave handlers | ||
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31 | ||
fireEvent.mouseEnter = fireEvent.mouseOver | ||
fireEvent.mouseLeave = fireEvent.mouseOut | ||
|
||
fireEvent.select = (node, init) => { | ||
// React tracks this event only on focused inputs | ||
node.focus() | ||
|
||
// React creates this event when one of the following native events happens | ||
// - contextMenu | ||
// - mouseUp | ||
// - dragEnd | ||
// - keyUp | ||
// - keyDown | ||
// so we can use any here | ||
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224 | ||
fireEvent.keyUp(node, init) | ||
} | ||
|
||
// just re-export everything from dom-testing-library | ||
export * from '@testing-library/dom' | ||
export {render, cleanup, fireEvent, act} | ||
|
||
// NOTE: we're not going to export asyncAct because that's our own compatibility | ||
// thing for people using [email protected]. Anyone else doesn't need it and | ||
// people should just upgrade anyway. | ||
|
||
/* eslint func-name-matching:0 */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1 @@ | ||
import '@testing-library/jest-dom/extend-expect' | ||
|
||
afterEach(() => { | ||
// have to do a dynamic import so we don't mess up jest mocking for old-act.js | ||
require('../src').cleanup() | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider - if ther are any hanging promises after a test is finished, that probably means the user hasn't asserted on the actual stable state of the UI. So this would hide a probable bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is a similar concern with the async act() in cleanup-after-each, ouch. at least there it's explicit tho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand how this change impacts that at all. This change is doing exactly the same thing that
cleanup-after-each
is doing. It's just wiring it up automatically.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this shouldn't be an act(), but it should flush promises anyway. So if there are any stray async updates, the warning will fire, and the dev will account for it in their test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suggesting it should be removed from that too.