feat(react): make vitest the default test runner since it supports ESM and different moduleResolution better (#28153)

This PR updates the default test runner for React/Web apps to be
`vitest`. It aligned better with our emphasis on modern tooling, and the
lack of ESM and proper TS support (using `module` other than `commonjs`)
in Jest makes it hard to use in some workspaces.

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jack Hsu 2024-09-30 09:00:01 -04:00 committed by GitHub
parent ac53df6c67
commit 85877e3e18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 64 additions and 31 deletions

View File

@ -70,8 +70,9 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests." "description": "Test runner to use for unit tests.",
"default": "vitest"
}, },
"tags": { "tags": {
"type": "string", "type": "string",

View File

@ -105,9 +105,9 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "default": "vitest"
}, },
"inSourceTests": { "inSourceTests": {
"type": "boolean", "type": "boolean",

View File

@ -80,7 +80,8 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?" "x-prompt": "What unit test runner should be used?"
}, },

View File

@ -49,7 +49,7 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test Runner to use for Unit Tests", "description": "Test Runner to use for Unit Tests",
"x-prompt": "What test runner should be used?", "x-prompt": "What test runner should be used?",
"default": "vitest" "default": "vitest"

View File

@ -55,6 +55,12 @@
"runtimeTsconfigFileName": { "runtimeTsconfigFileName": {
"type": "string", "type": "string",
"description": "The name of the project's tsconfig file that includes the runtime source files. If not provided, it will default to `tsconfig.lib.json` for libraries and `tsconfig.app.json` for applications." "description": "The name of the project's tsconfig file that includes the runtime source files. If not provided, it will default to `tsconfig.lib.json` for libraries and `tsconfig.app.json` for applications."
},
"compiler": {
"type": "string",
"enum": ["babel", "swc"],
"default": "babel",
"description": "The compiler to use"
} }
}, },
"required": ["project"], "required": ["project"],

View File

