From ca3177f50c10bdfc5d5df5bc91e06d82730be2ab Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 2 Apr 2024 18:54:03 -0400 Subject: [PATCH] Use jest projects instead of custom jest wrapper This uses Jest's first party support for "projects" instead of passing different config flags to jest. This should make it easier to locally run all variations of a single test and removes all the custom argument parsing. ## TODO - is the outcome actually easier? - look into devtools tests - figure out how to handle the "build" variants, I'm thinking those might be a different package.json script that sets an env variable as it's probably rarely used locally (super slow to build before) --- .circleci/config.yml | 27 +- jest.config.js | 164 ++++++++++ package.json | 8 +- scripts/jest/config.base.js | 32 -- scripts/jest/config.build-devtools.js | 110 ------- scripts/jest/config.build.js | 77 ----- scripts/jest/config.source-persistent.js | 21 -- scripts/jest/config.source-www.js | 16 - scripts/jest/config.source.js | 15 - scripts/jest/dont-run-jest-directly.js | 3 - scripts/jest/jest-cli.js | 386 ----------------------- scripts/jest/jestSequencer.js | 19 -- scripts/jest/preprocessor.base.js | 143 +++++++++ scripts/jest/preprocessor.dev.js | 3 + scripts/jest/preprocessor.js | 142 --------- scripts/jest/preprocessor.prod.js | 3 + scripts/jest/setupEnvDevelopment.js | 3 + scripts/jest/setupEnvProduction.js | 3 + scripts/jest/setupEnvVariant.js | 3 + 19 files changed, 324 insertions(+), 854 deletions(-) create mode 100644 jest.config.js delete mode 100644 scripts/jest/config.base.js delete mode 100644 scripts/jest/config.build-devtools.js delete mode 100644 scripts/jest/config.build.js delete mode 100644 scripts/jest/config.source-persistent.js delete mode 100644 scripts/jest/config.source-www.js delete mode 100644 scripts/jest/config.source.js delete mode 100644 scripts/jest/dont-run-jest-directly.js delete mode 100644 scripts/jest/jest-cli.js delete mode 100644 scripts/jest/jestSequencer.js create mode 100644 scripts/jest/preprocessor.base.js create mode 100644 scripts/jest/preprocessor.dev.js delete mode 100644 scripts/jest/preprocessor.js create mode 100644 scripts/jest/preprocessor.prod.js create mode 100644 scripts/jest/setupEnvDevelopment.js create mode 100644 scripts/jest/setupEnvProduction.js create mode 100644 scripts/jest/setupEnvVariant.js diff --git a/.circleci/config.yml b/.circleci/config.yml index f2c743e4525c..b90a3f40c786 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -387,13 +387,10 @@ jobs: docker: *docker environment: *environment parallelism: *TEST_PARALLELISM - parameters: - args: - type: string steps: - checkout - setup_node_modules - - run: yarn test <> --ci + - run: yarn jest --ci --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL yarn_test_build: docker: *docker @@ -482,28 +479,6 @@ workflows: branches: ignore: - builds/facebook-www - matrix: - parameters: - args: - # Intentionally passing these as strings instead of creating a - # separate parameter per CLI argument, since it's easier to - # control/see which combinations we want to run. - - "-r=stable --env=development" - - "-r=stable --env=production" - - "-r=experimental --env=development" - - "-r=experimental --env=production" - - "-r=www-classic --env=development --variant=false" - - "-r=www-classic --env=production --variant=false" - - "-r=www-classic --env=development --variant=true" - - "-r=www-classic --env=production --variant=true" - - "-r=www-modern --env=development --variant=false" - - "-r=www-modern --env=production --variant=false" - - "-r=www-modern --env=development --variant=true" - - "-r=www-modern --env=production --variant=true" - - # TODO: Test more persistent configurations? - - '-r=stable --env=development --persistent' - - '-r=experimental --env=development --persistent' - yarn_build: filters: branches: diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000000..f8c6ee55c6ea --- /dev/null +++ b/jest.config.js @@ -0,0 +1,164 @@ +'use strict'; + +function createConfig({releaseChannel, env, variant, persistent}) { + let displayName = `${releaseChannel}-${env}`; + if (variant === true) { + displayName += '-variant'; + } + if (persistent) { + displayName += '-persistent'; + } + + const setupFiles = []; + if (variant) { + setupFiles.push(require.resolve('./scripts/jest/setupEnvVariant.js')); + } + switch (env) { + case 'development': + setupFiles.push(require.resolve('./scripts/jest/setupEnvDevelopment.js')); + break; + case 'production': + setupFiles.push(require.resolve('./scripts/jest/setupEnvProduction.js')); + break; + default: + throw new Error(`Unexpected env: ${env}`); + } + setupFiles.push(require.resolve('./scripts/jest/setupEnvironment.js')); + if (persistent) { + setupFiles.push(require.resolve('./scripts/jest/setupTests.persistent.js')); + } else { + switch (releaseChannel) { + case 'www-classic': + case 'www-modern': + setupFiles.push(require.resolve('./scripts/jest/setupTests.www.js')); + break; + case 'experimental': + case 'stable': + // no special setup files + break; + default: + throw new Error(`Unexpected release channel: ${releaseChannel}`); + } + } + setupFiles.push(require.resolve('./scripts/jest/setupHostConfigs.js')); + + const modulePathIgnorePatterns = [ + '/scripts/rollup/shims/', + '/scripts/bench/', + 'packages/react-devtools-extensions', + 'packages/react-devtools-shared', + ]; + + if (persistent) { + modulePathIgnorePatterns.push( + 'ReactIncrementalPerf', + 'ReactIncrementalUpdatesMinimalism', + 'ReactIncrementalTriangle', + 'ReactIncrementalReflection', + 'forwardRef' + ); + } + + return { + displayName, + setupFiles, + modulePathIgnorePatterns, + transform: { + '.*': + env === 'development' + ? require.resolve('./scripts/jest/preprocessor.dev.js') + : require.resolve('./scripts/jest/preprocessor.prod.js'), + }, + prettierPath: require.resolve('prettier-2'), + setupFilesAfterEnv: [require.resolve('./scripts/jest/setupTests.js')], + // Only include files directly in __tests__, not in nested folders. + testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', + moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], + rootDir: process.cwd(), + roots: ['/packages', '/scripts'], + fakeTimers: { + enableGlobally: true, + legacyFakeTimers: true, + }, + snapshotSerializers: [require.resolve('jest-snapshot-serializer-raw')], + testEnvironment: 'jsdom', + testRunner: 'jest-circus/runner', + }; +} + +module.exports = { + globalSetup: require.resolve('./scripts/jest/setupGlobal.js'), + collectCoverageFrom: ['packages/**/*.js'], + + projects: [ + createConfig({ + releaseChannel: 'stable', + env: 'development', + }), + createConfig({ + releaseChannel: 'stable', + env: 'production', + }), + createConfig({ + releaseChannel: 'stable', + env: 'development', + persistent: true, + }), + + createConfig({ + releaseChannel: 'experimental', + env: 'development', + }), + createConfig({ + releaseChannel: 'experimental', + env: 'production', + }), + createConfig({ + releaseChannel: 'experimental', + env: 'development', + persistent: true, + }), + + createConfig({ + releaseChannel: 'www-classic', + env: 'development', + variant: false, + }), + createConfig({ + releaseChannel: 'www-classic', + env: 'production', + variant: false, + }), + createConfig({ + releaseChannel: 'www-classic', + env: 'development', + variant: true, + }), + createConfig({ + releaseChannel: 'www-classic', + env: 'production', + variant: true, + }), + + createConfig({ + releaseChannel: 'www-modern', + env: 'development', + variant: false, + }), + createConfig({ + releaseChannel: 'www-modern', + env: 'production', + variant: false, + }), + createConfig({ + releaseChannel: 'www-modern', + env: 'development', + variant: true, + }), + createConfig({ + releaseChannel: 'www-modern', + env: 'production', + variant: true, + }), + ], +}; diff --git a/package.json b/package.json index 785bbfe63113..d42ef33b3963 100644 --- a/package.json +++ b/package.json @@ -103,9 +103,6 @@ "devEngines": { "node": "16.x || 18.x || 20.x || 21.x" }, - "jest": { - "testRegex": "/scripts/jest/dont-run-jest-directly\\.js$" - }, "scripts": { "build": "node ./scripts/rollup/build-all-release-channels.js", "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react-dom/index,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE", @@ -116,10 +113,7 @@ "lint-build": "node ./scripts/rollup/validate/index.js", "extract-errors": "node scripts/error-codes/extract-errors.js", "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js", - "test": "node ./scripts/jest/jest-cli.js", - "test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable", - "test-www": "node ./scripts/jest/jest-cli.js --release-channel=www-modern", - "test-classic": "node ./scripts/jest/jest-cli.js --release-channel=www-classic", + "test": "jest", "test-build-devtools": "node ./scripts/jest/jest-cli.js --build --project devtools --release-channel=experimental", "test-dom-fixture": "cd fixtures/dom && yarn && yarn predev && yarn test", "flow": "node ./scripts/tasks/flow.js", diff --git a/scripts/jest/config.base.js b/scripts/jest/config.base.js deleted file mode 100644 index a887e66b7dd3..000000000000 --- a/scripts/jest/config.base.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -module.exports = { - globalSetup: require.resolve('./setupGlobal.js'), - modulePathIgnorePatterns: [ - '/scripts/rollup/shims/', - '/scripts/bench/', - ], - transform: { - '.*': require.resolve('./preprocessor.js'), - }, - prettierPath: require.resolve('prettier-2'), - setupFiles: [require.resolve('./setupEnvironment.js')], - setupFilesAfterEnv: [require.resolve('./setupTests.js')], - // Only include files directly in __tests__, not in nested folders. - testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', - moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], - rootDir: process.cwd(), - roots: ['/packages', '/scripts'], - collectCoverageFrom: ['packages/**/*.js'], - fakeTimers: { - enableGlobally: true, - legacyFakeTimers: true, - }, - snapshotSerializers: [require.resolve('jest-snapshot-serializer-raw')], - - testSequencer: require.resolve('./jestSequencer'), - - testEnvironment: 'jsdom', - - testRunner: 'jest-circus/runner', -}; diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js deleted file mode 100644 index e6de2e4a3213..000000000000 --- a/scripts/jest/config.build-devtools.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -const {readdirSync, statSync} = require('fs'); -const {join} = require('path'); -const baseConfig = require('./config.base'); -const devtoolsRegressionConfig = require('./devtools/config.build-devtools-regression'); - -const NODE_MODULES_DIR = - process.env.RELEASE_CHANNEL === 'stable' ? 'oss-stable' : 'oss-experimental'; - -// Find all folders in packages/* with package.json -const packagesRoot = join(__dirname, '..', '..', 'packages'); -const packages = readdirSync(packagesRoot).filter(dir => { - if (dir.charAt(0) === '.') { - return false; - } - if (dir.includes('react-devtools')) { - return false; - } - if (dir === 'internal-test-utils') { - // This is an internal package used only for testing. It's OK to read - // from source. - // TODO: Maybe let's have some convention for this? - return false; - } - const packagePath = join(packagesRoot, dir, 'package.json'); - let stat; - try { - stat = statSync(packagePath); - } catch (err) { - return false; - } - return stat.isFile(); -}); - -// Create a module map to point React packages to the build output -const moduleNameMapper = {}; - -moduleNameMapper['react-devtools-feature-flags'] = - '/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default'; - -// Map packages to bundles -packages.forEach(name => { - // Root entry point - moduleNameMapper[`^${name}$`] = `/build/${NODE_MODULES_DIR}/${name}`; - // Named entry points - moduleNameMapper[ - `^${name}\/([^\/]+)$` - ] = `/build/${NODE_MODULES_DIR}/${name}/$1`; -}); - -// Allow tests to import shared code (e.g. feature flags, getStackByFiberInDevAndProd) -moduleNameMapper['^shared/([^/]+)$'] = '/packages/shared/$1'; -moduleNameMapper['^react-reconciler/([^/]+)$'] = - '/packages/react-reconciler/$1'; - -module.exports = Object.assign({}, baseConfig, { - // Redirect imports to the compiled bundles - moduleNameMapper: { - ...devtoolsRegressionConfig.moduleNameMapper, - ...moduleNameMapper, - }, - // Don't run bundle tests on -test.internal.* files - testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'], - // Exclude the build output from transforms - transformIgnorePatterns: [ - '/node_modules/', - '/build/', - '/__compiled__/', - '/__untransformed__/', - ], - testRegex: 'packages/react-devtools(-(.+))?/.+/__tests__/[^]+.test.js$', - snapshotSerializers: [ - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/dehydratedValueSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/hookSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/profilingSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/timelineDataSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/treeContextStateSerializer.js' - ), - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/__serializers__/numberToFixedSerializer.js' - ), - ], - setupFiles: [ - ...baseConfig.setupFiles, - ...devtoolsRegressionConfig.setupFiles, - require.resolve('./setupTests.build.js'), - require.resolve('./devtools/setupEnv.js'), - ], - setupFilesAfterEnv: [ - require.resolve( - '../../packages/react-devtools-shared/src/__tests__/setupTests.js' - ), - ], -}); diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js deleted file mode 100644 index 9b8d328a509e..000000000000 --- a/scripts/jest/config.build.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -const {readdirSync, statSync} = require('fs'); -const {join} = require('path'); -const baseConfig = require('./config.base'); - -process.env.IS_BUILD = true; - -const NODE_MODULES_DIR = - process.env.RELEASE_CHANNEL === 'stable' ? 'oss-stable' : 'oss-experimental'; - -// Find all folders in packages/* with package.json -const packagesRoot = join(__dirname, '..', '..', 'packages'); -const packages = readdirSync(packagesRoot).filter(dir => { - if (dir === 'internal-test-utils') { - // This is an internal package used only for testing. It's OK to read - // from source. - // TODO: Maybe let's have some convention for this? - return false; - } - if (dir.charAt(0) === '.') { - return false; - } - const packagePath = join(packagesRoot, dir, 'package.json'); - let stat; - try { - stat = statSync(packagePath); - } catch (err) { - return false; - } - return stat.isFile(); -}); - -// Create a module map to point React packages to the build output -const moduleNameMapper = {}; - -// Allow bundle tests to read (but not write!) default feature flags. -// This lets us determine whether we're running in different modes -// without making relevant tests internal-only. -moduleNameMapper[ - '^shared/ReactFeatureFlags' -] = `/packages/shared/forks/ReactFeatureFlags.readonly`; - -// Map packages to bundles -packages.forEach(name => { - // Root entry point - moduleNameMapper[`^${name}$`] = `/build/${NODE_MODULES_DIR}/${name}`; - // Named entry points - moduleNameMapper[ - `^${name}\/([^\/]+)$` - ] = `/build/${NODE_MODULES_DIR}/${name}/$1`; -}); - -moduleNameMapper[ - 'use-sync-external-store/shim/with-selector' -] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/with-selector`; -moduleNameMapper[ - 'use-sync-external-store/shim/index.native' -] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/index.native`; - -module.exports = Object.assign({}, baseConfig, { - // Redirect imports to the compiled bundles - moduleNameMapper, - modulePathIgnorePatterns: [ - ...baseConfig.modulePathIgnorePatterns, - 'packages/react-devtools-extensions', - 'packages/react-devtools-shared', - ], - // Don't run bundle tests on -test.internal.* files - testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'], - // Exclude the build output from transforms - transformIgnorePatterns: ['/node_modules/', '/build/'], - setupFiles: [ - ...baseConfig.setupFiles, - require.resolve('./setupTests.build.js'), - ], -}); diff --git a/scripts/jest/config.source-persistent.js b/scripts/jest/config.source-persistent.js deleted file mode 100644 index 80bec669b643..000000000000 --- a/scripts/jest/config.source-persistent.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const baseConfig = require('./config.base'); - -module.exports = Object.assign({}, baseConfig, { - modulePathIgnorePatterns: [ - ...baseConfig.modulePathIgnorePatterns, - 'packages/react-devtools-extensions', - 'packages/react-devtools-shared', - 'ReactIncrementalPerf', - 'ReactIncrementalUpdatesMinimalism', - 'ReactIncrementalTriangle', - 'ReactIncrementalReflection', - 'forwardRef', - ], - setupFiles: [ - ...baseConfig.setupFiles, - require.resolve('./setupTests.persistent.js'), - require.resolve('./setupHostConfigs.js'), - ], -}); diff --git a/scripts/jest/config.source-www.js b/scripts/jest/config.source-www.js deleted file mode 100644 index 31e8841ac363..000000000000 --- a/scripts/jest/config.source-www.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const baseConfig = require('./config.base'); - -module.exports = Object.assign({}, baseConfig, { - modulePathIgnorePatterns: [ - ...baseConfig.modulePathIgnorePatterns, - 'packages/react-devtools-extensions', - 'packages/react-devtools-shared', - ], - setupFiles: [ - ...baseConfig.setupFiles, - require.resolve('./setupTests.www.js'), - require.resolve('./setupHostConfigs.js'), - ], -}); diff --git a/scripts/jest/config.source.js b/scripts/jest/config.source.js deleted file mode 100644 index 710df337b5ab..000000000000 --- a/scripts/jest/config.source.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const baseConfig = require('./config.base'); - -module.exports = Object.assign({}, baseConfig, { - modulePathIgnorePatterns: [ - ...baseConfig.modulePathIgnorePatterns, - 'packages/react-devtools-extensions', - 'packages/react-devtools-shared', - ], - setupFiles: [ - ...baseConfig.setupFiles, - require.resolve('./setupHostConfigs.js'), - ], -}); diff --git a/scripts/jest/dont-run-jest-directly.js b/scripts/jest/dont-run-jest-directly.js deleted file mode 100644 index 672d07caec57..000000000000 --- a/scripts/jest/dont-run-jest-directly.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -throw new Error("Don't run `jest` directly. Run `yarn test` instead."); diff --git a/scripts/jest/jest-cli.js b/scripts/jest/jest-cli.js deleted file mode 100644 index 22098a190561..000000000000 --- a/scripts/jest/jest-cli.js +++ /dev/null @@ -1,386 +0,0 @@ -'use strict'; - -const {spawn} = require('child_process'); -const chalk = require('chalk'); -const yargs = require('yargs'); -const fs = require('fs'); -const path = require('path'); -const semver = require('semver'); - -const ossConfig = './scripts/jest/config.source.js'; -const wwwConfig = './scripts/jest/config.source-www.js'; -const devToolsConfig = './scripts/jest/config.build-devtools.js'; - -// TODO: These configs are separate but should be rolled into the configs above -// so that the CLI can provide them as options for any of the configs. -const persistentConfig = './scripts/jest/config.source-persistent.js'; -const buildConfig = './scripts/jest/config.build.js'; - -const argv = yargs - .parserConfiguration({ - // Important: This option tells yargs to move all other options not - // specified here into the `_` key. We use this to send all of the - // Jest options that we don't use through to Jest (like --watch). - 'unknown-options-as-args': true, - }) - .wrap(yargs.terminalWidth()) - .options({ - debug: { - alias: 'd', - describe: 'Run with node debugger attached.', - requiresArg: false, - type: 'boolean', - default: false, - }, - project: { - alias: 'p', - describe: 'Run the given project.', - requiresArg: true, - type: 'string', - default: 'default', - choices: ['default', 'devtools'], - }, - releaseChannel: { - alias: 'r', - describe: 'Run with the given release channel.', - requiresArg: true, - type: 'string', - default: 'experimental', - choices: ['experimental', 'stable', 'www-classic', 'www-modern'], - }, - env: { - alias: 'e', - describe: 'Run with the given node environment.', - requiresArg: true, - type: 'string', - choices: ['development', 'production'], - }, - prod: { - describe: 'Run with NODE_ENV=production.', - requiresArg: false, - type: 'boolean', - default: false, - }, - dev: { - describe: 'Run with NODE_ENV=development.', - requiresArg: false, - type: 'boolean', - default: false, - }, - variant: { - alias: 'v', - describe: 'Run with www variant set to true.', - requiresArg: false, - type: 'boolean', - }, - build: { - alias: 'b', - describe: 'Run tests on builds.', - requiresArg: false, - type: 'boolean', - default: false, - }, - persistent: { - alias: 'n', - describe: 'Run with persistence.', - requiresArg: false, - type: 'boolean', - default: false, - }, - ci: { - describe: 'Run tests in CI', - requiresArg: false, - type: 'boolean', - default: false, - }, - compactConsole: { - alias: 'c', - describe: 'Compact console output (hide file locations).', - requiresArg: false, - type: 'boolean', - default: false, - }, - reactVersion: { - describe: 'DevTools testing for specific version of React', - requiresArg: true, - type: 'string', - }, - sourceMaps: { - describe: - 'Enable inline source maps when transforming source files with Jest. Useful for debugging, but makes it slower.', - type: 'boolean', - default: false, - }, - }).argv; - -function logError(message) { - console.error(chalk.red(`\n${message}`)); -} -function isWWWConfig() { - return ( - (argv.releaseChannel === 'www-classic' || - argv.releaseChannel === 'www-modern') && - argv.project !== 'devtools' - ); -} - -function isOSSConfig() { - return ( - argv.releaseChannel === 'stable' || argv.releaseChannel === 'experimental' - ); -} - -function validateOptions() { - let success = true; - - if (argv.project === 'devtools') { - if (argv.prod) { - logError( - 'DevTool tests do not support --prod. Remove this option to continue.' - ); - success = false; - } - - if (argv.dev) { - logError( - 'DevTool tests do not support --dev. Remove this option to continue.' - ); - success = false; - } - - if (argv.env) { - logError( - 'DevTool tests do not support --env. Remove this option to continue.' - ); - success = false; - } - - if (argv.persistent) { - logError( - 'DevTool tests do not support --persistent. Remove this option to continue.' - ); - success = false; - } - - if (argv.variant) { - logError( - 'DevTool tests do not support --variant. Remove this option to continue.' - ); - success = false; - } - - if (!argv.build) { - logError('DevTool tests require --build.'); - success = false; - } - - if (argv.reactVersion && !semver.validRange(argv.reactVersion)) { - success = false; - logError('please specify a valid version range for --reactVersion'); - } - } else { - if (argv.compactConsole) { - logError('Only DevTool tests support compactConsole flag.'); - success = false; - } - if (argv.reactVersion) { - logError('Only DevTools tests supports the --reactVersion flag.'); - success = false; - } - } - - if (isWWWConfig()) { - if (argv.variant === undefined) { - // Turn internal experiments on by default - argv.variant = true; - } - } else { - if (argv.variant) { - logError( - 'Variant is only supported for the www release channels. Update these options to continue.' - ); - success = false; - } - } - - if (argv.build && argv.persistent) { - logError( - 'Persistence is not supported for build targets. Update these options to continue.' - ); - success = false; - } - - if (!isOSSConfig() && argv.persistent) { - logError( - 'Persistence only supported for oss release channels. Update these options to continue.' - ); - success = false; - } - - if (argv.build && isWWWConfig()) { - logError( - 'Build targets are only not supported for www release channels. Update these options to continue.' - ); - success = false; - } - - if (argv.env && argv.env !== 'production' && argv.prod) { - logError( - 'Build type does not match --prod. Update these options to continue.' - ); - success = false; - } - - if (argv.env && argv.env !== 'development' && argv.dev) { - logError( - 'Build type does not match --dev. Update these options to continue.' - ); - success = false; - } - - if (argv.prod && argv.dev) { - logError( - 'Cannot supply both --prod and --dev. Remove one of these options to continue.' - ); - success = false; - } - - if (argv.build) { - // TODO: We could build this if it hasn't been built yet. - const buildDir = path.resolve('./build'); - if (!fs.existsSync(buildDir)) { - logError( - 'Build directory does not exist, please run `yarn build` or remove the --build option.' - ); - success = false; - } else if (Date.now() - fs.statSync(buildDir).mtimeMs > 1000 * 60 * 15) { - logError( - 'Warning: Running a build test with a build directory older than 15 minutes.\nPlease remember to run `yarn build` when using --build.' - ); - } - } - - if (!success) { - console.log(''); // Extra newline. - process.exit(1); - } -} - -function getCommandArgs() { - // Add the correct Jest config. - const args = ['./scripts/jest/jest.js', '--config']; - if (argv.project === 'devtools') { - args.push(devToolsConfig); - } else if (argv.build) { - args.push(buildConfig); - } else if (argv.persistent) { - args.push(persistentConfig); - } else if (isWWWConfig()) { - args.push(wwwConfig); - } else if (isOSSConfig()) { - args.push(ossConfig); - } else { - // We should not get here. - logError('Unrecognized release channel'); - process.exit(1); - } - - // Set the debug options, if necessary. - if (argv.debug) { - args.unshift('--inspect-brk'); - args.push('--runInBand'); - - // Prevent console logs from being hidden until test completes. - args.push('--useStderr'); - } - - // CI Environments have limited workers. - if (argv.ci) { - args.push('--maxWorkers=2'); - } - - // Push the remaining args onto the command. - // This will send args like `--watch` to Jest. - args.push(...argv._); - - return args; -} - -function getEnvars() { - const envars = { - NODE_ENV: argv.env || 'development', - RELEASE_CHANNEL: argv.releaseChannel.match(/modern|experimental/) - ? 'experimental' - : 'stable', - - // Pass this flag through to the config environment - // so the base config can conditionally load the console setup file. - compactConsole: argv.compactConsole, - }; - - if (argv.prod) { - envars.NODE_ENV = 'production'; - } - - if (argv.dev) { - envars.NODE_ENV = 'development'; - } - - if (argv.variant) { - envars.VARIANT = true; - } - - if (argv.reactVersion) { - envars.REACT_VERSION = semver.coerce(argv.reactVersion); - } - - if (argv.sourceMaps) { - // This is off by default because it slows down the test runner, but it's - // super useful when running the debugger. - envars.JEST_ENABLE_SOURCE_MAPS = 'inline'; - } - - return envars; -} - -function main() { - validateOptions(); - - const args = getCommandArgs(); - const envars = getEnvars(); - const env = Object.entries(envars).map(([k, v]) => `${k}=${v}`); - - // Print the full command we're actually running. - const command = `$ ${env.join(' ')} node ${args.join(' ')}`; - console.log(chalk.dim(command)); - - // Print the release channel and project we're running for quick confirmation. - console.log( - chalk.blue( - `\nRunning tests for ${argv.project} (${argv.releaseChannel})...` - ) - ); - - // Print a message that the debugger is starting just - // for some extra feedback when running the debugger. - if (argv.debug) { - console.log(chalk.green('\nStarting debugger...')); - console.log(chalk.green('Open chrome://inspect and press "inspect"\n')); - } - - // Run Jest. - const jest = spawn('node', args, { - stdio: 'inherit', - env: {...envars, ...process.env}, - }); - - // Ensure we close our process when we get a failure case. - jest.on('close', code => { - // Forward the exit code from the Jest process. - if (code === 1) { - process.exit(1); - } - }); -} - -main(); diff --git a/scripts/jest/jestSequencer.js b/scripts/jest/jestSequencer.js deleted file mode 100644 index b21cbbfc0ccb..000000000000 --- a/scripts/jest/jestSequencer.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const Sequencer = require('@jest/test-sequencer').default; - -class CustomSequencer extends Sequencer { - sort(tests) { - if (process.env.CIRCLE_NODE_TOTAL) { - // In CI, parallelize tests across multiple tasks. - const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); - const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10); - tests = tests - .sort((a, b) => (a.path < b.path ? -1 : 1)) - .filter((_, i) => i % nodeTotal === nodeIndex); - } - return tests; - } -} - -module.exports = CustomSequencer; diff --git a/scripts/jest/preprocessor.base.js b/scripts/jest/preprocessor.base.js new file mode 100644 index 000000000000..f36b8beaa53a --- /dev/null +++ b/scripts/jest/preprocessor.base.js @@ -0,0 +1,143 @@ +'use strict'; + +function createPreprocessor(isDev) { + const path = require('path'); + + const babel = require('@babel/core'); + const coffee = require('coffee-script'); + const hermesParser = require('hermes-parser'); + + const tsPreprocessor = require('./typescript/preprocessor'); + const createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction'); + const {ReactVersion} = require('../../ReactVersions'); + const semver = require('semver'); + + const pathToBabel = path.join( + require.resolve('@babel/core'), + '../..', + 'package.json' + ); + const pathToBabelPluginReplaceConsoleCalls = require.resolve( + '../babel/transform-replace-console-calls' + ); + const pathToTransformInfiniteLoops = require.resolve( + '../babel/transform-prevent-infinite-loops' + ); + const pathToTransformTestGatePragma = require.resolve( + '../babel/transform-test-gate-pragma' + ); + const pathToTransformReactVersionPragma = require.resolve( + '../babel/transform-react-version-pragma' + ); + const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js'); + const pathToErrorCodes = require.resolve('../error-codes/codes.json'); + + const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; + + const babelOptions = { + plugins: [ + // For Node environment only. For builds, Rollup takes care of ESM. + require.resolve('@babel/plugin-transform-modules-commonjs'), + + pathToTransformInfiniteLoops, + pathToTransformTestGatePragma, + + // This optimization is important for extremely performance-sensitive (e.g. React source). + // It's okay to disable it for tests. + [ + require.resolve('@babel/plugin-transform-block-scoping'), + {throwIfClosureRequired: false}, + ], + ], + retainLines: true, + }; + + return { + process: function (src, filePath) { + if (filePath.match(/\.css$/)) { + // Don't try to parse CSS modules; they aren't needed for tests anyway. + return {code: ''}; + } + if (filePath.match(/\.coffee$/)) { + return {code: coffee.compile(src, {bare: true})}; + } + if (filePath.match(/\.ts$/) && !filePath.match(/\.d\.ts$/)) { + return {code: tsPreprocessor.compile(src, filePath)}; + } + if (filePath.match(/\.json$/)) { + return {code: src}; + } + if (!filePath.match(/\/third_party\//)) { + // for test files, we also apply the async-await transform, but we want to + // make sure we don't accidentally apply that transform to product code. + const isTestFile = !!filePath.match(/\/__tests__\//); + const isInDevToolsPackages = !!filePath.match( + /\/packages\/react-devtools.*\// + ); + const testOnlyPlugins = []; + const sourceOnlyPlugins = []; + if (isDev && !isInDevToolsPackages) { + sourceOnlyPlugins.push(pathToBabelPluginReplaceConsoleCalls); + } + const plugins = ( + isTestFile ? testOnlyPlugins : sourceOnlyPlugins + ).concat(babelOptions.plugins); + if (isTestFile && isInDevToolsPackages) { + plugins.push(pathToTransformReactVersionPragma); + } + + // This is only for React DevTools tests with React 16.x + // `react/jsx-dev-runtime` and `react/jsx-runtime` are included in the package starting from v17 + if (semver.gte(ReactVersionTestingAgainst, '17.0.0')) { + plugins.push([ + process.env.NODE_ENV === 'development' + ? require.resolve('@babel/plugin-transform-react-jsx-development') + : require.resolve('@babel/plugin-transform-react-jsx'), + // The "automatic" runtime corresponds to react/jsx-runtime. "classic" + // would be React.createElement. + {runtime: 'automatic'}, + ]); + } else { + plugins.push( + require.resolve('@babel/plugin-transform-react-jsx'), + require.resolve('@babel/plugin-transform-react-jsx-source') + ); + } + + let sourceAst = hermesParser.parse(src, {babel: true}); + return { + code: babel.transformFromAstSync( + sourceAst, + src, + Object.assign( + {filename: path.relative(process.cwd(), filePath)}, + babelOptions, + { + plugins, + sourceMaps: process.env.JEST_ENABLE_SOURCE_MAPS + ? process.env.JEST_ENABLE_SOURCE_MAPS + : false, + } + ) + ).code, + }; + } + return {code: src}; + }, + + getCacheKey: createCacheKeyFunction( + [ + __filename, + pathToBabel, + pathToBabelrc, + pathToTransformInfiniteLoops, + pathToTransformTestGatePragma, + pathToTransformReactVersionPragma, + pathToErrorCodes, + ], + [(process.env.REACT_VERSION != null).toString(), isDev.toString()] + ), + }; +} + +module.exports = createPreprocessor; diff --git a/scripts/jest/preprocessor.dev.js b/scripts/jest/preprocessor.dev.js new file mode 100644 index 000000000000..844fd1cfd617 --- /dev/null +++ b/scripts/jest/preprocessor.dev.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./preprocessor.base')(true); diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js deleted file mode 100644 index f04cd2c3cfc8..000000000000 --- a/scripts/jest/preprocessor.js +++ /dev/null @@ -1,142 +0,0 @@ -'use strict'; - -const path = require('path'); - -const babel = require('@babel/core'); -const coffee = require('coffee-script'); -const hermesParser = require('hermes-parser'); - -const tsPreprocessor = require('./typescript/preprocessor'); -const createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction'); -const {ReactVersion} = require('../../ReactVersions'); -const semver = require('semver'); - -const pathToBabel = path.join( - require.resolve('@babel/core'), - '../..', - 'package.json' -); -const pathToBabelPluginReplaceConsoleCalls = require.resolve( - '../babel/transform-replace-console-calls' -); -const pathToTransformInfiniteLoops = require.resolve( - '../babel/transform-prevent-infinite-loops' -); -const pathToTransformTestGatePragma = require.resolve( - '../babel/transform-test-gate-pragma' -); -const pathToTransformReactVersionPragma = require.resolve( - '../babel/transform-react-version-pragma' -); -const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js'); -const pathToErrorCodes = require.resolve('../error-codes/codes.json'); - -const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; - -const babelOptions = { - plugins: [ - // For Node environment only. For builds, Rollup takes care of ESM. - require.resolve('@babel/plugin-transform-modules-commonjs'), - - pathToTransformInfiniteLoops, - pathToTransformTestGatePragma, - - // This optimization is important for extremely performance-sensitive (e.g. React source). - // It's okay to disable it for tests. - [ - require.resolve('@babel/plugin-transform-block-scoping'), - {throwIfClosureRequired: false}, - ], - ], - retainLines: true, -}; - -module.exports = { - process: function (src, filePath) { - if (filePath.match(/\.css$/)) { - // Don't try to parse CSS modules; they aren't needed for tests anyway. - return {code: ''}; - } - if (filePath.match(/\.coffee$/)) { - return {code: coffee.compile(src, {bare: true})}; - } - if (filePath.match(/\.ts$/) && !filePath.match(/\.d\.ts$/)) { - return {code: tsPreprocessor.compile(src, filePath)}; - } - if (filePath.match(/\.json$/)) { - return {code: src}; - } - if (!filePath.match(/\/third_party\//)) { - // for test files, we also apply the async-await transform, but we want to - // make sure we don't accidentally apply that transform to product code. - const isTestFile = !!filePath.match(/\/__tests__\//); - const isInDevToolsPackages = !!filePath.match( - /\/packages\/react-devtools.*\// - ); - const testOnlyPlugins = []; - const sourceOnlyPlugins = []; - if (process.env.NODE_ENV === 'development' && !isInDevToolsPackages) { - sourceOnlyPlugins.push(pathToBabelPluginReplaceConsoleCalls); - } - const plugins = (isTestFile ? testOnlyPlugins : sourceOnlyPlugins).concat( - babelOptions.plugins - ); - if (isTestFile && isInDevToolsPackages) { - plugins.push(pathToTransformReactVersionPragma); - } - - // This is only for React DevTools tests with React 16.x - // `react/jsx-dev-runtime` and `react/jsx-runtime` are included in the package starting from v17 - if (semver.gte(ReactVersionTestingAgainst, '17.0.0')) { - plugins.push([ - process.env.NODE_ENV === 'development' - ? require.resolve('@babel/plugin-transform-react-jsx-development') - : require.resolve('@babel/plugin-transform-react-jsx'), - // The "automatic" runtime corresponds to react/jsx-runtime. "classic" - // would be React.createElement. - {runtime: 'automatic'}, - ]); - } else { - plugins.push( - require.resolve('@babel/plugin-transform-react-jsx'), - require.resolve('@babel/plugin-transform-react-jsx-source') - ); - } - - let sourceAst = hermesParser.parse(src, {babel: true}); - return { - code: babel.transformFromAstSync( - sourceAst, - src, - Object.assign( - {filename: path.relative(process.cwd(), filePath)}, - babelOptions, - { - plugins, - sourceMaps: process.env.JEST_ENABLE_SOURCE_MAPS - ? process.env.JEST_ENABLE_SOURCE_MAPS - : false, - } - ) - ).code, - }; - } - return {code: src}; - }, - - getCacheKey: createCacheKeyFunction( - [ - __filename, - pathToBabel, - pathToBabelrc, - pathToTransformInfiniteLoops, - pathToTransformTestGatePragma, - pathToTransformReactVersionPragma, - pathToErrorCodes, - ], - [ - (process.env.REACT_VERSION != null).toString(), - (process.env.NODE_ENV === 'development').toString(), - ] - ), -}; diff --git a/scripts/jest/preprocessor.prod.js b/scripts/jest/preprocessor.prod.js new file mode 100644 index 000000000000..d00d05010e7f --- /dev/null +++ b/scripts/jest/preprocessor.prod.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./preprocessor.base')(false); diff --git a/scripts/jest/setupEnvDevelopment.js b/scripts/jest/setupEnvDevelopment.js new file mode 100644 index 000000000000..d4c28db32a39 --- /dev/null +++ b/scripts/jest/setupEnvDevelopment.js @@ -0,0 +1,3 @@ +'use strict'; + +process.env.NODE_ENV = 'development'; diff --git a/scripts/jest/setupEnvProduction.js b/scripts/jest/setupEnvProduction.js new file mode 100644 index 000000000000..10e22dc7fba8 --- /dev/null +++ b/scripts/jest/setupEnvProduction.js @@ -0,0 +1,3 @@ +'use strict'; + +process.env.NODE_ENV = 'production'; diff --git a/scripts/jest/setupEnvVariant.js b/scripts/jest/setupEnvVariant.js new file mode 100644 index 000000000000..ae70dbd66c0e --- /dev/null +++ b/scripts/jest/setupEnvVariant.js @@ -0,0 +1,3 @@ +'use strict'; + +process.env.VARIANT = 'true';