diff --git a/jest.config.js b/jest.config.js
index 0ed33704..f30f76e9 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -6,10 +6,10 @@ module.exports = Object.assign(jestConfig, {
// full coverage across the build matrix (React 17, 18) but not in a single job
'./src/pure': {
// minimum coverage of jobs using React 17 and 18
- branches: 80,
+ branches: 75,
functions: 78,
- lines: 79,
- statements: 79,
+ lines: 76,
+ statements: 76,
},
},
})
diff --git a/src/__tests__/render.js b/src/__tests__/render.js
index ac996444..d5f78b57 100644
--- a/src/__tests__/render.js
+++ b/src/__tests__/render.js
@@ -1,6 +1,13 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
-import {render, screen} from '../'
+import ReactDOMServer from 'react-dom/server'
+import {fireEvent, render, screen} from '../'
+
+afterEach(() => {
+ if (console.error.mockRestore !== undefined) {
+ console.error.mockRestore()
+ }
+})
test('renders div into document', () => {
const ref = React.createRef()
@@ -134,3 +141,30 @@ test('can be called multiple times on the same container', () => {
expect(container).toBeEmptyDOMElement()
})
+
+test('hydrate will make the UI interactive', () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {})
+ function App() {
+ const [clicked, handleClick] = React.useReducer(n => n + 1, 0)
+
+ return (
+
+ )
+ }
+ const ui =
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ container.innerHTML = ReactDOMServer.renderToString(ui)
+
+ expect(container).toHaveTextContent('clicked:0')
+
+ render(ui, {container, hydrate: true})
+
+ expect(console.error).not.toHaveBeenCalled()
+
+ fireEvent.click(container.querySelector('button'))
+
+ expect(container).toHaveTextContent('clicked:1')
+})
diff --git a/src/pure.js b/src/pure.js
index dc5fa3fa..309e2090 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -60,25 +60,36 @@ const mountedContainers = new Set()
*/
const mountedRootEntries = []
-function createConcurrentRoot(container, options) {
+function createConcurrentRoot(
+ container,
+ {hydrate, ui, wrapper: WrapperComponent},
+) {
if (typeof ReactDOM.createRoot !== 'function') {
throw new TypeError(
`Attempted to use concurrent React with \`react-dom@${ReactDOM.version}\`. Be sure to use the \`next\` or \`experimental\` release channel (https://reactjs.org/docs/release-channels.html).'`,
)
}
- const root = options.hydrate
- ? ReactDOM.hydrateRoot(container)
- : ReactDOM.createRoot(container)
+ let root
+ if (hydrate) {
+ act(() => {
+ root = ReactDOM.hydrateRoot(
+ container,
+ WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui,
+ )
+ })
+ } else {
+ root = ReactDOM.createRoot(container)
+ }
return {
- hydrate(element) {
+ hydrate() {
/* istanbul ignore if */
- if (!options.hydrate) {
+ if (!hydrate) {
throw new Error(
'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
)
}
- root.render(element)
+ // Nothing to do since hydration happens when creating the root object.
},
render(element) {
root.render(element)
@@ -183,7 +194,7 @@ function render(
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
if (!mountedContainers.has(container)) {
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
- root = createRootImpl(container, {hydrate})
+ root = createRootImpl(container, {hydrate, ui, wrapper})
mountedRootEntries.push({container, root})
// we'll add it to the mounted containers regardless of whether it's actually