Skip to content
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

Adding some server rendering unit tests. #9055

Merged
merged 7 commits into from
Mar 1, 2017
4 changes: 4 additions & 0 deletions scripts/fiber/tests-passing-except-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,7 @@ src/renderers/dom/shared/__tests__/ReactMountDestruction-test.js

src/renderers/dom/shared/__tests__/ReactServerRendering-test.js
* should have the correct mounting behavior
* should render a blank div with client render on top of bad server markup
* should render a div with inline styles with client render on top of bad server markup
* should render a self-closing tag with client render on top of bad server markup
* should render a self-closing tag as a child with client render on top of bad server markup
12 changes: 12 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,18 @@ src/renderers/dom/shared/__tests__/ReactServerRendering-test.js
* warns with a no-op when an async replaceState is triggered
* warns with a no-op when an async forceUpdate is triggered
* should warn when children are mutated during render
* should render a blank div with server string render
* should render a blank div with clean client render
* should render a blank div with client render on top of good server markup
* should render a div with inline styles with server string render
* should render a div with inline styles with clean client render
* should render a div with inline styles with client render on top of good server markup
* should render a self-closing tag with server string render
* should render a self-closing tag with clean client render
* should render a self-closing tag with client render on top of good server markup
* should render a self-closing tag as a child with server string render
* should render a self-closing tag as a child with clean client render
* should render a self-closing tag as a child with client render on top of good server markup

src/renderers/dom/shared/__tests__/escapeTextContentForBrowser-test.js
* should escape boolean to string
Expand Down
159 changes: 147 additions & 12 deletions src/renderers/dom/shared/__tests__/ReactServerRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,132 @@ var ReactTestUtils;
var ID_ATTRIBUTE_NAME;
var ROOT_ATTRIBUTE_NAME;

// performs fn asynchronously and expects count errors logged to console.error.
// will fail the test if the count of errors logged is not equal to count.
function expectErrors(fn, count) {
if (console.error.calls && console.error.calls.reset) {
console.error.calls.reset();
} else {
spyOn(console, 'error');
}

return fn().then((result) => {
if (console.error.calls.count() !== count) {
console.log(`We expected ${count} warning(s), but saw ${console.error.calls.count()} warning(s).`);
if (console.error.calls.count() > 0) {
console.log(`We saw these warnings:`);
for (var i = 0; i < console.error.calls.count(); i++) {
console.log(console.error.calls.argsFor(i)[0]);
}
}
}
expectDev(console.error.calls.count()).toBe(count);
return result;
});
}

// renders the reactElement into domElement, and expects a certain number of errors.
// returns a Promise that resolves when the render is complete.
function renderIntoDom(reactElement, domElement, errorCount = 0) {
return expectErrors(
() => new Promise((resolve) => ReactDOM.render(reactElement, domElement, () => resolve(domElement.firstChild))),
errorCount
);
}

// Renders text using SSR and then stuffs it into a DOM node; returns the DOM
// element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
function serverRender(reactElement, errorCount = 0) {
return expectErrors(
() => Promise.resolve(ReactDOMServer.renderToString(reactElement)),
errorCount)
.then((markup) => {
var domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
});
}

const clientCleanRender = (element, errorCount = 0) => {
const div = document.createElement('div');
return renderIntoDom(element, div, errorCount);
};

const clientRenderOnServerString = (element, errorCount = 0) => {
return serverRender(element, errorCount).then((markup) => {
resetModules();
var domElement = document.createElement('div');
domElement.innerHTML = markup;
return renderIntoDom(element, domElement, errorCount);
});
};

const clientRenderOnBadMarkup = (element, errorCount = 0) => {
var domElement = document.createElement('div');
domElement.innerHTML = '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
return renderIntoDom(element, domElement, errorCount + 1);
};

