arkit
Version:
Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams
494 lines (421 loc) • 16.5 kB
JavaScript
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
;
let React;
let ReactDOM;
let ReactDOMServer;
// These tests rely both on ReactDOMServer and ReactDOM.
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
describe('ReactDOMServerHydration', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
});
it('should have the correct mounting behavior (old hydrate API)', () => {
let mountCount = 0;
let numClicks = 0;
class TestComponent extends React.Component {
componentDidMount() {
mountCount++;
}
click = () => {
numClicks++;
};
render() {
return (
<span ref="span" onClick={this.click}>
Name: {this.props.name}
</span>
);
}
}
const element = document.createElement('div');
document.body.appendChild(element);
try {
ReactDOM.render(<TestComponent />, element);
let lastMarkup = element.innerHTML;
// Exercise the update path. Markup should not change,
// but some lifecycle methods should be run again.
ReactDOM.render(<TestComponent name="x" />, element);
expect(mountCount).toEqual(1);
// Unmount and remount. We should get another mount event and
// we should get different markup, as the IDs are unique each time.
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
ReactDOM.render(<TestComponent name="x" />, element);
expect(mountCount).toEqual(2);
expect(element.innerHTML).not.toEqual(lastMarkup);
// Now kill the node and render it on top of server-rendered markup, as if
// we used server rendering. We should mount again, but the markup should
// be unchanged. We will append a sentinel at the end of innerHTML to be
// sure that innerHTML was not changed.
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
element.innerHTML = lastMarkup;
let instance;
expect(() => {
instance = ReactDOM.render(<TestComponent name="x" />, element);
}).toLowPriorityWarnDev(
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
'will stop working in React v17. Replace the ReactDOM.render() call ' +
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
{withoutStack: true},
);
expect(mountCount).toEqual(3);
expect(element.innerHTML).toBe(lastMarkup);
// Ensure the events system works after mount into server markup
expect(numClicks).toEqual(0);
instance.refs.span.click();
expect(numClicks).toEqual(1);
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
// Now simulate a situation where the app is not idempotent. React should
// warn but do the right thing.
element.innerHTML = lastMarkup;
expect(() => {
instance = ReactDOM.render(<TestComponent name="y" />, element);
}).toWarnDev('Text content did not match. Server: "x" Client: "y"', {
withoutStack: true,
});
expect(mountCount).toEqual(4);
expect(element.innerHTML.length > 0).toBe(true);
expect(element.innerHTML).not.toEqual(lastMarkup);
// Ensure the events system works after markup mismatch.
expect(numClicks).toEqual(1);
instance.refs.span.click();
expect(numClicks).toEqual(2);
} finally {
document.body.removeChild(element);
}
});
it('should have the correct mounting behavior (new hydrate API)', () => {
let mountCount = 0;
let numClicks = 0;
class TestComponent extends React.Component {
componentDidMount() {
mountCount++;
}
click = () => {
numClicks++;
};
render() {
return (
<span ref="span" onClick={this.click}>
Name: {this.props.name}
</span>
);
}
}
const element = document.createElement('div');
document.body.appendChild(element);
try {
ReactDOM.render(<TestComponent />, element);
let lastMarkup = element.innerHTML;
// Exercise the update path. Markup should not change,
// but some lifecycle methods should be run again.
ReactDOM.render(<TestComponent name="x" />, element);
expect(mountCount).toEqual(1);
// Unmount and remount. We should get another mount event and
// we should get different markup, as the IDs are unique each time.
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
ReactDOM.render(<TestComponent name="x" />, element);
expect(mountCount).toEqual(2);
expect(element.innerHTML).not.toEqual(lastMarkup);
// Now kill the node and render it on top of server-rendered markup, as if
// we used server rendering. We should mount again, but the markup should
// be unchanged. We will append a sentinel at the end of innerHTML to be
// sure that innerHTML was not changed.
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
element.innerHTML = lastMarkup;
let instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
expect(mountCount).toEqual(3);
expect(element.innerHTML).toBe(lastMarkup);
// Ensure the events system works after mount into server markup
expect(numClicks).toEqual(0);
instance.refs.span.click();
expect(numClicks).toEqual(1);
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
// Now simulate a situation where the app is not idempotent. React should
// warn but do the right thing.
element.innerHTML = lastMarkup;
expect(() => {
instance = ReactDOM.hydrate(<TestComponent name="y" />, element);
}).toWarnDev('Text content did not match. Server: "x" Client: "y"', {
withoutStack: true,
});
expect(mountCount).toEqual(4);
expect(element.innerHTML.length > 0).toBe(true);
expect(element.innerHTML).not.toEqual(lastMarkup);
// Ensure the events system works after markup mismatch.
expect(numClicks).toEqual(1);
instance.refs.span.click();
expect(numClicks).toEqual(2);
} finally {
document.body.removeChild(element);
}
});
// We have a polyfill for autoFocus on the client, but we intentionally don't
// want it to call focus() when hydrating because this can mess up existing
// focus before the JS has loaded.
it('should emit autofocus on the server but not focus() when hydrating', () => {
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(
<input autoFocus={true} />,
);
expect(element.firstChild.autofocus).toBe(true);
// It should not be called on mount.
element.firstChild.focus = jest.fn();
ReactDOM.hydrate(<input autoFocus={true} />, element);
expect(element.firstChild.focus).not.toHaveBeenCalled();
// Or during an update.
ReactDOM.render(<input autoFocus={true} />, element);
expect(element.firstChild.focus).not.toHaveBeenCalled();
});
it('should not focus on either server or client with autofocus={false}', () => {
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(
<input autoFocus={false} />,
);
expect(element.firstChild.autofocus).toBe(false);
element.firstChild.focus = jest.fn();
ReactDOM.hydrate(<input autoFocus={false} />, element);
expect(element.firstChild.focus).not.toHaveBeenCalled();
ReactDOM.render(<input autoFocus={false} />, element);
expect(element.firstChild.focus).not.toHaveBeenCalled();
});
// Regression test for https://github.com/facebook/react/issues/11726
it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', () => {
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(
<button autoFocus={false}>server</button>,
);
expect(element.firstChild.autofocus).toBe(false);
element.firstChild.focus = jest.fn();
expect(() =>
ReactDOM.hydrate(<button autoFocus={false}>client</button>, element),
).toWarnDev(
'Warning: Text content did not match. Server: "server" Client: "client"',
{withoutStack: true},
);
expect(element.firstChild.focus).not.toHaveBeenCalled();
});
it('should warn when the style property differs', () => {
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(
<div style={{textDecoration: 'none', color: 'black', height: '10px'}} />,
);
expect(element.firstChild.style.textDecoration).toBe('none');
expect(element.firstChild.style.color).toBe('black');
expect(() =>
ReactDOM.hydrate(
<div
style={{textDecoration: 'none', color: 'white', height: '10px'}}
/>,
element,
),
).toWarnDev(
'Warning: Prop `style` did not match. Server: ' +
'"text-decoration:none;color:black;height:10px" Client: ' +
'"text-decoration:none;color:white;height:10px"',
{withoutStack: true},
);
});
it('should not warn when the style property differs on whitespace or order in IE', () => {
document.documentMode = 11;
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
try {
const element = document.createElement('div');
// Simulate IE normalizing the style attribute. IE makes it equal to
// what's available under `node.style.cssText`.
element.innerHTML =
'<div style="height: 10px; color: black; text-decoration: none;" data-reactroot=""></div>';
// We don't expect to see false positive warnings.
// https://github.com/facebook/react/issues/11807
ReactDOM.hydrate(
<div
style={{textDecoration: 'none', color: 'black', height: '10px'}}
/>,
element,
);
} finally {
delete document.documentMode;
}
});
it('should warn when the style property differs on whitespace in non-IE browsers', () => {
const element = document.createElement('div');
element.innerHTML =
'<div style="text-decoration: none; color: black; height: 10px;" data-reactroot=""></div>';
expect(() =>
ReactDOM.hydrate(
<div
style={{textDecoration: 'none', color: 'black', height: '10px'}}
/>,
element,
),
).toWarnDev(
'Warning: Prop `style` did not match. Server: ' +
'"text-decoration: none; color: black; height: 10px;" Client: ' +
'"text-decoration:none;color:black;height:10px"',
{withoutStack: true},
);
});
it('should throw rendering portals on the server', () => {
const div = document.createElement('div');
expect(() => {
ReactDOMServer.renderToString(
<div>{ReactDOM.createPortal(<div />, div)}</div>,
);
}).toThrow(
'Portals are not currently supported by the server renderer. ' +
'Render them conditionally so that they only appear on the client render.',
);
});
it('should be able to render and hydrate Mode components', () => {
class ComponentWithWarning extends React.Component {
componentWillMount() {
// Expected warning
}
render() {
return 'Hi';
}
}
const markup = (
<React.StrictMode>
<ComponentWithWarning />
</React.StrictMode>
);
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(markup);
expect(element.textContent).toBe('Hi');
expect(() => ReactDOM.hydrate(markup, element)).toWarnDev(
'Please update the following components to use componentDidMount instead: ComponentWithWarning',
);
expect(element.textContent).toBe('Hi');
});
it('should be able to render and hydrate forwardRef components', () => {
const FunctionComponent = ({label, forwardedRef}) => (
<div ref={forwardedRef}>{label}</div>
);
const WrappedFunctionComponent = React.forwardRef((props, ref) => (
<FunctionComponent {...props} forwardedRef={ref} />
));
const ref = React.createRef();
const markup = <WrappedFunctionComponent ref={ref} label="Hi" />;
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(markup);
expect(element.textContent).toBe('Hi');
expect(ref.current).toBe(null);
ReactDOM.hydrate(markup, element);
expect(element.textContent).toBe('Hi');
expect(ref.current.tagName).toBe('DIV');
});
it('should be able to render and hydrate Profiler components', () => {
const callback = jest.fn();
const markup = (
<React.unstable_Profiler id="profiler" onRender={callback}>
<div>Hi</div>
</React.unstable_Profiler>
);
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(markup);
expect(element.textContent).toBe('Hi');
expect(callback).not.toHaveBeenCalled();
ReactDOM.hydrate(markup, element);
expect(element.textContent).toBe('Hi');
if (__DEV__) {
expect(callback).toHaveBeenCalledTimes(1);
const [id, phase] = callback.mock.calls[0];
expect(id).toBe('profiler');
expect(phase).toBe('mount');
} else {
expect(callback).toHaveBeenCalledTimes(0);
}
});
// Regression test for https://github.com/facebook/react/issues/11423
it('should ignore noscript content on the client and not warn about mismatches', () => {
const callback = jest.fn();
const TestComponent = ({onRender}) => {
onRender();
return <div>Enable JavaScript to run this app.</div>;
};
const markup = (
<noscript>
<TestComponent onRender={callback} />
</noscript>
);
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(markup);
expect(callback).toHaveBeenCalledTimes(1);
expect(element.textContent).toBe(
'<div>Enable JavaScript to run this app.</div>',
);
// On the client we want to keep the existing markup, but not render the
// actual elements for performance reasons and to avoid for example
// downloading images. This should also not warn for hydration mismatches.
ReactDOM.hydrate(markup, element);
expect(callback).toHaveBeenCalledTimes(1);
expect(element.textContent).toBe(
'<div>Enable JavaScript to run this app.</div>',
);
});
it('should be able to use lazy components after hydrating', async () => {
const Lazy = React.lazy(
() =>
new Promise(resolve => {
setTimeout(
() =>
resolve({
default: function World() {
return 'world';
},
}),
1000,
);
}),
);
class HelloWorld extends React.Component {
state = {isClient: false};
componentDidMount() {
this.setState({
isClient: true,
});
}
render() {
return (
<div>
Hello{' '}
{this.state.isClient && (
<React.Suspense fallback="loading">
<Lazy />
</React.Suspense>
)}
</div>
);
}
}
const element = document.createElement('div');
element.innerHTML = ReactDOMServer.renderToString(<HelloWorld />);
expect(element.textContent).toBe('Hello ');
ReactDOM.hydrate(<HelloWorld />, element);
expect(element.textContent).toBe('Hello loading');
jest.runAllTimers();
await Promise.resolve();
expect(element.textContent).toBe('Hello world');
});
});