@ -74,7 +74,8 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests. Default value is 'jest' when using 'webpack' or 'none' as the bundler and 'vitest' when using 'vite' as the bundler" "description": "Test runner to use for unit tests. Default value is 'jest' when using 'webpack' or 'none' as the bundler and 'vitest' when using 'vite' as the bundler"
}, },
"inSourceTests": { "inSourceTests": {

View File

@ -685,7 +685,9 @@ describe('Linter', () => {
const myapp = uniq('myapp'); const myapp = uniq('myapp');
const mylib = uniq('mylib'); const mylib = uniq('mylib');
runCLI(`generate @nx/react:app ${myapp} --rootProject=true`); runCLI(
`generate @nx/react:app ${myapp} --unitTestRunner=jest --rootProject=true`
);
verifySuccessfulStandaloneSetup(myapp); verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json'); let appEslint = readJson('.eslintrc.json');

View File

@ -331,8 +331,6 @@ describe('Nx Running Tests', () => {
.filter((r) => r); .filter((r) => r);
withBail = withBail.slice(withBail.indexOf('Failed tasks:')); withBail = withBail.slice(withBail.indexOf('Failed tasks:'));
expect(withBail).toHaveLength(2);
if (withBail[1] === `- ${myapp1}:error`) { if (withBail[1] === `- ${myapp1}:error`) {
expect(withBail).not.toContain(`- ${myapp2}:error`); expect(withBail).not.toContain(`- ${myapp2}:error`);
} else { } else {

View File

@ -107,7 +107,7 @@ describe('React Applications', () => {
const redSvg = `<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" viewBox="0 0 30 30"><rect x="10" y="10" width="10" height="10" fill="red"/></svg>`; const redSvg = `<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" viewBox="0 0 30 30"><rect x="10" y="10" width="10" height="10" fill="red"/></svg>`;
runCLI( runCLI(
`generate @nx/react:app ${appName} --directory=apps/${appName} --style=css --bundler=webpack --no-interactive --skipFormat` `generate @nx/react:app ${appName} --directory=apps/${appName} --style=css --bundler=webpack --unit-test-runner=jest --no-interactive --skipFormat`
); );
runCLI( runCLI(
`generate @nx/react:lib ${libName} --directory=libs${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat` `generate @nx/react:lib ${libName} --directory=libs${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat`
@ -234,7 +234,7 @@ describe('React Applications', () => {
); );
const appTestResults = await runCLIAsync(`test ${appName}`); const appTestResults = await runCLIAsync(`test ${appName}`);
expect(appTestResults.combinedOutput).toContain( expect(appTestResults.combinedOutput).toContain(
'Test Suites: 2 passed, 2 total' `Successfully ran target test for project ${appName}`
); );
lintResults = runCLI(`lint ${libName}`); lintResults = runCLI(`lint ${libName}`);
@ -243,7 +243,7 @@ describe('React Applications', () => {
); );
const libTestResults = await runCLIAsync(`test ${libName}`); const libTestResults = await runCLIAsync(`test ${libName}`);
expect(libTestResults.combinedOutput).toContain( expect(libTestResults.combinedOutput).toContain(
'Test Suites: 2 passed, 2 total' `Successfully ran target test for project ${libName}`
); );
}, 250_000); }, 250_000);
@ -414,7 +414,7 @@ describe('React Applications', () => {
const plainJsLib = uniq('jslib'); const plainJsLib = uniq('jslib');
runCLI( runCLI(
`generate @nx/react:app ${appName} --directory=apps/${appName} --bundler=webpack --no-interactive --js --skipFormat` `generate @nx/react:app ${appName} --directory=apps/${appName} --bundler=webpack --unit-test-runner=jest --no-interactive --js --skipFormat`
); );
runCLI( runCLI(
`generate @nx/react:lib ${libName} --directory=libs/${libName} --no-interactive --js --unit-test-runner=none --skipFormat` `generate @nx/react:lib ${libName} --directory=libs/${libName} --no-interactive --js --unit-test-runner=none --skipFormat`

View File

@ -30,7 +30,9 @@ describe('Web Components Applications with bundler set as vite', () => {
const testResults = await runCLIAsync(`test ${appName}`); const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.combinedOutput).toContain(`PASS ${appName}`); expect(testResults.combinedOutput).toContain(
`Successfully ran target test for project ${appName}`
);
const lintE2eResults = runCLI(`lint ${appName}-e2e`); const lintE2eResults = runCLI(`lint ${appName}-e2e`);

View File

@ -35,7 +35,7 @@ describe('Web Components Applications', () => {
const testResults = await runCLIAsync(`test ${appName}`); const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.combinedOutput).toContain( expect(testResults.combinedOutput).toContain(
'Test Suites: 1 passed, 1 total' `Successfully ran target test for project ${appName}`
); );
const lintE2eResults = runCLI(`lint ${appName}-e2e`); const lintE2eResults = runCLI(`lint ${appName}-e2e`);

View File

@ -85,6 +85,13 @@ exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert wi
} }
} }
}, },
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../coverage/app3224373"
}
},
"lint": { "lint": {
"executor": "@nx/eslint:lint" "executor": "@nx/eslint:lint"
}, },
@ -95,13 +102,6 @@ exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert wi
"buildTarget": "app3224373:build", "buildTarget": "app3224373:build",
"spa": true "spa": true
} }
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "app3224373/jest.config.ts"
}
} }
} }
} }

View File

@ -168,6 +168,7 @@ export async function libraryGeneratorInternal(
skipFormat: true, skipFormat: true,
testEnvironment: options.testEnvironment, testEnvironment: options.testEnvironment,
runtimeTsconfigFileName: 'tsconfig.lib.json', runtimeTsconfigFileName: 'tsconfig.lib.json',
compiler: options.compiler === 'swc' ? 'swc' : 'babel',
}); });
tasks.push(vitestTask); tasks.push(vitestTask);
createOrEditViteConfig( createOrEditViteConfig(

View File

@ -73,8 +73,9 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests." "description": "Test runner to use for unit tests.",
"default": "vitest"
}, },
"tags": { "tags": {
"type": "string", "type": "string",

View File

@ -111,9 +111,9 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "default": "vitest"
}, },
"inSourceTests": { "inSourceTests": {
"type": "boolean", "type": "boolean",

View File

@ -158,6 +158,7 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
skipFormat: true, skipFormat: true,
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
compiler: options.compiler,
}); });
tasks.push(vitestTask); tasks.push(vitestTask);
createOrEditViteConfig( createOrEditViteConfig(

View File

@ -83,7 +83,8 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?" "x-prompt": "What unit test runner should be used?"
}, },