// runs a DOM rendering test as four different tests, with four different rendering
// scenarios:
// -- render to string on server
// -- render on client without any server markup "clean client render"
// -- render on client on top of good server-generated string markup
// -- render on client on top of bad server-generated markup
//
// testFn is a test that has one arg, which is a render function. the render
// function takes in a ReactElement and an optional expected error count and
// returns a promise of a DOM Element.
//
// You should only perform tests that examine the DOM of the results of
// render; you should not depend on the interactivity of the returned DOM element,
// as that will not work in the server string scenario.
function itRenders(desc, testFn) {
it(`${desc} with server string render`,
() => testFn(serverRender));
itClientRenders(desc, testFn);
}

// run testFn in three different rendering scenarios:
// -- render on client without any server markup "clean client render"
// -- render on client on top of good server-generated string markup
// -- render on client on top of bad server-generated markup
//
// testFn is a test that has one arg, which is a render function. the render
// function takes in a ReactElement and an optional expected error count and
// returns a promise of a DOM Element.
//
// Since all of the renders in this function are on the client, you can test interactivity,
// unlike with itRenders.
function itClientRenders(desc, testFn) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason these three are split from the other one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the tests in this PR use it, but some of the ones coming later do.

The idea is that itRenders is for tests that only need to inspect the DOM, whereas itClientRenders is for tests that need to test interactivity (things like events, refs, etc.). The latter tests are only possible when ReactDOM.render has been called, which doesn't happen in the server render-only tests.

I tried to explain that in the comment above the function, but obviously it wasn't clear enough. Any suggestions on how to make it clearer?

it(`${desc} with clean client render`,
() => testFn(clientCleanRender));
it(`${desc} with client render on top of good server markup`,
() => testFn(clientRenderOnServerString));
it(`${desc} with client render on top of bad server markup`,
() => testFn(clientRenderOnBadMarkup));
}

function resetModules() {
jest.resetModuleRegistry();
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactMarkupChecksum = require('ReactMarkupChecksum');
ReactTestUtils = require('ReactTestUtils');
ReactReconcileTransaction = require('ReactReconcileTransaction');

ExecutionEnvironment = require('ExecutionEnvironment');
ExecutionEnvironment.canUseDOM = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be conditional on whether we are going to do client or server rendering, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right; this was an incantation I copied from the old code without thinking. I've changed it in a91c836 to change canUseDom to true before the call to ReactDOM.render in renderToDom (and change it back after). Seems to work right AFAICT.

ReactDOMServer = require('ReactDOMServer');


}

describe('ReactDOMServer', () => {
beforeEach(() => {
jest.resetModules();
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactMarkupChecksum = require('ReactMarkupChecksum');
ReactTestUtils = require('ReactTestUtils');
ReactReconcileTransaction = require('ReactReconcileTransaction');

ExecutionEnvironment = require('ExecutionEnvironment');
ExecutionEnvironment.canUseDOM = false;
ReactDOMServer = require('ReactDOMServer');

resetModules();
var DOMProperty = require('DOMProperty');
ID_ATTRIBUTE_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
ROOT_ATTRIBUTE_NAME = DOMProperty.ROOT_ATTRIBUTE_NAME;
Expand Down Expand Up @@ -549,4 +661,27 @@ describe('ReactDOMServer', () => {
);
}).toThrowError(/Cannot assign to read only property.*/);
});

describe('basic rendering', function() {
itRenders('should render a blank div', render =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we rename this function or the tests so that "it renders should render a blank div" is an actual sentence/phrase?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. Changed in a91c836.

render(<div />).then(e => expect(e.tagName.toLowerCase()).toBe('div')));

itRenders('should render a div with inline styles', render =>
render(<div style={{color:'red', width:'30px'}} />).then(e => {
expect(e.style.color).toBe('red');
expect(e.style.width).toBe('30px');
})
);

itRenders('should render a self-closing tag', render =>
render(<br />).then(e => expect(e.tagName.toLowerCase()).toBe('br')));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I think tagName is guaranteed to be uppercase so this could just be expect(e.tagName).toBe('BR')?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Modified all three tests in a91c836. Thanks!


itRenders('should render a self-closing tag as a child', render =>
render(<div><br /></div>).then(e => {
expect(e.childNodes.length).toBe(1);
expect(e.firstChild.tagName.toLowerCase()).toBe('br');
})
);
});

});