Skip to content

Commit f6338cb

Browse files
authored
fix: restore schematics unit tests under Jest (#570)
- Mock ora (ESM-only v9+) via moduleNameMapper so @angular-devkit/schematics/testing can be loaded in Jest's CJS environment - Add skipConsoleLogging utility and wrap all runSchematic calls to suppress NX warnings from polluting the test terminal - Fix updateTSConfig to only update the `files` field when it already exists (Angular 16+ uses include/exclude instead); add explanatory comments - Update issue-249 fixture to inject a `files` array alongside the Angular 10 comment, accurately simulating the scenario the test was written for - Add jest branch to createLogger with a comment explaining the fallback to console.warn
1 parent 2b7656b commit f6338cb

10 files changed

Lines changed: 203 additions & 84 deletions

File tree

__mocks__/ora.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// CJS mock for ora (ESM-only in v9+) to allow schematics tests to run under Jest
2+
'use strict';
3+
4+
const mockSpinner = {
5+
start: () => mockSpinner,
6+
stop: () => mockSpinner,
7+
succeed: () => mockSpinner,
8+
fail: () => mockSpinner,
9+
warn: () => mockSpinner,
10+
info: () => mockSpinner,
11+
text: '',
12+
prefixText: '',
13+
};
14+
15+
const ora = () => mockSpinner;
16+
ora.default = ora;
17+
18+
module.exports = ora;
19+
module.exports.default = ora;

jest.config.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import type { Config } from 'jest';
2-
import {
3-
createDefaultEsmPreset,
4-
createDefaultPreset,
5-
ESM_TS_TRANSFORM_PATTERN,
6-
TS_EXT_TO_TREAT_AS_ESM,
7-
} from 'ts-jest';
8-
9-
const esModules = ['@angular'];
2+
import { createDefaultPreset } from 'ts-jest';
103

114
const config: Config = {
125
...createDefaultPreset(),
136
testPathIgnorePatterns: ['/node_modules/', '/lib/', 'cypress'],
147
snapshotFormat: { escapeString: true, printBasicPrototype: true },
8+
moduleNameMapper: {
9+
// ora v9+ is ESM-only; mock it so @angular-devkit/schematics/testing can load under Jest (CJS)
10+
'^ora$': '<rootDir>/__mocks__/ora.js',
11+
},
1512
};
1613

1714
export default config;

libs/single-spa-community-angular/webpack/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,16 @@ function mergeConfigs(
272272
}
273273
}
274274