View File

@ -49,7 +49,7 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test Runner to use for Unit Tests", "description": "Test Runner to use for Unit Tests",
"x-prompt": "What test runner should be used?", "x-prompt": "What test runner should be used?",
"default": "vitest" "default": "vitest"

View File

@ -163,6 +163,7 @@ export async function viteConfigurationGeneratorInternal(
testTarget: 'test', testTarget: 'test',
skipFormat: true, skipFormat: true,
addPlugin: schema.addPlugin, addPlugin: schema.addPlugin,
compiler: schema.compiler,
}); });
tasks.push(vitestTask); tasks.push(vitestTask);
} }

View File

@ -9,4 +9,5 @@ export interface VitestGeneratorSchema {
testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string; testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string;
addPlugin?: boolean; addPlugin?: boolean;
runtimeTsconfigFileName?: string; runtimeTsconfigFileName?: string;
compiler?: 'babel' | 'swc'; // default: babel
} }

View File

@ -54,6 +54,12 @@
"runtimeTsconfigFileName": { "runtimeTsconfigFileName": {
"type": "string", "type": "string",
"description": "The name of the project's tsconfig file that includes the runtime source files. If not provided, it will default to `tsconfig.lib.json` for libraries and `tsconfig.app.json` for applications." "description": "The name of the project's tsconfig file that includes the runtime source files. If not provided, it will default to `tsconfig.lib.json` for libraries and `tsconfig.app.json` for applications."
},
"compiler": {
"type": "string",
"enum": ["babel", "swc"],
"default": "babel",
"description": "The compiler to use"
} }
}, },
"required": ["project"] "required": ["project"]

View File

@ -46,6 +46,10 @@ export async function vitestGeneratorInternal(
schema: VitestGeneratorSchema, schema: VitestGeneratorSchema,
hasPlugin = false hasPlugin = false
) { ) {
// Setting default to jsdom since it is the most common use case (React, Web).
// The @nx/js:lib generator specifically sets this to node to be more generic.
schema.testEnvironment ??= 'jsdom';
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const { root, projectType } = readProjectConfiguration(tree, schema.project); const { root, projectType } = readProjectConfiguration(tree, schema.project);
@ -85,7 +89,11 @@ export async function vitestGeneratorInternal(
"'react-dom'", "'react-dom'",
"'react/jsx-runtime'", "'react/jsx-runtime'",
], ],
imports: [`import react from '@vitejs/plugin-react'`], imports: [
schema.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc'`
: `import react from '@vitejs/plugin-react'`,
],
plugins: ['react()'], plugins: ['react()'],
coverageProvider: schema.coverageProvider, coverageProvider: schema.coverageProvider,
}, },

View File

@ -325,6 +325,7 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
inSourceTests: options.inSourceTests, inSourceTests: options.inSourceTests,
skipFormat: true, skipFormat: true,
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
compiler: options.compiler,
}); });
tasks.push(vitestTask); tasks.push(vitestTask);
createOrEditViteConfig( createOrEditViteConfig(

View File

@ -77,7 +77,8 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["vitest", "jest", "none"],
"default": "vitest",
"description": "Test runner to use for unit tests. Default value is 'jest' when using 'webpack' or 'none' as the bundler and 'vitest' when using 'vite' as the bundler" "description": "Test runner to use for unit tests. Default value is 'jest' when using 'webpack' or 'none' as the bundler and 'vitest' when using 'vite' as the bundler"
}, },
"inSourceTests": { "inSourceTests": {