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