Skip to content

Commit 1a235d7

Browse files
authored
fix(react): react-router should work with jest out of the box (#30487)
Jest should be compatible with react-router out of the box. <!-- 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 --> Currently, there are two issues when using `jest` with react-router out of the box 1. Test files are not included from `tsconfig` 2. While running the test `jsdom` is missing Node's `TextEncoder` and `TextDecoder` so compilation fails. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Running a test should work without issues when you create a react-router app with Jest. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #30387
1 parent b371512 commit 1a235d7

File tree

7 files changed

+202
-53
lines changed

7 files changed

+202
-53
lines changed

e2e/react/src/react-router.test.ts

Lines changed: 103 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,117 @@ import {
99
} from '@nx/e2e/utils';
1010

1111
describe('React Router Applications', () => {
12-
beforeAll(() => {
13-
newProject({ packages: ['@nx/react'] });
14-
ensureCypressInstallation();
15-
});
12+
describe('TS paths', () => {
13+
const appName = uniq('app');
14+
beforeAll(() => {
15+
newProject({ packages: ['@nx/react'] });
16+
ensureCypressInstallation();
17+
runCLI(
18+
`generate @nx/react:app ${appName} --use-react-router --routing --linter=eslint --unit-test-runner=vitest --no-interactive`
19+
);
20+
});
1621

17-
afterAll(() => cleanupProject());
22+
afterAll(() => cleanupProject());
1823

19-
it('should generate a react-router application', async () => {
20-
const appName = uniq('app');
21-
runCLI(
22-
`generate @nx/react:app ${appName} --use-react-router --routing --no-interactive`
23-
);
24-
25-
const packageJson = JSON.parse(readFile('package.json'));
26-
expect(packageJson.dependencies['react-router']).toBeDefined();
27-
expect(packageJson.dependencies['@react-router/node']).toBeDefined();
28-
expect(packageJson.dependencies['@react-router/serve']).toBeDefined();
29-
expect(packageJson.dependencies['isbot']).toBeDefined();
30-
31-
checkFilesExist(`${appName}/app/app.tsx`);
32-
checkFilesExist(`${appName}/app/entry.client.tsx`);
33-
checkFilesExist(`${appName}/app/entry.server.tsx`);
34-
checkFilesExist(`${appName}/app/routes.tsx`);
35-
checkFilesExist(`${appName}/react-router.config.ts`);
36-
checkFilesExist(`${appName}/vite.config.ts`);
37-
});
24+
it('should generate a react-router application', async () => {
25+
const packageJson = JSON.parse(readFile('package.json'));
26+
expect(packageJson.dependencies['react-router']).toBeDefined();
27+
expect(packageJson.dependencies['@react-router/node']).toBeDefined();
28+
expect(packageJson.dependencies['@react-router/serve']).toBeDefined();
29+
expect(packageJson.dependencies['isbot']).toBeDefined();
3830

39-
it('should be able to build a react-router application', async () => {
40-
const appName = uniq('app');
41-
runCLI(
42-
`generate @nx/react:app ${appName} --use-react-router --routing --no-interactive`
43-
);
31+
checkFilesExist(`${appName}/app/app.tsx`);
32+
checkFilesExist(`${appName}/app/entry.client.tsx`);
33+
checkFilesExist(`${appName}/app/entry.server.tsx`);
34+
checkFilesExist(`${appName}/app/routes.tsx`);
35+
checkFilesExist(`${appName}/react-router.config.ts`);
36+
checkFilesExist(`${appName}/vite.config.ts`);
37+
});
4438

45-
const buildResult = runCLI(`build ${appName}`);
46-
expect(buildResult).toContain('Successfully ran target build');
47-
});
39+
it('should be able to build a react-router application', async () => {
40+
const buildResult = runCLI(`build ${appName}`);
41+
expect(buildResult).toContain('Successfully ran target build');
42+
});
4843

49-
it('should be able to lint a react-router application', async () => {
50-
const appName = uniq('app');
51-
runCLI(
52-
`generate @nx/react:app ${appName} --use-react-router --routing --linter=eslint --no-interactive`
53-
);
44+
it('should be able to lint a react-router application', async () => {
45+
const lintResult = runCLI(`lint ${appName}`);
46+
expect(lintResult).toContain('Successfully ran target lint');
47+
});
5448

55-
const buildResult = runCLI(`lint ${appName}`);
56-
expect(buildResult).toContain('Successfully ran target lint');
57-
});
49+
it('should be able to test and typecheck a react-router application', async () => {
50+
const typeCheckResult = runCLI(`typecheck ${appName}`);
51+
expect(typeCheckResult).toContain('Successfully ran target typecheck');
52+
});
53+
54+
it('should be able to test and typecheck a react-router application with jest', async () => {
55+
const jestApp = uniq('jestApp');
56+
runCLI(
57+
`generate @nx/react:app ${jestApp} --use-react-router --routing --unit-test-runner=jest --no-interactive`
58+
);
59+
60+
const testResult = runCLI(`test ${jestApp}`);
61+
expect(testResult).toContain('Successfully ran target test');
5862

59-
it('should be able to test a react-router application', async () => {
63+
const typeCheckResult = runCLI(`typecheck ${jestApp}`);
64+
expect(typeCheckResult).toContain('Successfully ran target typecheck');
65+
});
66+
});
67+
describe('TS Solution', () => {
6068
const appName = uniq('app');
61-
runCLI(
62-
`generate @nx/react:app ${appName} --use-react-router --routing --unit-test-runner=vitest --no-interactive`
63-
);
69+
beforeAll(() => {
70+
newProject({ preset: 'ts', packages: ['@nx/react'] });
71+
ensureCypressInstallation();
72+
runCLI(
73+
`generate @nx/react:app ${appName} --use-react-router --routing --linter=eslint --unit-test-runner=vitest --no-interactive`
74+
);
75+
});
76+
77+
afterAll(() => cleanupProject());
78+
79+
it('should generate a react-router application', async () => {
80+
const packageJson = JSON.parse(readFile('package.json'));
81+
expect(packageJson.dependencies['react-router']).toBeDefined();
82+
expect(packageJson.dependencies['@react-router/node']).toBeDefined();
83+
expect(packageJson.dependencies['@react-router/serve']).toBeDefined();
84+
expect(packageJson.dependencies['isbot']).toBeDefined();
85+
86+
checkFilesExist(`${appName}/app/app.tsx`);
87+
checkFilesExist(`${appName}/app/entry.client.tsx`);
88+
checkFilesExist(`${appName}/app/entry.server.tsx`);
89+
checkFilesExist(`${appName}/app/routes.tsx`);
90+
checkFilesExist(`${appName}/react-router.config.ts`);
91+
checkFilesExist(`${appName}/vite.config.ts`);
92+
});
93+
94+
it('should be able to build a react-router application', async () => {
95+
const buildResult = runCLI(`build ${appName}`);
96+
expect(buildResult).toContain('Successfully ran target build');
97+
});
98+
99+
it('should be able to lint a react-router application', async () => {
100+
const lintResult = runCLI(`lint ${appName}`);
101+
expect(lintResult).toContain('Successfully ran target lint');
102+
});
103+
104+
it('should be able to test and typecheck a react-router application', async () => {
105+
const testResult = runCLI(`test ${appName}`);
106+
expect(testResult).toContain('Successfully ran target test');
107+
108+
const typeCheckResult = runCLI(`typecheck ${appName}`);
109+
expect(typeCheckResult).toContain('Successfully ran target typecheck');
110+
});
111+
112+
it('should be able to test and typecheck a react-router application with jest', async () => {
113+
const jestApp = uniq('jestApp');
114+
runCLI(
115+
`generate @nx/react:app ${jestApp} --use-react-router --routing --unit-test-runner=jest --no-interactive`
116+
);
117+
118+
const testResult = runCLI(`test ${jestApp}`);
119+
expect(testResult).toContain('Successfully ran target test');
64120

65-
const buildResult = runCLI(`test ${appName}`);
66-
expect(buildResult).toContain('Successfully ran target test');
121+
const typeCheckResult = runCLI(`typecheck ${jestApp}`);
122+
expect(typeCheckResult).toContain('Successfully ran target typecheck');
123+
});
67124
});
68125
});
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
<% if(setupFile === 'react-native') { %>import '@testing-library/jest-native/extend-expect';<% } %>
1+
<% if(setupFile === 'react-native') { %>import '@testing-library/jest-native/extend-expect';<% } %>
2+
<%_ if(setupFile === 'react-router') { _%>
3+
import { TextEncoder, TextDecoder as NodeTextDecoder } from "util";
4+
5+
global.TextEncoder = TextEncoder;
6+
global.TextDecoder = NodeTextDecoder as typeof TextDecoder; // necessary because there is a mismatch between ts type and node type
7+
<%_ } _%>

packages/jest/src/generators/configuration/schema.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ export interface JestProjectSchema {
66
* @deprecated use setupFile instead
77
*/
88
skipSetupFile?: boolean;
9-
setupFile?: 'angular' | 'web-components' | 'react-native' | 'none';
9+
setupFile?:
10+
| 'angular'
11+
| 'web-components'
12+
| 'react-native'
13+
| 'react-router'
14+
| 'none';
1015
skipSerializers?: boolean;
1116
testEnvironment?: 'node' | 'jsdom' | 'none';
1217
/**

packages/react/src/generators/application/application.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,56 @@ describe('app', () => {
11611161
expect(packageJson.dependencies['react-router']).toBeDefined();
11621162
expect(packageJson.devDependencies['@react-router/dev']).toBeDefined();
11631163
});
1164+
1165+
it('should be configured to work with jest', async () => {
1166+
await applicationGenerator(appTree, {
1167+
...schema,
1168+
skipFormat: false,
1169+
useReactRouter: true,
1170+
routing: true,
1171+
bundler: 'vite',
1172+
unitTestRunner: 'jest',
1173+
});
1174+
1175+
const jestConfig = appTree.read('my-app/jest.config.ts').toString();
1176+
expect(jestConfig).toContain('@nx/react/plugins/jest');
1177+
expect(appTree.read('my-app/tsconfig.spec.json').toString())
1178+
.toMatchInlineSnapshot(`
1179+
"{
1180+
"extends": "./tsconfig.json",
1181+
"compilerOptions": {
1182+
"outDir": "../dist/out-tsc",
1183+
"module": "commonjs",
1184+
"moduleResolution": "node10",
1185+
"jsx": "react-jsx",
1186+
"types": [
1187+
"jest",
1188+
"node",
1189+
"@nx/react/typings/cssmodule.d.ts",
1190+
"@nx/react/typings/image.d.ts"
1191+
]
1192+
},
1193+
"files": ["src/test-setup.ts"],
1194+
"include": [
1195+
"jest.config.ts",
1196+
"src/**/*.test.ts",
1197+
"src/**/*.spec.ts",
1198+
"src/**/*.test.tsx",
1199+
"src/**/*.spec.tsx",
1200+
"src/**/*.test.js",
1201+
"src/**/*.spec.js",
1202+
"src/**/*.test.jsx",
1203+
"src/**/*.spec.jsx",
1204+
"src/**/*.d.ts",
1205+
"test/**/*.spec.tsx",
1206+
"test/**/*.spec.ts",
1207+
"test/**/*.test.tsx",
1208+
"test/**/*.test.ts"
1209+
]
1210+
}
1211+
"
1212+
`);
1213+
});
11641214
});
11651215

11661216
describe('--directory="." (--root-project)', () => {

packages/react/src/generators/application/application.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ export async function applicationGeneratorInternal(
235235
},
236236
options.linter === 'eslint'
237237
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
238-
: undefined
238+
: undefined,
239+
options.useReactRouter ? 'app' : 'src'
239240
);
240241

241242
sortPackageJsonFields(tree, options.appProjectRoot);

packages/react/src/generators/application/files/react-router-ssr/common/app/app-nav.tsx__tmpl__

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as React from "react";
21
import { NavLink } from "react-router";
32

43
export function AppNav() {

packages/react/src/generators/application/lib/add-jest.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ensurePackage, GeneratorCallback, Tree } from '@nx/devkit';
1+
import { ensurePackage, GeneratorCallback, Tree, updateJson } from '@nx/devkit';
22
import { NormalizedSchema } from '../schema';
33
import { nxVersion } from '../../../utils/versions';
4+
import { join } from 'node:path';
45

56
export async function addJest(
67
host: Tree,
@@ -15,14 +16,44 @@ export async function addJest(
1516
nxVersion
1617
);
1718

18-
return await configurationGenerator(host, {
19+
await configurationGenerator(host, {
1920
...options,
2021
project: options.projectName,
2122
supportTsx: true,
2223
skipSerializers: true,
23-
setupFile: 'none',
24+
setupFile: options.useReactRouter ? 'react-router' : 'none',
2425
compiler: options.compiler,
2526
skipFormat: true,
2627
runtimeTsconfigFileName: 'tsconfig.app.json',
2728
});
29+
30+
if (options.useReactRouter) {
31+
updateJson(
32+
host,
33+
join(options.appProjectRoot, 'tsconfig.spec.json'),
34+
(json) => {
35+
json.include = json.include ?? [];
36+
const reactRouterTestGlob = options.js
37+
? [
38+
'test/**/*.spec.jsx',
39+
'test/**/*.spec.js',
40+
'test/**/*.test.jsx',
41+
'test/**/*.test.js',
42+
]
43+
: [
44+
'test/**/*.spec.tsx',
45+
'test/**/*.spec.ts',
46+
'test/**/*.test.tsx',
47+
'test/**/*.test.ts',
48+
];
49+
return {
50+
...json,
51+
include: Array.from(
52+
new Set([...json.include, ...reactRouterTestGlob])
53+
),
54+
};
55+
}
56+
);
57+
}
58+
return () => {};
2859
}

0 commit comments

Comments
 (0)