From a48b7b32e5954f97834217e17d61e4c8bd3e86fe Mon Sep 17 00:00:00 2001 From: Sasha Aickin Date: Sun, 15 May 2016 22:49:22 -0700 Subject: [PATCH 01/31] Interim checking with a bunch of unit tests for server rendering, and the unit tests all pass right now. Still more to add. --- .../__tests__/ReactServerRendering-test.js | 1366 +++++++++++++++++ 1 file changed, 1366 insertions(+) diff --git a/src/renderers/dom/server/__tests__/ReactServerRendering-test.js b/src/renderers/dom/server/__tests__/ReactServerRendering-test.js index d3919e7b93be..bb515244257b 100644 --- a/src/renderers/dom/server/__tests__/ReactServerRendering-test.js +++ b/src/renderers/dom/server/__tests__/ReactServerRendering-test.js @@ -22,6 +22,175 @@ var ReactServerRendering; var ID_ATTRIBUTE_NAME; var ROOT_ATTRIBUTE_NAME; +const TEXT_NODE_TYPE = 3; +const COMMENT_NODE_TYPE = 8; + +// Renders text using SSR and then stuffs it into a DOM node, which is returned. +// Does not perform client-side reconnect. +function renderOnServer(reactElement, warningCount = 0) { + const markup = expectWarnings( + () => ReactServerRendering.renderToString(reactElement), + warningCount); + + var domElement = document.createElement('div'); + domElement.innerHTML = markup; + return domElement; +} + +// returns a DOM of the react element when server rendered and NOT rendered on client. +function getSsrDom(reactElement, warningCount = 0) { + return renderOnServer(reactElement, warningCount).firstChild; +} + +function expectWarnings(fn, count) { + var oldConsoleError = console.error; + console.error = jasmine.createSpy(); + try { + var result = fn(); + } finally { + expect(console.error.argsForCall.length).toBe(count); + console.error = oldConsoleError; + } + return result; +} + +function renderOnClient(reactElement, domElement, warningCount = 0) { + expectWarnings(() => ReactDOM.render(reactElement, domElement), warningCount); + return domElement; +} +// renders the first element with renderToString, puts it into a DOM node, +// runs React.render on that DOM node with the second element. returns the DOM +// node. +function connectToServerRendering( + elementToRenderOnServer, + elementToRenderOnClient = elementToRenderOnServer, + shouldMatch = true, + warningCount = 0 +) { + return renderOnClient( + elementToRenderOnClient, + renderOnServer(elementToRenderOnServer, warningCount), + warningCount + (shouldMatch ? 0 : 1)); +} + +function expectMarkupMismatch(serverRendering, elementToRenderOnClient, warningCount = 0) { + if (typeof serverRendering === 'string') { + var domElement = document.createElement('div'); + domElement.innerHTML = serverRendering; + return renderOnClient(elementToRenderOnClient, domElement, warningCount + 1); + } + return connectToServerRendering(serverRendering, elementToRenderOnClient, false, warningCount); +} + +function expectMarkupMatch(serverRendering, elementToRenderOnClient = serverRendering, warningCount = 0) { + if (typeof serverRendering === 'string') { + var domElement = document.createElement('div'); + domElement.innerHTML = serverRendering; + return renderOnClient(elementToRenderOnClient, domElement, warningCount); + } + return connectToServerRendering(serverRendering, elementToRenderOnClient, true, warningCount); +} + +function promiseToJasmineTest(promiseFn) { + return function() { + var done = false; + waitsFor(() => done); + promiseFn().then(() => done = true); + }; +} + +function failingPromiseToJasmineTest(promiseFn) { + return function() { + var done = false; + waitsFor(() => done); + promiseFn().catch(() => done = true); + }; +} + +const serverStringRender = (element, warningCount = 0) => { + try { + return Promise.resolve(getSsrDom(element, warningCount)); + } catch (e) { + return Promise.reject(e); + } +}; +const clientRender = (element, warningCount = 0) => { + try { + const div = document.createElement('div'); + return Promise.resolve(renderOnClient(element, div, warningCount).firstChild); + } catch (e) { + return Promise.reject(e); + } +}; +const clientRenderOnServerString = (element, warningCount = 0) => { + try { + return Promise.resolve(connectToServerRendering(element, element, true, warningCount).firstChild); + } catch (e) { + return Promise.reject(e); + } +}; +const clientRenderOnBadMarkup = (element, warningCount = 0) => { + try { + var domElement = document.createElement('div'); + domElement.innerHTML = '
'; + // ReactDOM.render(element,domElement); + return Promise.resolve(renderOnClient(element, domElement, warningCount + 1).firstChild); + } catch (e) { + return Promise.reject(e); + } +}; + +// 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 returns a promise of a DOM Element. +// +// Note that 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`, promiseToJasmineTest( + () => testFn(serverStringRender))); + itClientRenders(desc, testFn); +} + +// run testFn in four 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 takes in a render function and returns a Promise that resolves or rejects +// when the test is done. the render function takes in a ReactElement 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`, promiseToJasmineTest( + () => testFn(clientRender))); + it(`${desc} with client render on top of server string markup`, promiseToJasmineTest( + () => testFn(clientRenderOnServerString))); + it(`${desc} with client render on top of bad server markup`, promiseToJasmineTest( + () => testFn(clientRenderOnBadMarkup))); +} + +function itThrowsOnRender(desc, testFn) { + it(`${desc} with server string render`, failingPromiseToJasmineTest( + () => testFn(serverStringRender))); + it(`${desc} with clean client render`, failingPromiseToJasmineTest( + () => testFn(clientRender))); + + // we subtract one from the warning count here because the throw means that it won't + // get the usual markup mismatch warning. + it(`${desc} with client render on top of bad server markup`, failingPromiseToJasmineTest( + () => testFn((element, warningCount = 0) => clientRenderOnBadMarkup(element, warningCount - 1)))); +} + + describe('ReactServerRendering', function() { beforeEach(function() { jest.resetModuleRegistry(); @@ -260,6 +429,1203 @@ describe('ReactServerRendering', function() { expect(numClicks).toEqual(2); }); + itRenders('should get initial state from getInitialState', (render) => { + const Component = React.createClass({ + getInitialState: function() { + return {text: 'foo'}; + }, + render: function() { + return
{this.state.text}
; + }, + }); + return render().then(e => expect(e.textContent).toBe('foo')); + }); + + describe('basic rendering', function() { + itRenders('should render a blank div', render => + render(
).then(e => expect(e.tagName.toLowerCase()).toBe('div'))); + itRenders('should reconnect a div with inline styles', render => + render(
).then(e => { + expect(e.style.color).toBe('red'); + expect(e.style.width).toBe('30px'); + }) + ); + itRenders('should reconnect a self-closing tag', render => + render(
).then(e => expect(e.tagName.toLowerCase()).toBe('br'))); + itRenders('should reconnect a self-closing tag as a child', render => + render(

).then(e => { + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.tagName.toLowerCase()).toBe('br'); + })); + }); + + describe('property to attribute mapping', function() { + itRenders('renders simple numbers', (render) => { + return render(
).then(e => expect(e.getAttribute('width')).toBe('30')); + }); + + itRenders('renders simple strings', (render) => { + return render(
).then(e => expect(e.getAttribute('width')).toBe('30')); + }); + + itRenders('renders booleans correctly', (render) => { + return Promise.all([ + render(