From fa7a97fc46935e1611d52da2fdb7d53f6ab9577d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 23 Nov 2017 17:44:58 +0000 Subject: [PATCH] Run 90% of tests on compiled bundles (both development and production) (#11633) * Extract Jest config into a separate file * Refactor Jest scripts directory structure Introduces a more consistent naming scheme. * Add yarn test-bundles and yarn test-prod-bundles Only files ending with -test.public.js are opted in (so far we don't have any). * Fix error decoding for production bundles GCC seems to remove `new` from `new Error()` which broke our proxy. * Build production version of react-noop-renderer This lets us test more bundles. * Switch to blacklist (exclude .private.js tests) * Rename tests that are currently broken against bundles to *-test.internal.js Some of these are using private APIs. Some have other issues. * Add bundle tests to CI * Split private and public ReactJSXElementValidator tests * Remove internal deps from ReactServerRendering-test and make it public * Only run tests directly in __tests__ This lets us share code between test files by placing them in __tests__/utils. * Remove ExecutionEnvironment dependency from DOMServerIntegrationTest It's not necessary since Stack. * Split up ReactDOMServerIntegration into test suite and utilities This enables us to further split it down. Good both for parallelization and extracting public parts. * Split Fragment tests from other DOMServerIntegration tests This enables them to opt other DOMServerIntegration tests into bundle testing. * Split ReactDOMServerIntegration into different test files It was way too slow to run all these in sequence. * Don't reset the cache twice in DOMServerIntegration tests We used to do this to simulate testing separate bundles. But now we actually *do* test bundles. So there is no need for this, as it makes tests slower. * Rename test-bundles* commands to test-build* Also add test-prod-build as alias for test-build-prod because I keep messing them up. * Use regenerator polyfill for react-noop This fixes other issues and finally lets us run ReactNoop tests against a prod bundle. * Run most Incremental tests against bundles Now that GCC generator issue is fixed, we can do this. I split ErrorLogging test separately because it does mocking. Other error handling tests don't need it. * Update sizes * Fix ReactMount test * Enable ReactDOMComponent test * Fix a warning issue uncovered by flat bundle testing With flat bundles, we couldn't produce a good warning for
on SSR because it doesn't use the event system. However the issue was not visible in normal Jest runs because the event plugins have been injected by the time the test ran. To solve this, I am explicitly passing whether event system is available as an argument to the hook. This makes the behavior consistent between source and bundle tests. Then I change the tests to document the actual logic and _attempt_ to show a nice message (e.g. we know for sure `onclick` is a bad event but we don't know the right name for it on the server so we just say a generic message about camelCase naming convention). --- package.json | 37 +- ...s => EventPluginRegistry-test.internal.js} | 0 ... => ResponderEventPlugin-test.internal.js} | 0 ...est.js => accumulateInto-test.internal.js} | 0 ...test.js => ReactNativeCS-test.internal.js} | 0 ...ReactBrowserEventEmitter-test.internal.js} | 0 .../src/__tests__/ReactDOMComponent-test.js | 33 +- ...js => ReactDOMFiberAsync-test.internal.js} | 0 ...-test.js => ReactDOMRoot-test.internal.js} | 0 ....js => ReactDOMSelection-test.internal.js} | 0 .../ReactDOMServerIntegration-test.js | 3150 ----------------- ...eactDOMServerIntegrationAttributes-test.js | 597 ++++ .../ReactDOMServerIntegrationBasic-test.js | 152 + .../ReactDOMServerIntegrationContext-test.js | 292 ++ .../ReactDOMServerIntegrationElements-test.js | 865 +++++ .../ReactDOMServerIntegrationForms-test.js | 612 ++++ ...ServerIntegrationFragment-test.internal.js | 107 + ...ctDOMServerIntegrationReconnecting-test.js | 424 +++ .../ReactDOMServerIntegrationRefs-test.js | 99 + .../src/__tests__/ReactMount-test.js | 4 +- .../__tests__/ReactServerRendering-test.js | 42 +- ...js => ReactTreeTraversal-test.internal.js} | 0 .../ReactDOMServerIntegrationTestUtils.js | 328 ++ ...js => validateDOMNesting-test.internal.js} | 0 .../src/client/ReactDOMFiberComponent.js | 2 +- ...> BeforeInputEventPlugin-test.internal.js} | 0 ....js => ChangeEventPlugin-test.internal.js} | 0 ....js => SelectEventPlugin-test.internal.js} | 0 ...> SyntheticKeyboardEvent-test.internal.js} | 0 .../src/server/ReactPartialRenderer.js | 2 +- .../src/shared/ReactDOMUnknownPropertyHook.js | 101 +- ....js => ReactNativeEvents-test.internal.js} | 0 ...t.js => ReactNativeMount-test.internal.js} | 0 ...> ReactNativeEvents-test.internal.js.snap} | 0 ...=> ReactNativeMount-test.internal.js.snap} | 0 ...eactNativeComponentClass-test.internal.js} | 0 packages/react-noop-renderer/npm/index.js | 6 +- packages/react-noop-renderer/package.json | 3 +- ...test.js => ReactFragment-test.internal.js} | 0 .../ReactIncrementalErrorHandling-test.js | 254 -- ...ctIncrementalErrorLogging-test.internal.js | 1207 +++++++ ... => ReactIncrementalPerf-test.internal.js} | 0 ...st.js => ReactPersistent-test.internal.js} | 0 ...eactIncrementalPerf-test.internal.js.snap} | 0 ...test.js => ReactNativeRT-test.internal.js} | 0 ...ReactAsyncClassComponent-test.internal.js} | 0 .../ReactJSXElementValidator-test.internal.js | 104 + .../ReactJSXElementValidator-test.js | 82 - ...st.js => ReactErrorUtils-test.internal.js} | 0 ...js => reactProdInvariant-test.internal.js} | 0 scripts/circleci/test_entry_point.sh | 30 +- ...o-primitive-constructors-test.internal.js} | 0 ...rning-and-invariant-args-test.internal.js} | 0 scripts/jest/config.build.js | 32 + scripts/jest/config.source.js | 20 + scripts/jest/preprocessor.js | 2 +- .../{environment.js => setupEnvironment.js} | 0 ...{test-framework-setup.js => setupTests.js} | 7 +- .../setupTests.js} | 0 scripts/jest/{ => typescript}/jest.d.ts | 0 .../preprocessor.js} | 0 scripts/rollup/bundles.js | 17 +- scripts/rollup/results.json | 94 +- scripts/rollup/wrappers.js | 17 + yarn.lock | 40 +- 65 files changed, 5081 insertions(+), 3681 deletions(-) rename packages/events/__tests__/{EventPluginRegistry-test.js => EventPluginRegistry-test.internal.js} (100%) rename packages/events/__tests__/{ResponderEventPlugin-test.js => ResponderEventPlugin-test.internal.js} (100%) rename packages/events/__tests__/{accumulateInto-test.js => accumulateInto-test.internal.js} (100%) rename packages/react-cs-renderer/src/__tests__/{ReactNativeCS-test.js => ReactNativeCS-test.internal.js} (100%) rename packages/react-dom/src/__tests__/{ReactBrowserEventEmitter-test.js => ReactBrowserEventEmitter-test.internal.js} (100%) rename packages/react-dom/src/__tests__/{ReactDOMFiberAsync-test.js => ReactDOMFiberAsync-test.internal.js} (100%) rename packages/react-dom/src/__tests__/{ReactDOMRoot-test.js => ReactDOMRoot-test.internal.js} (100%) rename packages/react-dom/src/__tests__/{ReactDOMSelection-test.js => ReactDOMSelection-test.internal.js} (100%) delete mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationContext-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.internal.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js rename packages/react-dom/src/__tests__/{ReactTreeTraversal-test.js => ReactTreeTraversal-test.internal.js} (100%) create mode 100644 packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js rename packages/react-dom/src/__tests__/{validateDOMNesting-test.js => validateDOMNesting-test.internal.js} (100%) rename packages/react-dom/src/events/__tests__/{BeforeInputEventPlugin-test.js => BeforeInputEventPlugin-test.internal.js} (100%) rename packages/react-dom/src/events/__tests__/{ChangeEventPlugin-test.js => ChangeEventPlugin-test.internal.js} (100%) rename packages/react-dom/src/events/__tests__/{SelectEventPlugin-test.js => SelectEventPlugin-test.internal.js} (100%) rename packages/react-dom/src/events/__tests__/{SyntheticKeyboardEvent-test.js => SyntheticKeyboardEvent-test.internal.js} (100%) rename packages/react-native-renderer/src/__tests__/{ReactNativeEvents-test.js => ReactNativeEvents-test.internal.js} (100%) rename packages/react-native-renderer/src/__tests__/{ReactNativeMount-test.js => ReactNativeMount-test.internal.js} (100%) rename packages/react-native-renderer/src/__tests__/__snapshots__/{ReactNativeEvents-test.js.snap => ReactNativeEvents-test.internal.js.snap} (100%) rename packages/react-native-renderer/src/__tests__/__snapshots__/{ReactNativeMount-test.js.snap => ReactNativeMount-test.internal.js.snap} (100%) rename packages/react-native-renderer/src/__tests__/{createReactNativeComponentClass-test.js => createReactNativeComponentClass-test.internal.js} (100%) rename packages/react-reconciler/src/__tests__/{ReactFragment-test.js => ReactFragment-test.internal.js} (100%) create mode 100644 packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js rename packages/react-reconciler/src/__tests__/{ReactIncrementalPerf-test.js => ReactIncrementalPerf-test.internal.js} (100%) rename packages/react-reconciler/src/__tests__/{ReactPersistent-test.js => ReactPersistent-test.internal.js} (100%) rename packages/react-reconciler/src/__tests__/__snapshots__/{ReactIncrementalPerf-test.js.snap => ReactIncrementalPerf-test.internal.js.snap} (100%) rename packages/react-rt-renderer/src/__tests__/{ReactNativeRT-test.js => ReactNativeRT-test.internal.js} (100%) rename packages/react/src/__tests__/{ReactAsyncClassComponent-test.js => ReactAsyncClassComponent-test.internal.js} (100%) create mode 100644 packages/react/src/__tests__/ReactJSXElementValidator-test.internal.js rename packages/shared/__tests__/{ReactErrorUtils-test.js => ReactErrorUtils-test.internal.js} (100%) rename packages/shared/__tests__/{reactProdInvariant-test.js => reactProdInvariant-test.internal.js} (100%) rename scripts/eslint-rules/__tests__/{no-primitive-constructors-test.js => no-primitive-constructors-test.internal.js} (100%) rename scripts/eslint-rules/__tests__/{warning-and-invariant-args-test.js => warning-and-invariant-args-test.internal.js} (100%) create mode 100644 scripts/jest/config.build.js create mode 100644 scripts/jest/config.source.js rename scripts/jest/{environment.js => setupEnvironment.js} (100%) rename scripts/jest/{test-framework-setup.js => setupTests.js} (91%) rename scripts/jest/{setupSpecEquivalenceReporter.js => spec-equivalence-reporter/setupTests.js} (100%) rename scripts/jest/{ => typescript}/jest.d.ts (100%) rename scripts/jest/{ts-preprocessor.js => typescript/preprocessor.js} (100%) diff --git a/package.json b/package.json index 65085e7a08b2..c8ce7b99bce5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "babel-plugin-transform-es3-property-literals": "^6.5.0", "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-plugin-transform-react-jsx-source": "^6.8.0", + "babel-plugin-transform-regenerator": "^6.26.0", "babel-preset-react": "^6.5.0", "babel-traverse": "^6.9.0", "babylon": "6.15.0", @@ -103,40 +104,14 @@ "linc": "node ./scripts/tasks/linc.js", "lint": "node ./scripts/tasks/eslint.js", "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", - "test": "cross-env NODE_ENV=development jest", - "test-prod": "cross-env NODE_ENV=production jest", + "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", + "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", + "test-prod-build": "yarn test-build-prod", + "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js", + "test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js", "flow": "node ./scripts/tasks/flow.js", "prettier": "node ./scripts/prettier/index.js write-changed", "prettier-all": "node ./scripts/prettier/index.js write", "version-check": "node ./scripts/tasks/version-check.js" - }, - "jest": { - "modulePathIgnorePatterns": [ - "/scripts/rollup/shims/", - "/scripts/bench/" - ], - "transform": { - ".*": "./scripts/jest/preprocessor.js" - }, - "setupFiles": [ - "./scripts/jest/environment.js" - ], - "setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js", - "testRegex": "/__tests__/.*(\\.js|\\.coffee|[^d]\\.ts)$", - "moduleFileExtensions": [ - "js", - "json", - "node", - "coffee", - "ts" - ], - "roots": [ - "/packages", - "/scripts" - ], - "collectCoverageFrom": [ - "packages/**/*.js" - ], - "timers": "fake" } } diff --git a/packages/events/__tests__/EventPluginRegistry-test.js b/packages/events/__tests__/EventPluginRegistry-test.internal.js similarity index 100% rename from packages/events/__tests__/EventPluginRegistry-test.js rename to packages/events/__tests__/EventPluginRegistry-test.internal.js diff --git a/packages/events/__tests__/ResponderEventPlugin-test.js b/packages/events/__tests__/ResponderEventPlugin-test.internal.js similarity index 100% rename from packages/events/__tests__/ResponderEventPlugin-test.js rename to packages/events/__tests__/ResponderEventPlugin-test.internal.js diff --git a/packages/events/__tests__/accumulateInto-test.js b/packages/events/__tests__/accumulateInto-test.internal.js similarity index 100% rename from packages/events/__tests__/accumulateInto-test.js rename to packages/events/__tests__/accumulateInto-test.internal.js diff --git a/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js b/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.internal.js similarity index 100% rename from packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js rename to packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js similarity index 100% rename from packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.js rename to packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 2c40743854c3..4414dd37e33a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -1709,15 +1709,25 @@ describe('ReactDOMComponent', () => { it('should warn about incorrect casing on event handlers (ssr)', () => { spyOnDev(console, 'error'); ReactDOMServer.renderToString( - React.createElement('input', {type: 'text', onclick: '1'}), + React.createElement('input', {type: 'text', oninput: '1'}), ); ReactDOMServer.renderToString( React.createElement('input', {type: 'text', onKeydown: '1'}), ); if (__DEV__) { - expect(console.error.calls.count()).toBe(2); - expect(console.error.calls.argsFor(0)[0]).toContain('onClick'); - expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown'); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Invalid event handler property `oninput`. ' + + 'React events use the camelCase naming convention, ' + + // Note: we don't know the right event name so we + // use a generic one (onClick) as a suggestion. + // This is because we don't bundle the event system + // on the server. + 'for example `onClick`.', + ); + // We can't warn for `onKeydown` on the server because + // there is no way tell if this is a valid event or not + // without access to the event system (which we don't bundle). } }); @@ -1735,14 +1745,14 @@ describe('ReactDOMComponent', () => { it('should warn about incorrect casing on event handlers', () => { spyOnDev(console, 'error'); ReactTestUtils.renderIntoDocument( - React.createElement('input', {type: 'text', onclick: '1'}), + React.createElement('input', {type: 'text', oninput: '1'}), ); ReactTestUtils.renderIntoDocument( React.createElement('input', {type: 'text', onKeydown: '1'}), ); if (__DEV__) { expect(console.error.calls.count()).toBe(2); - expect(console.error.calls.argsFor(0)[0]).toContain('onClick'); + expect(console.error.calls.argsFor(0)[0]).toContain('onInput'); expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown'); } }); @@ -1860,15 +1870,20 @@ describe('ReactDOMComponent', () => { it('gives source code refs for unknown prop warning (ssr)', () => { spyOnDev(console, 'error'); ReactDOMServer.renderToString(
); - ReactDOMServer.renderToString(); + ReactDOMServer.renderToString(); if (__DEV__) { expect(console.error.calls.count()).toBe(2); expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)', ); expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( - 'Warning: Invalid event handler property `onclick`. Did you mean ' + - '`onClick`?\n in input (at **)', + 'Warning: Invalid event handler property `oninput`. ' + + // Note: we don't know the right event name so we + // use a generic one (onClick) as a suggestion. + // This is because we don't bundle the event system + // on the server. + 'React events use the camelCase naming convention, for example `onClick`.' + + '\n in input (at **)', ); } }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js similarity index 100% rename from packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js rename to packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.internal.js similarity index 100% rename from packages/react-dom/src/__tests__/ReactDOMRoot-test.js rename to packages/react-dom/src/__tests__/ReactDOMRoot-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactDOMSelection-test.js b/packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js similarity index 100% rename from packages/react-dom/src/__tests__/ReactDOMSelection-test.js rename to packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js deleted file mode 100644 index efc2e7e514bd..000000000000 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js +++ /dev/null @@ -1,3150 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * 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 - */ - -'use strict'; - -let ExecutionEnvironment; -let PropTypes; -let React; -let ReactDOM; -let ReactDOMServer; -let ReactTestUtils; - -const stream = require('stream'); - -const TEXT_NODE_TYPE = 3; - -// Helper functions for rendering tests -// ==================================== - -// promisified version of ReactDOM.render() -function asyncReactDOMRender(reactElement, domElement, forceHydrate) { - return new Promise(resolve => { - if (forceHydrate) { - ReactDOM.hydrate(reactElement, domElement); - } else { - ReactDOM.render(reactElement, domElement); - } - // We can't use the callback for resolution because that will not catch - // errors. They're thrown. - resolve(); - }); -} -// 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. -async function expectErrors(fn, count) { - if (console.error.calls && console.error.calls.reset) { - console.error.calls.reset(); - } else { - spyOnDev(console, 'error'); - } - - const result = await fn(); - if ( - console.error.calls && - console.error.calls.count() !== count && - console.error.calls.count() !== 0 - ) { - 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]); - } - } - } - if (__DEV__) { - expect(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, forceHydrate, errorCount = 0) { - return expectErrors(async () => { - ExecutionEnvironment.canUseDOM = true; - await asyncReactDOMRender(reactElement, domElement, forceHydrate); - ExecutionEnvironment.canUseDOM = false; - return domElement.firstChild; - }, errorCount); -} - -async function renderIntoString(reactElement, errorCount = 0) { - return await expectErrors( - () => - new Promise(resolve => - resolve(ReactDOMServer.renderToString(reactElement)), - ), - 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. -async function serverRender(reactElement, errorCount = 0) { - const markup = await renderIntoString(reactElement, errorCount); - var domElement = document.createElement('div'); - domElement.innerHTML = markup; - return domElement.firstChild; -} - -// this just drains a readable piped into it to a string, which can be accessed -// via .buffer. -class DrainWritable extends stream.Writable { - constructor(options) { - super(options); - this.buffer = ''; - } - - _write(chunk, encoding, cb) { - this.buffer += chunk; - cb(); - } -} - -async function renderIntoStream(reactElement, errorCount = 0) { - return await expectErrors( - () => - new Promise(resolve => { - let writable = new DrainWritable(); - ReactDOMServer.renderToNodeStream(reactElement).pipe(writable); - writable.on('finish', () => resolve(writable.buffer)); - }), - errorCount, - ); -} - -// Renders text using node stream 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. -async function streamRender(reactElement, errorCount = 0) { - const markup = await renderIntoStream(reactElement, errorCount); - 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, false, errorCount); -}; - -const clientRenderOnServerString = async (element, errorCount = 0) => { - const markup = await renderIntoString(element, errorCount); - resetModules(); - - var domElement = document.createElement('div'); - domElement.innerHTML = markup; - let serverNode = domElement.firstChild; - - const firstClientNode = await renderIntoDom( - element, - domElement, - true, - 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); - // 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() {} - -const clientRenderOnBadMarkup = async (element, errorCount = 0) => { - // First we render the top of bad mark up. - var domElement = document.createElement('div'); - domElement.innerHTML = - '
'; - await renderIntoDom(element, domElement, true, errorCount + 1); - - // This gives us the resulting text content. - var hydratedTextContent = domElement.textContent; - - // Next we render the element into a clean DOM node client side. - const cleanDomElement = document.createElement('div'); - ExecutionEnvironment.canUseDOM = true; - await asyncReactDOMRender(element, cleanDomElement, true); - ExecutionEnvironment.canUseDOM = false; - // This gives us the expected text content. - const cleanTextContent = cleanDomElement.textContent; - - // The only guarantee is that text content has been patched up if needed. - expect(hydratedTextContent).toBe(cleanTextContent); - - // Abort any further expects. All bets are off at this point. - throw new BadMarkupExpected(); -}; - -// 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(`renders ${desc} with server string render`, () => testFn(serverRender)); - it(`renders ${desc} with server stream render`, () => testFn(streamRender)); - 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(`renders ${desc} with clean client render`, () => - testFn(clientCleanRender)); - it(`renders ${desc} with client render on top of good server markup`, () => - testFn(clientRenderOnServerString)); - it(`renders ${ - desc - } with client render on top of bad server markup`, async () => { - try { - await testFn(clientRenderOnBadMarkup); - } catch (x) { - // We expect this to trigger the BadMarkupExpected rejection. - if (!(x instanceof BadMarkupExpected)) { - // If not, rethrow. - throw x; - } - } - }); -} - -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, partialMessage) { - itThrows( - `when rendering ${desc} with server string render`, - () => testFn(serverRender), - partialMessage, - ); - 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 - // get the usual markup mismatch warning. - itThrows( - `when rendering ${desc} with client render on top of bad server markup`, - () => - testFn((element, warningCount = 0) => - clientRenderOnBadMarkup(element, warningCount - 1), - ), - partialMessage, - ); -} - -// renders serverElement to a string, sticks it into a DOM element, and then -// tries to render clientElement on top of it. shouldMatch is a boolean -// telling whether we should expect the markup to match or not. -async function testMarkupMatch(serverElement, clientElement, shouldMatch) { - const domElement = await serverRender(serverElement); - resetModules(); - return renderIntoDom( - clientElement, - domElement.parentNode, - true, - shouldMatch ? 0 : 1, - ); -} - -// expects that rendering clientElement on top of a server-rendered -// serverElement does NOT raise a markup mismatch warning. -function expectMarkupMatch(serverElement, clientElement) { - return testMarkupMatch(serverElement, clientElement, true); -} - -// expects that rendering clientElement on top of a server-rendered -// serverElement DOES raise a markup mismatch warning. -function expectMarkupMismatch(serverElement, clientElement) { - return testMarkupMatch(serverElement, clientElement, false); -} - -// When there is a test that renders on server and then on client and expects a logged -// error, you want to see the error show up both on server and client. Unfortunately, -// React refuses to issue the same error twice to avoid clogging up the console. -// To get around this, we must reload React modules in between server and client render. -function resetModules() { - // First, reset the modules to load the client renderer. - jest.resetModuleRegistry(); - - // TODO: can we express this test with only public API? - ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment'); - require('shared/ReactFeatureFlags').enableReactFragment = true; - - PropTypes = require('prop-types'); - React = require('react'); - ReactDOM = require('react-dom'); - ReactTestUtils = require('react-dom/test-utils'); - - // Now we reset the modules again to load the server renderer. - // Resetting is important because we want to avoid any shared state - // influencing the tests. - jest.resetModuleRegistry(); - require('shared/ReactFeatureFlags').enableReactFragment = true; - ReactDOMServer = require('react-dom/server'); -} - -describe('ReactDOMServerIntegration', () => { - beforeEach(() => { - resetModules(); - - ExecutionEnvironment.canUseDOM = false; - }); - - describe('basic rendering', function() { - itRenders('a blank div', async render => { - const e = await render(
); - expect(e.tagName).toBe('DIV'); - }); - - itRenders('a self-closing tag', async render => { - const e = await render(
); - expect(e.tagName).toBe('BR'); - }); - - itRenders('a self-closing tag as a child', async render => { - const e = await render( -
-
-
, - ); - expect(e.childNodes.length).toBe(1); - expect(e.firstChild.tagName).toBe('BR'); - }); - - 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([
text1
]); - let parent = e.parentNode; - expect(parent.childNodes[0].tagName).toBe('DIV'); - }); - - itRenders('an array with several children', async render => { - let Header = props => { - return

header

; - }; - let Footer = props => { - return [

footer

,

about

]; - }; - let e = await render([ -
text1
, - text2, -
, -