mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Fix invariant parity for SSR (#10333)
This commit is contained in:
@@ -45,7 +45,7 @@ function renderToStream(element) {
|
||||
if (disableNewFiberFeatures) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToStream(): You must pass a valid ReactElement.',
|
||||
'renderToStream(): Invalid component element.',
|
||||
);
|
||||
}
|
||||
return new ReactMarkupReadableStream(element, false);
|
||||
@@ -61,7 +61,7 @@ function renderToStaticStream(element) {
|
||||
if (disableNewFiberFeatures) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToStaticStream(): You must pass a valid ReactElement.',
|
||||
'renderToStaticStream(): Invalid component element.',
|
||||
);
|
||||
}
|
||||
return new ReactMarkupReadableStream(element, true);
|
||||
|
||||
@@ -26,7 +26,7 @@ function renderToString(element) {
|
||||
if (disableNewFiberFeatures) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToString(): You must pass a valid ReactElement.',
|
||||
'renderToString(): Invalid component element.',
|
||||
);
|
||||
}
|
||||
var renderer = new ReactPartialRenderer(element, false);
|
||||
@@ -44,7 +44,7 @@ function renderToStaticMarkup(element) {
|
||||
if (disableNewFiberFeatures) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToStaticMarkup(): You must pass a valid ReactElement.',
|
||||
'renderToStaticMarkup(): Invalid component element.',
|
||||
);
|
||||
}
|
||||
var renderer = new ReactPartialRenderer(element, true);
|
||||
|
||||
@@ -23,6 +23,9 @@ let ReactTestUtils;
|
||||
|
||||
const stream = require('stream');
|
||||
|
||||
const TEXT_NODE_TYPE = 3;
|
||||
const COMMENT_NODE_TYPE = 8;
|
||||
|
||||
// Helper functions for rendering tests
|
||||
// ====================================
|
||||
|
||||
@@ -138,15 +141,36 @@ const clientCleanRender = (element, errorCount = 0) => {
|
||||
const clientRenderOnServerString = async (element, errorCount = 0) => {
|
||||
const markup = await renderIntoString(element, errorCount);
|
||||
resetModules();
|
||||
|
||||
var domElement = document.createElement('div');
|
||||
domElement.innerHTML = markup;
|
||||
const serverElement = domElement.firstChild;
|
||||
const clientElement = await renderIntoDom(element, domElement, errorCount);
|
||||
// assert that the DOM element hasn't been replaced.
|
||||
// Note that we cannot use expect(serverElement).toBe(clientElement) because
|
||||
// of jest bug #1772
|
||||
expect(serverElement === clientElement).toBe(true);
|
||||
return clientElement;
|
||||
let serverNode = domElement.firstChild;
|
||||
|
||||
const firstClientNode = await renderIntoDom(element, domElement, errorCount);
|
||||
let clientNode = firstClientNode;
|
||||
|
||||
// Make sure all top level nodes match up
|
||||
while (serverNode || clientNode) {
|
||||
expect(serverNode != null).toBe(true);
|
||||
expect(clientNode != null).toBe(true);
|
||||
expect(clientNode.nodeType).toBe(serverNode.nodeType);
|
||||
if (clientNode.nodeType === TEXT_NODE_TYPE) {
|
||||
// Text nodes are stateless so we can just compare values.
|
||||
// This works around a current issue where hydration replaces top-level
|
||||
// text node, but otherwise works.
|
||||
// TODO: we can remove this branch if we add explicit hydration opt-in.
|
||||
// https://github.com/facebook/react/issues/10189
|
||||
expect(serverNode.nodeValue).toBe(clientNode.nodeValue);
|
||||
} else {
|
||||
// Assert that the DOM element hasn't been replaced.
|
||||
// Note that we cannot use expect(serverNode).toBe(clientNode) because
|
||||
// of jest bug #1772.
|
||||
expect(serverNode === clientNode).toBe(true);
|
||||
}
|
||||
serverNode = serverNode.nextSibling;
|
||||
clientNode = clientNode.nextSibling;
|
||||
}
|
||||
return firstClientNode;
|
||||
};
|
||||
|
||||
function BadMarkupExpected() {}
|
||||
@@ -227,21 +251,28 @@ function itClientRenders(desc, testFn) {
|
||||
});
|
||||
}
|
||||
|
||||
function itThrows(desc, testFn) {
|
||||
function itThrows(desc, testFn, partialMessage) {
|
||||
it(`throws ${desc}`, () => {
|
||||
return testFn().then(
|
||||
() => expect(false).toBe('The promise resolved and should not have.'),
|
||||
() => {},
|
||||
err => {
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(err.message).toContain(partialMessage);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function itThrowsWhenRendering(desc, testFn) {
|
||||
itThrows(`when rendering ${desc} with server string render`, () =>
|
||||
testFn(serverRender),
|
||||
function itThrowsWhenRendering(desc, testFn, partialMessage) {
|
||||
itThrows(
|
||||
`when rendering ${desc} with server string render`,
|
||||
() => testFn(serverRender),
|
||||
partialMessage,
|
||||
);
|
||||
itThrows(`when rendering ${desc} with clean client render`, () =>
|
||||
testFn(clientCleanRender),
|
||||
itThrows(
|
||||
`when rendering ${desc} with clean client render`,
|
||||
() => testFn(clientCleanRender),
|
||||
partialMessage,
|
||||
);
|
||||
|
||||
// we subtract one from the warning count here because the throw means that it won't
|
||||
@@ -252,6 +283,7 @@ function itThrowsWhenRendering(desc, testFn) {
|
||||
testFn((element, warningCount = 0) =>
|
||||
clientRenderOnBadMarkup(element, warningCount - 1),
|
||||
),
|
||||
partialMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,6 +330,7 @@ function resetModules() {
|
||||
// TODO: can we express this test with only public API?
|
||||
ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
|
||||
require('ReactFeatureFlags').disableNewFiberFeatures = false;
|
||||
PropTypes = require('prop-types');
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
@@ -311,7 +344,7 @@ function resetModules() {
|
||||
if (typeof onAfterResetModules === 'function') {
|
||||
onAfterResetModules();
|
||||
}
|
||||
|
||||
require('ReactFeatureFlags').disableNewFiberFeatures = false;
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
|
||||
// Finally, reset modules one more time before importing the stream renderer.
|
||||
@@ -319,7 +352,7 @@ function resetModules() {
|
||||
if (typeof onAfterResetModules === 'function') {
|
||||
onAfterResetModules();
|
||||
}
|
||||
|
||||
require('ReactFeatureFlags').disableNewFiberFeatures = false;
|
||||
ReactDOMNodeStream = require('react-dom/node-stream');
|
||||
}
|
||||
|
||||
@@ -331,19 +364,6 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
describe('basic rendering', function() {
|
||||
beforeEach(() => {
|
||||
onAfterResetModules = () => {
|
||||
const ReactFeatureFlags = require('ReactFeatureFlags');
|
||||
ReactFeatureFlags.disableNewFiberFeatures = false;
|
||||
};
|
||||
|
||||
resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
onAfterResetModules = null;
|
||||
});
|
||||
|
||||
itRenders('a blank div', async render => {
|
||||
const e = await render(<div />);
|
||||
expect(e.tagName).toBe('DIV');
|
||||
@@ -361,7 +381,25 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
if (ReactDOMFeatureFlags.useFiber) {
|
||||
itRenders('a array type children as a child', async render => {
|
||||
itRenders('a string', async render => {
|
||||
let e = await render('Hello');
|
||||
expect(e.nodeType).toBe(3);
|
||||
expect(e.nodeValue).toMatch('Hello');
|
||||
});
|
||||
|
||||
itRenders('a number', async render => {
|
||||
let e = await render(42);
|
||||
expect(e.nodeType).toBe(3);
|
||||
expect(e.nodeValue).toMatch('42');
|
||||
});
|
||||
|
||||
itRenders('an array with one child', async render => {
|
||||
let e = await render([<div key={1}>text1</div>]);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
itRenders('an array with several children', async render => {
|
||||
let Header = props => {
|
||||
return <p>header</p>;
|
||||
};
|
||||
@@ -382,13 +420,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(parent.childNodes[4].tagName).toBe('H3');
|
||||
});
|
||||
|
||||
itRenders('a single array element children as a child', async render => {
|
||||
let e = await render([<div key={1}>text1</div>]);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
itRenders('a nested array children as a child', async render => {
|
||||
itRenders('a nested array', async render => {
|
||||
let e = await render([
|
||||
[<div key={1}>text1</div>],
|
||||
<span key={1}>text2</span>,
|
||||
@@ -400,7 +432,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(parent.childNodes[2].tagName).toBe('P');
|
||||
});
|
||||
|
||||
itRenders('an iterable as a child', async render => {
|
||||
itRenders('an iterable', async render => {
|
||||
const threeDivIterable = {
|
||||
'@@iterator': function() {
|
||||
var i = 0;
|
||||
@@ -801,10 +833,6 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
describe('elements and children', function() {
|
||||
// helper functions.
|
||||
const TEXT_NODE_TYPE = 3;
|
||||
const COMMENT_NODE_TYPE = 8;
|
||||
|
||||
function expectNode(node, type, value) {
|
||||
expect(node).not.toBe(null);
|
||||
expect(node.nodeType).toBe(type);
|
||||
@@ -1401,26 +1429,81 @@ describe('ReactDOMServerIntegration', () => {
|
||||
});
|
||||
|
||||
describe('components that throw errors', function() {
|
||||
itThrowsWhenRendering('a string component', async render => {
|
||||
const StringComponent = () => 'foo';
|
||||
await render(<StringComponent />, 1);
|
||||
});
|
||||
itThrowsWhenRendering(
|
||||
'a function returning undefined',
|
||||
async render => {
|
||||
const UndefinedComponent = () => undefined;
|
||||
await render(<UndefinedComponent />, 1);
|
||||
},
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'UndefinedComponent(...): Nothing was returned from render. ' +
|
||||
'This usually means a return statement is missing. Or, to ' +
|
||||
'render nothing, return null.'
|
||||
: 'A valid React element (or null) must be returned. ' +
|
||||
'You may have returned undefined, an array or some other invalid object.',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering('an undefined component', async render => {
|
||||
const UndefinedComponent = () => undefined;
|
||||
await render(<UndefinedComponent />, 1);
|
||||
});
|
||||
itThrowsWhenRendering(
|
||||
'a class returning undefined',
|
||||
async render => {
|
||||
class UndefinedComponent extends React.Component {
|
||||
render() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
await render(<UndefinedComponent />, 1);
|
||||
},
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'UndefinedComponent(...): Nothing was returned from render. ' +
|
||||
'This usually means a return statement is missing. Or, to ' +
|
||||
'render nothing, return null.'
|
||||
: 'A valid React element (or null) must be returned. ' +
|
||||
'You may have returned undefined, an array or some other invalid object.',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering('a number component', async render => {
|
||||
const NumberComponent = () => 54;
|
||||
await render(<NumberComponent />, 1);
|
||||
});
|
||||
itThrowsWhenRendering(
|
||||
'a function returning an object',
|
||||
async render => {
|
||||
const ObjectComponent = () => ({x: 123});
|
||||
await render(<ObjectComponent />, 1);
|
||||
},
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'Objects are not valid as a React child (found: object with keys ' +
|
||||
'{x}). If you meant to render a collection of children, use ' +
|
||||
'an array instead.'
|
||||
: 'A valid React element (or null) must be returned. ' +
|
||||
'You may have returned undefined, an array or some other invalid object.',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering('null', render => render(null));
|
||||
itThrowsWhenRendering('false', render => render(false));
|
||||
itThrowsWhenRendering('undefined', render => render(undefined));
|
||||
itThrowsWhenRendering('number', render => render(30));
|
||||
itThrowsWhenRendering('string', render => render('foo'));
|
||||
itThrowsWhenRendering(
|
||||
'a class returning an object',
|
||||
async render => {
|
||||
class ObjectComponent extends React.Component {
|
||||
render() {
|
||||
return {x: 123};
|
||||
}
|
||||
}
|
||||
await render(<ObjectComponent />, 1);
|
||||
},
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'Objects are not valid as a React child (found: object with keys ' +
|
||||
'{x}). If you meant to render a collection of children, use ' +
|
||||
'an array instead.'
|
||||
: 'A valid React element (or null) must be returned. ' +
|
||||
'You may have returned undefined, an array or some other invalid object.',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'top-level object',
|
||||
async render => {
|
||||
await render({x: 123});
|
||||
},
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'Objects are not valid as a React child (found: object with keys ' +
|
||||
'{x}). If you meant to render a collection of children, use ' +
|
||||
'an array instead.'
|
||||
: 'Invalid component element.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2165,7 +2248,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
itThrowsWhenRendering(
|
||||
'if getChildContext exists without childContextTypes',
|
||||
render => {
|
||||
class Component extends React.Component {
|
||||
class MyComponent extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
@@ -2173,14 +2256,16 @@ describe('ReactDOMServerIntegration', () => {
|
||||
return {foo: 'bar'};
|
||||
}
|
||||
}
|
||||
return render(<Component />);
|
||||
return render(<MyComponent />);
|
||||
},
|
||||
'MyComponent.getChildContext(): childContextTypes must be defined ' +
|
||||
'in order to use getChildContext().',
|
||||
);
|
||||
|
||||
itThrowsWhenRendering(
|
||||
'if getChildContext returns a value not in childContextTypes',
|
||||
render => {
|
||||
class Component extends React.Component {
|
||||
class MyComponent extends React.Component {
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
@@ -2188,9 +2273,10 @@ describe('ReactDOMServerIntegration', () => {
|
||||
return {value1: 'foo', value2: 'bar'};
|
||||
}
|
||||
}
|
||||
Component.childContextTypes = {value1: PropTypes.string};
|
||||
return render(<Component />);
|
||||
MyComponent.childContextTypes = {value1: PropTypes.string};
|
||||
return render(<MyComponent />);
|
||||
},
|
||||
'MyComponent.getChildContext(): key "value2" is not defined in childContextTypes.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ describe('ReactDOMServer', () => {
|
||||
).toThrowError(
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'Objects are not valid as a React child (found: object with keys {x})'
|
||||
: 'renderToString(): You must pass a valid ReactElement.',
|
||||
: 'renderToString(): Invalid component element.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -443,7 +443,7 @@ describe('ReactDOMServer', () => {
|
||||
).toThrowError(
|
||||
ReactDOMFeatureFlags.useFiber
|
||||
? 'Objects are not valid as a React child (found: object with keys {x})'
|
||||
: 'renderToStaticMarkup(): You must pass a valid ReactElement.',
|
||||
: 'renderToStaticMarkup(): Invalid component element.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ function renderToStringImpl(element, makeStaticMarkup) {
|
||||
function renderToString(element) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToString(): You must pass a valid ReactElement.',
|
||||
'renderToString(): Invalid component element.',
|
||||
);
|
||||
return renderToStringImpl(element, false);
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function renderToString(element) {
|
||||
function renderToStaticMarkup(element) {
|
||||
invariant(
|
||||
React.isValidElement(element),
|
||||
'renderToStaticMarkup(): You must pass a valid ReactElement.',
|
||||
'renderToStaticMarkup(): Invalid component element.',
|
||||
);
|
||||
return renderToStringImpl(element, true);
|
||||
}
|
||||
|
||||
@@ -309,6 +309,18 @@ function createOpenTagMarkup(
|
||||
return ret;
|
||||
}
|
||||
|
||||
function validateRenderResult(child, type) {
|
||||
if (child === undefined) {
|
||||
invariant(
|
||||
false,
|
||||
'%s(...): Nothing was returned from render. This usually means a ' +
|
||||
'return statement is missing. Or, to render nothing, ' +
|
||||
'return null.',
|
||||
getComponentName(type) || 'Component',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolve(child, context) {
|
||||
while (React.isValidElement(child)) {
|
||||
if (__DEV__) {
|
||||
@@ -351,6 +363,7 @@ function resolve(child, context) {
|
||||
inst = Component(child.props, publicContext, updater);
|
||||
if (inst == null || inst.render == null) {
|
||||
child = inst;
|
||||
validateRenderResult(child, Component);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -405,6 +418,7 @@ function resolve(child, context) {
|
||||
child = null;
|
||||
}
|
||||
}
|
||||
validateRenderResult(child, Component);
|
||||
|
||||
var childContext;
|
||||
if (typeof inst.getChildContext === 'function') {
|
||||
|
||||
Reference in New Issue
Block a user