Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions e2e/next/src/next-vitest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
cleanupProject,
createFile,
newProject,
runCLIAsync,
runCLI,
uniq,
} from '@nx/e2e-utils';

describe('Next.js Vitest', () => {
beforeAll(() => {
newProject({
packages: [
'@nx/next',
'@nx/js',
'@nx/vitest',
'@nx/eslint',
'@nx/playwright',
],
preset: 'ts',
});
});

afterAll(() => cleanupProject());

it('should test an app generated with --unitTestRunner=vitest', async () => {
const appName = uniq('app');

runCLI(
`generate @nx/next:app ${appName} --style=css --unitTestRunner=vitest --linter=eslint --no-interactive`
);

createFile(
`${appName}/src/app/page.spec.tsx`,
`
import { render } from '@testing-library/react';
import Page from '@/app/page';

describe('Page', () => {
it('should render successfully', () => {
const { baseElement } = render(<Page />);
expect(baseElement).toBeTruthy();
});
});
`
);

const testResult = await runCLIAsync(`test ${appName}`);
expect(testResult.combinedOutput).toContain('Successfully ran target test');
expect(testResult.combinedOutput).toContain('page.spec');
expect(testResult.combinedOutput).toContain('index.spec');
}, 300_000);

it('should test a lib generated with --unitTestRunner=vitest', async () => {
const libName = uniq('lib');

runCLI(
`generate @nx/next:lib packages/${libName} --style=css --unitTestRunner=vitest --linter=eslint --no-interactive`
);

const testResult = await runCLIAsync(`test ${libName}`);
expect(testResult.combinedOutput).toContain('Successfully ran target test');
}, 300_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ describe('determineUnitTestRunner', () => {
workspaceType: 'integrated',
},
expected: 'jest',
excluded: 'vitest',
excluded: '',
},
};

Expand Down
18 changes: 7 additions & 11 deletions packages/create-nx-workspace/bin/create-nx-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ interface NodeArguments extends BaseArguments {
appName: string;
framework: 'none' | 'express' | 'fastify' | 'koa' | 'nest';
docker: boolean;
unitTestRunner: 'none' | 'jest';
unitTestRunner: 'none' | 'jest' | 'vitest';
}

interface WebArguments extends BaseArguments {
Expand Down Expand Up @@ -1341,12 +1341,10 @@ async function determineReactOptions(
preferVitest: bundler === 'vite',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
} else if (
preset === Preset.NextJs ||
preset === Preset.NextJsStandalone ||
preset === Preset.ReactNative ||
preset === Preset.Expo
) {
} else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) {
unitTestRunner = await determineUnitTestRunner(parsedArgs);
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
} else if (preset === Preset.ReactNative || preset === Preset.Expo) {
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
Expand Down Expand Up @@ -1653,7 +1651,7 @@ async function determineNodeOptions(
let framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none';
let docker: boolean;
let linter: undefined | Linter;
let unitTestRunner: undefined | 'none' | 'jest' = undefined;
let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined;
const workspaces = parsedArgs.workspaces;

if (parsedArgs.preset) {
Expand Down Expand Up @@ -1711,9 +1709,7 @@ async function determineNodeOptions(
// without asking.
const formatter = await determineFormatterOptions(parsedArgs);
linter = await determineLinterOptions(parsedArgs);
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
unitTestRunner = await determineUnitTestRunner(parsedArgs);

return {
preset,
Expand Down
1 change: 1 addition & 0 deletions packages/next/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default [
'@nx/cypress',
'@nx/jest',
'@nx/playwright',
'@nx/vitest',
'typescript',
'react',
'webpack',
Expand Down
1 change: 1 addition & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"devDependencies": {
"@nx/cypress": "workspace:*",
"@nx/playwright": "workspace:*",
"@nx/vitest": "workspace:*",
"@nx/webpack": "workspace:*",
"nx": "workspace:*"
},
Expand Down
165 changes: 165 additions & 0 deletions packages/next/src/generators/application/application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,43 @@ describe('app', () => {
});
});

it('should emit the @/* alias in the vitest config', async () => {
await applicationGenerator(tree, {
linter: 'eslint',
directory: 'apps/myapp',
unitTestRunner: 'vitest',
style: 'css',
useTsSolution: true,
skipFormat: true,
});

const vitestConfig = tree.read('apps/myapp/vitest.config.mts', 'utf-8');
expect(vitestConfig).toContain(
`'@': join(import.meta.dirname, './src'),`
);
const tsconfigSpec = readJson(tree, 'apps/myapp/tsconfig.spec.json');
expect(tsconfigSpec.compilerOptions.paths).toEqual({
'@/*': ['./src/*'],
});
});

it('should emit the @/* alias for --no-src apps', async () => {
await applicationGenerator(tree, {
linter: 'eslint',
directory: 'apps/myapp',
unitTestRunner: 'vitest',
style: 'css',
src: false,
useTsSolution: true,
skipFormat: true,
});

const vitestConfig = tree.read('apps/myapp/vitest.config.mts', 'utf-8');
expect(vitestConfig).toContain(`'@': join(import.meta.dirname, '.'),`);
const tsconfigSpec = readJson(tree, 'apps/myapp/tsconfig.spec.json');
expect(tsconfigSpec.compilerOptions.paths).toEqual({ '@/*': ['./*'] });
});

it('should add project references when using TS solution', async () => {
await applicationGenerator(tree, {
linter: 'eslint',
Expand Down Expand Up @@ -1098,6 +1135,134 @@ describe('app', () => {
});
});

describe('--unit-test-runner vitest', () => {
it('should generate a vitest config and no jest config', async () => {
const name = 'myapp';
await applicationGenerator(tree, {
directory: name,
style: 'css',
unitTestRunner: 'vitest',
skipFormat: true,
});

expect(tree.exists(`${name}/jest.config.cts`)).toBeFalsy();
expect(tree.exists(`${name}/specs/index.spec.tsx`)).toBeTruthy();
expect(
readJson(tree, 'package.json').devDependencies['@nx/vite']
).toBeUndefined();
expect(
readJson(tree, 'package.json').devDependencies['vitest']
).toBeDefined();
expect(tree.read(`${name}/vitest.config.mts`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig(() => ({
root: import.meta.dirname,
cacheDir: '../node_modules/.vite/myapp',
plugins: [react()],
test: {
name: 'myapp',
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,app,pages,specs}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/myapp',
provider: 'v8' as const,
}
},
}));
"
`);
});

it('should add test types to tsconfig.json so specs outside src type-check', async () => {
const name = 'myapp';
await applicationGenerator(tree, {
directory: name,
style: 'css',
unitTestRunner: 'vitest',
skipFormat: true,
});

const tsconfig = readJson(tree, `${name}/tsconfig.json`);
expect(tsconfig.compilerOptions.types).toEqual(
expect.arrayContaining(['vitest/globals', 'node'])
);
const tsconfigSpec = readJson(tree, `${name}/tsconfig.spec.json`);
expect(tsconfigSpec.compilerOptions.types).toEqual(
expect.arrayContaining(['vitest/globals', 'vite/client', 'node'])
);
});
});

describe('--unit-test-runner vitest workspace aliases', () => {
it('should mirror root tsconfig paths as aliases in non-ts-solution workspaces', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
json.compilerOptions ??= {};
json.compilerOptions.paths = {
'@proj/mylib': ['libs/mylib/src/index.ts'],
'@proj/other/*': ['libs/other/src/*'],
};
return json;
});

await applicationGenerator(tree, {
directory: 'myapp',
style: 'css',
unitTestRunner: 'vitest',
skipFormat: true,
});

const vitestConfig = tree.read('myapp/vitest.config.mts', 'utf-8');
expect(vitestConfig).toContain(
`'@proj/mylib': join(import.meta.dirname, '../libs/mylib/src/index.ts'),`
);
expect(vitestConfig).toContain(
`'@proj/other': join(import.meta.dirname, '../libs/other/src'),`
);
});

it('should resolve root tsconfig paths against its baseUrl', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
json.compilerOptions ??= {};
json.compilerOptions.baseUrl = 'src';
json.compilerOptions.paths = {
'shared/*': ['shared/*'],
};
return json;
});

await applicationGenerator(tree, {
directory: 'myapp',
style: 'css',
unitTestRunner: 'vitest',
skipFormat: true,
});

const vitestConfig = tree.read('myapp/vitest.config.mts', 'utf-8');
expect(vitestConfig).toContain(
`'shared': join(import.meta.dirname, '../src/shared'),`
);
});

it('should enable allowJs in tsconfig.spec.json for --js apps', async () => {
await applicationGenerator(tree, {
directory: 'myapp',
style: 'css',
unitTestRunner: 'vitest',
js: true,
skipFormat: true,
});

const tsconfigSpec = readJson(tree, 'myapp/tsconfig.spec.json');
expect(tsconfigSpec.compilerOptions.allowJs).toBe(true);
});
});

describe('--unit-test-runner jest', () => {
it('should use next/jest.js for Jest configuration', async () => {
const name = 'myapp';
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
import { addE2e } from './lib/add-e2e';
import { addJest } from './lib/add-jest';
import { addVitest } from './lib/add-vitest';
import { addProject } from './lib/add-project';
import { createApplicationFiles } from './lib/create-application-files';
import { setDefaults } from './lib/set-defaults';
Expand Down Expand Up @@ -93,6 +94,9 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
const jestTask = await addJest(host, options);
tasks.push(jestTask);

const vitestTask = await addVitest(host, options);
tasks.push(vitestTask);

const styledTask = addStyleDependencies(host, {
style: options.style,
swc: !host.exists(joinPathFragments(options.appProjectRoot, '.babelrc')),
Expand Down
Loading
Loading