diff --git a/.travis.yml b/.travis.yml index f27e0e9a2b8..6dbea3e4ee5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ script: - 'if [ $TEST_SUITE = "simple" ]; then tasks/e2e-simple.sh; fi' - 'if [ $TEST_SUITE = "installs" ]; then tasks/e2e-installs.sh; fi' - 'if [ $TEST_SUITE = "kitchensink" ]; then tasks/e2e-kitchensink.sh; fi' + - 'if [ $TEST_SUITE = "kitchensink-eject" ]; then tasks/e2e-kitchensink-eject.sh; fi' - 'if [ $TEST_SUITE = "old-node" ]; then tasks/e2e-old-node.sh; fi' - 'if [ $TEST_SUITE = "monorepos" ]; then tasks/e2e-monorepos.sh; fi' env: @@ -21,6 +22,7 @@ env: - TEST_SUITE=simple - TEST_SUITE=installs - TEST_SUITE=kitchensink + - TEST_SUITE=kitchensink-eject - TEST_SUITE=monorepos matrix: include: @@ -28,3 +30,5 @@ matrix: env: TEST_SUITE=old-node - node_js: 6 env: TEST_SUITE=kitchensink + - node_js: 6 + env: TEST_SUITE=kitchensink-eject diff --git a/appveyor.yml b/appveyor.yml index db1d640caa1..9cb7b58f8a4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,6 +8,8 @@ environment: test_suite: "installs" - nodejs_version: 8 test_suite: "kitchensink" + - nodejs_version: 8 + test_suite: "kitchensink-eject" - nodejs_version: 8 test_suite: "monorepos" - nodejs_version: 6 @@ -16,6 +18,8 @@ environment: test_suite: "installs" - nodejs_version: 6 test_suite: "kitchensink" + - nodejs_version: 6 + test_suite: "kitchensink-eject" - nodejs_version: 6 test_suite: "monorepos" cache: diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js index 43badcbde8e..b1f5f5d5544 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js @@ -35,6 +35,7 @@ describe('Integration', () => { 'x-from-development-env' ); } + doc.defaultView.close(); }); it('NODE_PATH', async () => { @@ -43,6 +44,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-node-path').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('PUBLIC_URL', async () => { @@ -58,6 +60,7 @@ describe('Integration', () => { expect( doc.querySelector('head link[rel="shortcut icon"]').getAttribute('href') ).to.equal(`${prefix}/favicon.ico`); + doc.defaultView.close(); }); it('shell env variables', async () => { @@ -66,6 +69,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-shell-env-variables').textContent ).to.equal('fromtheshell.'); + doc.defaultView.close(); }); it('expand .env variables', async () => { @@ -83,6 +87,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-expand-env-existing').textContent ).to.equal('fromtheshell'); + doc.defaultView.close(); }); }); }); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js index 36e8de9999f..0ed5a940891 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js @@ -62,6 +62,7 @@ export default feature => created: (_, win) => win.addEventListener('ReactFeatureDidMount', () => resolve(doc), true), deferClose: true, + pretendToBeVisual: true, resourceLoader, url: `${host}#${feature}`, virtualConsole: jsdom.createVirtualConsole().sendTo(console), diff --git a/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js b/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js index 82e2c9e0db2..9e34b669d6e 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js @@ -16,6 +16,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-array-destructuring').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('array spread', async () => { @@ -24,6 +25,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-array-spread').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('async/await', async () => { @@ -32,6 +34,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-async-await').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('class properties', async () => { @@ -40,6 +43,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-class-properties').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('computed properties', async () => { @@ -48,6 +52,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-computed-properties').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('custom interpolation', async () => { @@ -56,6 +61,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-custom-interpolation').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('default parameters', async () => { @@ -64,6 +70,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-default-parameters').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('destructuring and await', async () => { @@ -72,6 +79,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-destructuring-and-await').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('generators', async () => { @@ -80,6 +88,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-generators').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('object destructuring', async () => { @@ -88,6 +97,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-object-destructuring').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('object spread', async () => { @@ -96,6 +106,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-object-spread').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('promises', async () => { @@ -104,6 +115,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-promises').childElementCount).to.equal( 4 ); + doc.defaultView.close(); }); it('rest + default', async () => { @@ -112,6 +124,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-rest-and-default').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('rest parameters', async () => { @@ -120,6 +133,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-rest-parameters').childElementCount ).to.equal(4); + doc.defaultView.close(); }); it('template interpolation', async () => { @@ -128,6 +142,7 @@ describe('Integration', () => { expect( doc.getElementById('feature-template-interpolation').childElementCount ).to.equal(4); + doc.defaultView.close(); }); }); }); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index ff3d383219d..4aca73603dd 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -11,63 +11,75 @@ import url from 'url'; const matchCSS = (doc, regexes) => { if (process.env.E2E_FILE) { - const elements = doc.getElementsByTagName('link'); - let href = ""; - for (const elem of elements) { - if (elem.rel === 'stylesheet') { - href = elem.href; - } + const elements = doc.getElementsByTagName('link'); + let href = ''; + for (const elem of elements) { + if (elem.rel === 'stylesheet') { + href = elem.href; } - resourceLoader( - { url: url.parse(href) }, - (_, textContent) => { - for (const regex of regexes) { - expect(textContent).to.match(regex); - } - } - ); - + } + resourceLoader({ url: url.parse(href) }, (_, textContent) => { + for (const regex of regexes) { + expect(textContent).to.match(regex); + } + }); } else { for (let i = 0; i < regexes.length; ++i) { - expect(doc.getElementsByTagName('style')[i].textContent.replace(/\s/g, '')).to.match(regexes[i]); + expect( + doc.getElementsByTagName('style')[i].textContent.replace(/\s/g, '') + ).to.match(regexes[i]); } } -} +}; describe('Integration', () => { describe('Webpack plugins', () => { it('css inclusion', async () => { const doc = await initDOM('css-inclusion'); - matchCSS(doc, [/html\{/, /#feature-css-inclusion\{background:.+;color:.+}/]); + matchCSS(doc, [ + /html\{/, + /#feature-css-inclusion\{background:.+;color:.+}/, + ]); + doc.defaultView.close(); }); it('css modules inclusion', async () => { const doc = await initDOM('css-modules-inclusion'); - matchCSS(doc, [/.+style_cssModulesInclusion__.+\{background:.+;color:.+}/, - /.+assets_cssModulesIndexInclusion__.+\{background:.+;color:.+}/]); + matchCSS(doc, [ + /.+style_cssModulesInclusion__.+\{background:.+;color:.+}/, + /.+assets_cssModulesIndexInclusion__.+\{background:.+;color:.+}/, + ]); + doc.defaultView.close(); }); it('scss inclusion', async () => { const doc = await initDOM('scss-inclusion'); matchCSS(doc, [/#feature-scss-inclusion\{background:.+;color:.+}/]); + doc.defaultView.close(); }); it('scss modules inclusion', async () => { const doc = await initDOM('scss-modules-inclusion'); - matchCSS(doc, [/.+scss-styles_scssModulesInclusion.+\{background:.+;color:.+}/, - /.+assets_scssModulesIndexInclusion.+\{background:.+;color:.+}/]); - + matchCSS(doc, [ + /.+scss-styles_scssModulesInclusion.+\{background:.+;color:.+}/, + /.+assets_scssModulesIndexInclusion.+\{background:.+;color:.+}/, + ]); + doc.defaultView.close(); }); it('sass inclusion', async () => { const doc = await initDOM('sass-inclusion'); matchCSS(doc, [/#feature-sass-inclusion\{background:.+;color:.+}/]); + doc.defaultView.close(); }); it('sass modules inclusion', async () => { const doc = await initDOM('sass-modules-inclusion'); - matchCSS(doc, [/.+sass-styles_sassModulesInclusion.+\{background:.+;color:.+}/, - /.+assets_sassModulesIndexInclusion.+\{background:.+;color:.+}/]); + matchCSS(doc, [ + /.+sass-styles_sassModulesInclusion.+\{background:.+;color:.+}/, + /.+assets_sassModulesIndexInclusion.+\{background:.+;color:.+}/, + ]); + doc.defaultView.close(); }); it('graphql files inclusion', async () => { @@ -78,6 +90,7 @@ describe('Integration', () => { expect(children[0].textContent.replace(/\s/g, '')).to.equal( '{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","variableDefinitions":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"test"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"test"},"value":{"kind":"StringValue","value":"test","block":false}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"test"},"arguments":[],"directives":[]}]}}]}}],"loc":{"start":0,"end":40,"source":{"body":"{\\ntest(test:\\"test\\"){\\ntest\\n}\\n}\\n","name":"GraphQLrequest","locationOffset":{"line":1,"column":1}}}}' ); + doc.defaultView.close(); }); it('image inclusion', async () => { @@ -86,6 +99,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-image-inclusion').src).to.match( /^data:image\/jpeg;base64.+==$/ ); + doc.defaultView.close(); }); it('no ext inclusion', async () => { @@ -94,6 +108,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-no-ext-inclusion').href).to.match( /\/static\/media\/aFileWithoutExt\.[a-f0-9]{8}\.bin$/ ); + doc.defaultView.close(); }); it('json inclusion', async () => { @@ -102,6 +117,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-json-inclusion').textContent).to.equal( 'This is an abstract.' ); + doc.defaultView.close(); }); it('linked modules', async () => { @@ -110,6 +126,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-linked-modules').textContent).to.equal( '2.0.0' ); + doc.defaultView.close(); }); it('svg inclusion', async () => { @@ -117,6 +134,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-svg-inclusion').src).to.match( /\/static\/media\/logo\..+\.svg$/ ); + doc.defaultView.close(); }); it('svg component', async () => { @@ -125,11 +143,13 @@ describe('Integration', () => { expect(doc.getElementById('feature-svg-component').textContent).to.equal( '' ); + doc.defaultView.close(); }); it('svg in css', async () => { const doc = await initDOM('svg-in-css'); matchCSS(doc, [/\/static\/media\/logo\..+\.svg/]); + doc.defaultView.close(); }); it('unknown ext inclusion', async () => { @@ -138,6 +158,7 @@ describe('Integration', () => { expect(doc.getElementById('feature-unknown-ext-inclusion').href).to.match( /\/static\/media\/aFileWithExt\.[a-f0-9]{8}\.unknown$/ ); + doc.defaultView.close(); }); }); }); diff --git a/tasks/e2e-kitchensink-eject.sh b/tasks/e2e-kitchensink-eject.sh new file mode 100755 index 00000000000..76a50f1b8be --- /dev/null +++ b/tasks/e2e-kitchensink-eject.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# Copyright (c) 2015-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. + +# ****************************************************************************** +# This is an end-to-end kitchensink test intended to run on CI. +# You can also run it locally but it's slow. +# ****************************************************************************** + +# Start in tasks/ even if run from root directory +cd "$(dirname "$0")" + +# CLI, app, and test module temporary locations +# http://unix.stackexchange.com/a/84980 +temp_app_path=`mktemp -d 2>/dev/null || mktemp -d -t 'temp_app_path'` +temp_module_path=`mktemp -d 2>/dev/null || mktemp -d -t 'temp_module_path'` +custom_registry_url=http://localhost:4873 +original_npm_registry_url=`npm get registry` +original_yarn_registry_url=`yarn config get registry` + +function cleanup { + echo 'Cleaning up.' + unset BROWSERSLIST + ps -ef | grep 'react-scripts' | grep -v grep | awk '{print $2}' | xargs kill -9 + cd "$root_path" + # TODO: fix "Device or resource busy" and remove ``|| $CI` + rm -rf "$temp_app_path" "$temp_module_path" || $CI + npm set registry "$original_npm_registry_url" + yarn config set registry "$original_yarn_registry_url" +} + +# Error messages are redirected to stderr +function handle_error { + echo "$(basename $0): ERROR! An error was encountered executing line $1." 1>&2; + cleanup + echo 'Exiting with error.' 1>&2; + exit 1 +} + +function handle_exit { + cleanup + echo 'Exiting without error.' 1>&2; + exit +} + +# Check for the existence of one or more files. +function exists { + for f in $*; do + test -e "$f" + done +} + +# Exit the script with a helpful error message when any error is encountered +trap 'set +x; handle_error $LINENO $BASH_COMMAND' ERR + +# Cleanup before exit on any termination signal +trap 'set +x; handle_exit' SIGQUIT SIGTERM SIGINT SIGKILL SIGHUP + +# Echo every command being executed +set -x + +# Go to root +cd .. +root_path=$PWD + +if hash npm 2>/dev/null +then + npm i -g npm@latest +fi + +# Bootstrap monorepo +yarn + +# ****************************************************************************** +# First, publish the monorepo. +# ****************************************************************************** + +# Start local registry +tmp_registry_log=`mktemp` +nohup npx verdaccio@3.2.0 -c tasks/verdaccio.yaml &>$tmp_registry_log & +# Wait for `verdaccio` to boot +grep -q 'http address' <(tail -f $tmp_registry_log) + +# Set registry to local registry +npm set registry "$custom_registry_url" +yarn config set registry "$custom_registry_url" + +# Login so we can publish packages +(cd && npx npm-auth-to-token@1.0.0 -u user -p password -e user@example.com -r "$custom_registry_url") + +# Publish the monorepo +git clean -df +./tasks/publish.sh --yes --force-publish=* --skip-git --cd-version=prerelease --exact --npm-tag=latest + +# ****************************************************************************** +# Now that we have published them, create a clean app folder and install them. +# ****************************************************************************** + +# Install the app in a temporary location +cd $temp_app_path +npx create-react-app --internal-testing-template="$root_path"/packages/react-scripts/fixtures/kitchensink test-kitchensink + +# Install the test module +cd "$temp_module_path" +yarn add test-integrity@^2.0.1 + +# ****************************************************************************** +# Now that we used create-react-app to create an app depending on react-scripts, +# let's make sure all npm scripts are in the working state. +# ****************************************************************************** + +# Enter the app directory +cd "$temp_app_path/test-kitchensink" + +# In kitchensink, we want to test all transforms +export BROWSERSLIST='ie 9' + +# Link to test module +npm link "$temp_module_path/node_modules/test-integrity" + +# ****************************************************************************** +# Finally, let's check that everything still works after ejecting. +# ****************************************************************************** + +# Eject... +echo yes | npm run eject + +# Link to test module +npm link "$temp_module_path/node_modules/test-integrity" + +# Test the build +REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ + yarn build + +# Check for expected output +exists build/*.html +exists build/static/js/main.*.js + +# Unit tests +REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + CI=true \ + NODE_PATH=src \ + NODE_ENV=test \ + yarn test --no-cache --testPathPattern=src + +# Test "development" environment +tmp_server_log=`mktemp` +PORT=3002 \ + REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + NODE_PATH=src \ + nohup yarn start &>$tmp_server_log & +grep -q 'You can now view' <(tail -f $tmp_server_log) +E2E_URL="http://localhost:3002" \ + REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ + CI=true NODE_PATH=src \ + NODE_ENV=development \ + BABEL_ENV=test \ + node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js + +# Test "production" environment +E2E_FILE=./build/index.html \ + CI=true \ + NODE_ENV=production \ + BABEL_ENV=test \ + NODE_PATH=src \ + PUBLIC_URL=http://www.example.org/spa/ \ + node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js + +# Cleanup +cleanup diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh index 833956d0c0e..c79f9f77a10 100755 --- a/tasks/e2e-kitchensink.sh +++ b/tasks/e2e-kitchensink.sh @@ -161,55 +161,5 @@ E2E_FILE=./build/index.html \ PUBLIC_URL=http://www.example.org/spa/ \ node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js -# ****************************************************************************** -# Finally, let's check that everything still works after ejecting. -# ****************************************************************************** - -# Eject... -echo yes | npm run eject - -# Link to test module -npm link "$temp_module_path/node_modules/test-integrity" - -# Test the build -REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ - NODE_PATH=src \ - PUBLIC_URL=http://www.example.org/spa/ \ - yarn build - -# Check for expected output -exists build/*.html -exists build/static/js/main.*.js - -# Unit tests -REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ - CI=true \ - NODE_PATH=src \ - NODE_ENV=test \ - yarn test --no-cache --testPathPattern=src - -# Test "development" environment -tmp_server_log=`mktemp` -PORT=3002 \ - REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ - NODE_PATH=src \ - nohup yarn start &>$tmp_server_log & -grep -q 'You can now view' <(tail -f $tmp_server_log) -E2E_URL="http://localhost:3002" \ - REACT_APP_SHELL_ENV_MESSAGE=fromtheshell \ - CI=true NODE_PATH=src \ - NODE_ENV=development \ - BABEL_ENV=test \ - node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js - -# Test "production" environment -E2E_FILE=./build/index.html \ - CI=true \ - NODE_ENV=production \ - BABEL_ENV=test \ - NODE_PATH=src \ - PUBLIC_URL=http://www.example.org/spa/ \ - node_modules/.bin/mocha --compilers js:@babel/register --require @babel/polyfill integration/*.test.js - # Cleanup cleanup