-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Changes from 6 commits
437d029
7c85408
c1d9787
5531bf3
9257660
08b4936
a91c836
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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; | ||
|
@@ -549,4 +661,27 @@ describe('ReactDOMServer', () => { | |
); | ||
}).toThrowError(/Cannot assign to read only property.*/); | ||
}); | ||
|
||
describe('basic rendering', function() { | ||
itRenders('should render a blank div', render => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')?) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
}) | ||
); | ||
}); | ||
|
||
}); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, whereasitClientRenders
is for tests that need to test interactivity (things like events, refs, etc.). The latter tests are only possible whenReactDOM.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?