275+
declare const jest: any;
275276
function createLogger() {
277+
// Under Jest, skip the NX logger and fall back to console.warn.
278+
// Tests that care about suppressing output should wrap their call with `skipConsoleLogging`.
279+
if (typeof jest !== 'undefined') {
280+
return {
281+
warn: (message: string) => console.warn(message),
282+
};
283+
}
284+
276285
try {
277286
// If we're in an Nx workspace then use its logger.
278287
// eslint-disable-next-line

schematics/ng-add/rules/update-configuration.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,16 @@ function updateTSConfig(tree: Tree, buildTarget: workspaces.TargetDefinition): v
109109

110110
const tsConfig = parse(buffer.toString());
111111

112+
// Only update `files` when it already exists in the tsconfig.
113+
// Older Angular versions (≤15) used a `files` array to declare entry points
114+
// (`main.ts` and `polyfills.ts`). We replace it with just `main.single-spa.ts`
115+
// because polyfills are handled separately via the Webpack `entry` property.
116+
// Newer Angular versions use `include`/`exclude` instead; those projects do not
117+
// need this transformation.
112118
if (!Array.isArray(tsConfig.files)) {
113119
return;
114120
}
115121

116-
// The "files" property will only contain path to `main.single-spa.ts` file,
117-
// because we remove `polyfills` from Webpack `entry` property.
118122
tsConfig.files = [normalize('src/main.single-spa.ts')];
119123
tree.overwrite(tsConfigPath, JSON.stringify(tsConfig, null, 2));
120124
}

schematics/ng-add/tests/add-scripts.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { UnitTestTree } from '@angular-devkit/schematics/testing';
22

3-
import { createTestRunner, VERSION } from './utils';
3+
import { createTestRunner, skipConsoleLogging, VERSION } from './utils';
44

55
const workspaceOptions = {
66
name: 'workspace',
@@ -40,7 +40,9 @@ describe('ng-add', () => {
4040
await generateApplication('first-cool-app');
4141
await generateApplication('second-cool-app');
4242

43-
await testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, workspaceTree);
43+
await skipConsoleLogging(() => {
44+
return testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, workspaceTree);
45+
});
4446

4547
const tree = await testRunner.runSchematic(
4648
'ng-add',

schematics/ng-add/tests/index.spec.ts

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { normalize } from 'path';
22
import { UnitTestTree } from '@angular-devkit/schematics/testing';
33

44
import { Schema as NgAddOptions } from '../schema';
5-
import { createTestRunner, createWorkspace, getFileContent, VERSION } from './utils';
5+
import {
6+
createTestRunner,
7+
createWorkspace,
8+
getFileContent,
9+
skipConsoleLogging,
10+
VERSION,
11+
} from './utils';
612

713
const workspaceOptions = {
814
name: 'ss-workspace',
@@ -30,67 +36,79 @@ describe('ng-add', () => {
3036
});
3137

3238
test('should run ng-add', async () => {
33-
const tree = await testRunner.runSchematic(
34-
'ng-add',
35-
{ project: 'ss-angular-cli-app' },
36-
appTree,
37-
);
39+
const tree = await skipConsoleLogging(() => {
40+
return testRunner.runSchematic<NgAddOptions>(
41+
'ng-add',
42+
{ project: 'ss-angular-cli-app' },
43+
appTree,
44+
);
45+
});
3846

3947
expect(tree.files).toBeDefined();
4048
});
4149

4250
test('should add single-spa and single-spa-angular to dependencies', async () => {
43-
const tree = await testRunner.runSchematic<NgAddOptions>(
44-
'ng-add',
45-
{ project: 'ss-angular-cli-app' },
46-
appTree,
47-
);
51+
const tree = await skipConsoleLogging(() => {
52+
return testRunner.runSchematic<NgAddOptions>(
53+
'ng-add',
54+
{ project: 'ss-angular-cli-app' },
55+
appTree,
56+
);
57+
});
4858

4959
const packageJSON = JSON.parse(getFileContent(tree, '/package.json'));
5060
expect(packageJSON.dependencies['single-spa']).toBeDefined();
5161
expect(packageJSON.dependencies['single-spa-angular']).toBeDefined();
5262
});
5363

5464
test('should add style-laoder to devDependencies', async () => {
55-
const tree = await testRunner.runSchematic<NgAddOptions>(
56-
'ng-add',
57-
{ project: 'ss-angular-cli-app' },
58-
appTree,
59-
);
65+
const tree = await skipConsoleLogging(() => {
66+
return testRunner.runSchematic<NgAddOptions>(
67+
'ng-add',
68+
{ project: 'ss-angular-cli-app' },
69+
appTree,
70+
);
71+
});
6072

6173
const packageJSON = JSON.parse(getFileContent(tree, '/package.json'));
6274
expect(packageJSON.devDependencies['style-loader']).toBeDefined();
6375
});
6476

6577
test('should add @angular-builders/custom-webpack to devDependencies', async () => {
66-
const tree = await testRunner.runSchematic<NgAddOptions>(
67-
'ng-add',
68-
{ project: 'ss-angular-cli-app' },
69-
appTree,
70-
);
78+
const tree = await skipConsoleLogging(() => {
79+
return testRunner.runSchematic<NgAddOptions>(
80+
'ng-add',
81+
{ project: 'ss-angular-cli-app' },
82+
appTree,
83+
);
84+
});
7185

7286
const packageJSON = JSON.parse(getFileContent(tree, '/package.json'));
7387
expect(packageJSON.devDependencies['@angular-builders/custom-webpack']).toBeDefined();
7488
});
7589

7690
test('should add main-single-spa.ts', async () => {
77-
const tree = await testRunner.runSchematic<NgAddOptions>(
78-
'ng-add',
79-
{ project: 'ss-angular-cli-app' },
80-
appTree,
81-
);
91+
const tree = await skipConsoleLogging(() => {
92+
return testRunner.runSchematic<NgAddOptions>(
93+
'ng-add',
94+
{ project: 'ss-angular-cli-app' },
95+
appTree,
96+
);
97+
});
8298

8399
expect(
84100
tree.files.indexOf('/projects/ss-angular-cli-app/src/main.single-spa.ts'),
85101
).toBeGreaterThan(-1);
86102
});
87103

88104
test('should use correct prefix for root', async () => {
89-
const tree = await testRunner.runSchematic<NgAddOptions>(
90-
'ng-add',
91-
{ project: 'ss-angular-cli-app' },
92-
appTree,
93-
);
105+
const tree = await skipConsoleLogging(() => {
106+
return testRunner.runSchematic<NgAddOptions>(
107+
'ng-add',
108+
{ project: 'ss-angular-cli-app' },
109+
appTree,
110+
);
111+
});
94112

95113
const mainModuleContent = getFileContent(
96114
tree,
@@ -100,11 +118,13 @@ describe('ng-add', () => {
100118
});
101119

102120
test('should not add router dependencies', async () => {
103-
const tree = await testRunner.runSchematic<NgAddOptions>(
104-
'ng-add',
105-
{ project: 'ss-angular-cli-app', routing: false },
106-
appTree,
107-
);
121+
const tree = await skipConsoleLogging(() => {
122+
return testRunner.runSchematic<NgAddOptions>(
123+
'ng-add',
124+
{ project: 'ss-angular-cli-app', routing: false },
125+
appTree,
126+
);
127+
});
108128

109129
const mainModuleContent = getFileContent(
110130
tree,
@@ -114,11 +134,13 @@ describe('ng-add', () => {
114134
});
115135

116136
test('should add router dependencies', async () => {
117-
const tree = await testRunner.runSchematic<NgAddOptions>(
118-
'ng-add',
119-
{ project: 'ss-angular-cli-app', routing: true },
120-
appTree,
121-
);
137+
const tree = await skipConsoleLogging(() => {
138+
return testRunner.runSchematic<NgAddOptions>(
139+
'ng-add',
140+
{ project: 'ss-angular-cli-app', routing: true },
141+
appTree,
142+
);
143+
});
122144

123145
const mainModuleContent = getFileContent(
124146
tree,
@@ -128,11 +150,13 @@ describe('ng-add', () => {
128150
});
129151

130152
test('should modify angular.json', async () => {
131-
const tree = await testRunner.runSchematic<NgAddOptions>(
132-
'ng-add',
133-
{ routing: true, project: 'ss-angular-cli-app' },
134-
appTree,
135-
);
153+
const tree = await skipConsoleLogging(() => {
154+
return testRunner.runSchematic<NgAddOptions>(
155+
'ng-add',
156+
{ routing: true, project: 'ss-angular-cli-app' },
157+
appTree,
158+
);
159+
});
136160

137161
const angularJSON = JSON.parse(getFileContent(tree, '/angular.json'));
138162
const ssApp = angularJSON.projects['ss-angular-cli-app'];
@@ -155,11 +179,13 @@ describe('ng-add', () => {
155179
});
156180

157181
test('should add build:single-spa:PROJECT_NAME npm script', async () => {
158-
const tree = await testRunner.runSchematic<NgAddOptions>(
159-
'ng-add',
160-
{ project: 'ss-angular-cli-app', routing: true },
161-
appTree,
162-
);
182+
const tree = await skipConsoleLogging(() => {
183+
return testRunner.runSchematic<NgAddOptions>(
184+
'ng-add',
185+
{ project: 'ss-angular-cli-app', routing: true },
186+
appTree,
187+
);
188+
});
163189

164190
const packageJSON = JSON.parse(getFileContent(tree, '/package.json'));
165191
expect(packageJSON.scripts['build:single-spa:ss-angular-cli-app']).toBeDefined();

schematics/ng-add/tests/issue-168.spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { UnitTestTree } from '@angular-devkit/schematics/testing';
22

3-
import { createTestRunner, VERSION } from './utils';
3+
import { createTestRunner, skipConsoleLogging, VERSION } from './utils';
44

55
const workspaceOptions = {
66
name: 'workspace',
@@ -45,7 +45,9 @@ describe('https://github.com/single-spa/single-spa-angular/issues/168', () => {
4545
// Act
4646
appTree.overwrite('/angular.json', JSON.stringify(buildTarget));
4747

48-
await testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, appTree);
48+
await skipConsoleLogging(() => {
49+
return testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, appTree);
50+
});
4951

5052
buildTarget = JSON.parse(`${appTree.get('/angular.json')!.content}`);
5153
configurations = buildTarget.projects['first-cool-app'].architect.build.configurations;
@@ -73,7 +75,9 @@ describe('https://github.com/single-spa/single-spa-angular/issues/168', () => {
7375
// Act
7476
appTree.overwrite('/angular.json', JSON.stringify(buildTarget));
7577

76-
await testRunner.runSchematic('ng-add', { project: 'second-cool-app' }, appTree);
78+
await skipConsoleLogging(() => {
79+
return testRunner.runSchematic('ng-add', { project: 'second-cool-app' }, appTree);
80+
});
7781

7882
buildTarget = JSON.parse(`${appTree.get('/angular.json')!.content}`);
7983
configurations = buildTarget.projects['second-cool-app'].architect.build.configurations;

schematics/ng-add/tests/issue-249.spec.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { UnitTestTree } from '@angular-devkit/schematics/testing';
33
import * as JSON5 from 'json5';
44

55
import { Schema as NgAddOptions } from '../schema';
6-
import { createWorkspace, createTestRunner, getFileContent, VERSION } from './utils';
6+
import {
7+
createWorkspace,
8+
createTestRunner,
9+
getFileContent,
10+
VERSION,
11+
skipConsoleLogging,
12+
} from './utils';
713

814
const workspaceOptions = {
915
name: 'ss-workspace',
@@ -23,12 +29,21 @@ const appOptions = {
2329

2430
const angular10Comment = '/* Unexpected comment from Angular */';
2531

26-
// Simulate what angular 10 is doing adding a comment to the tsconfig file
32+
// Simulate what Angular 10 does: prepend a comment AND include a `files` array.
33+
// Newer Angular versions dropped `files` in favour of `include`/`exclude`, but
34+
// Angular 10–15 shipped both. The comment would break JSON.parse; JSON5 handles it.
2735
// https://github.com/single-spa/single-spa-angular/issues/249
28-
function appendCommentToTsConfig(tree: UnitTestTree) {
29-
let content = getFileContent(tree, '/projects/ss-angular-cli-app/tsconfig.app.json');
30-
content = angular10Comment + '\n' + content;
31-
tree.overwrite('/projects/ss-angular-cli-app/tsconfig.app.json', content);
36+
function patchTsConfigToSimulateAngular10(tree: UnitTestTree) {
37+
const tsConfig = JSON5.parse(
38+
getFileContent(tree, '/projects/ss-angular-cli-app/tsconfig.app.json'),
39+
);
40+
41+
// Inject the `files` entry that older Angular versions included.
42+
tsConfig.files = ['src/main.ts', 'src/polyfills.ts'];
43+
44+
// Prepend the comment that Angular 10 adds to every tsconfig.
45+
const patched = angular10Comment + '\n' + JSON.stringify(tsConfig, null, 2);
46+
tree.overwrite('/projects/ss-angular-cli-app/tsconfig.app.json', patched);
3247
}
3348

3449
describe('https://github.com/single-spa/single-spa-angular/issues/249', () => {
@@ -37,15 +52,17 @@ describe('https://github.com/single-spa/single-spa-angular/issues/249', () => {
3752

3853
beforeEach(async () => {
3954
appTree = await createWorkspace(testRunner, appTree, workspaceOptions, appOptions);
40-
appendCommentToTsConfig(appTree);
55+
patchTsConfigToSimulateAngular10(appTree);
4156
});
4257

4358
test('should update `tsconfig.app.json` and add `main.single-spa.ts` to `files`', async () => {
44-
appTree = await testRunner.runSchematic<NgAddOptions>(
45-
'ng-add',
46-
{ project: 'ss-angular-cli-app', routing: true },
47-
appTree,
48-
);
59+
appTree = await skipConsoleLogging(() => {
60+
return testRunner.runSchematic<NgAddOptions>(
61+
'ng-add',
62+
{ project: 'ss-angular-cli-app', routing: true },
63+
appTree,
64+
);
65+
});
4966

5067
const expectedTsConfigPath = normalize('projects/ss-angular-cli-app/tsconfig.app.json');
5168
const buffer: Buffer | null = appTree.read(expectedTsConfigPath);

0 commit comments

Comments
 (0)