diff --git a/__mocks__/ora.js b/__mocks__/ora.js new file mode 100644 index 00000000..60e663e0 --- /dev/null +++ b/__mocks__/ora.js @@ -0,0 +1,19 @@ +// CJS mock for ora (ESM-only in v9+) to allow schematics tests to run under Jest +'use strict'; + +const mockSpinner = { + start: () => mockSpinner, + stop: () => mockSpinner, + succeed: () => mockSpinner, + fail: () => mockSpinner, + warn: () => mockSpinner, + info: () => mockSpinner, + text: '', + prefixText: '', +}; + +const ora = () => mockSpinner; +ora.default = ora; + +module.exports = ora; +module.exports.default = ora; diff --git a/jest.config.ts b/jest.config.ts index 9c658224..1a100123 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,17 +1,14 @@ import type { Config } from 'jest'; -import { - createDefaultEsmPreset, - createDefaultPreset, - ESM_TS_TRANSFORM_PATTERN, - TS_EXT_TO_TREAT_AS_ESM, -} from 'ts-jest'; - -const esModules = ['@angular']; +import { createDefaultPreset } from 'ts-jest'; const config: Config = { ...createDefaultPreset(), testPathIgnorePatterns: ['/node_modules/', '/lib/', 'cypress'], snapshotFormat: { escapeString: true, printBasicPrototype: true }, + moduleNameMapper: { + // ora v9+ is ESM-only; mock it so @angular-devkit/schematics/testing can load under Jest (CJS) + '^ora$': '/__mocks__/ora.js', + }, }; export default config; diff --git a/libs/single-spa-community-angular/webpack/index.ts b/libs/single-spa-community-angular/webpack/index.ts index 00467904..3f172e32 100644 --- a/libs/single-spa-community-angular/webpack/index.ts +++ b/libs/single-spa-community-angular/webpack/index.ts @@ -272,7 +272,16 @@ function mergeConfigs( } } +declare const jest: any; function createLogger() { + // Under Jest, skip the NX logger and fall back to console.warn. + // Tests that care about suppressing output should wrap their call with `skipConsoleLogging`. + if (typeof jest !== 'undefined') { + return { + warn: (message: string) => console.warn(message), + }; + } + try { // If we're in an Nx workspace then use its logger. // eslint-disable-next-line diff --git a/schematics/ng-add/rules/update-configuration.ts b/schematics/ng-add/rules/update-configuration.ts index 69bb2ab5..29839b49 100644 --- a/schematics/ng-add/rules/update-configuration.ts +++ b/schematics/ng-add/rules/update-configuration.ts @@ -109,12 +109,16 @@ function updateTSConfig(tree: Tree, buildTarget: workspaces.TargetDefinition): v const tsConfig = parse(buffer.toString()); + // Only update `files` when it already exists in the tsconfig. + // Older Angular versions (≤15) used a `files` array to declare entry points + // (`main.ts` and `polyfills.ts`). We replace it with just `main.single-spa.ts` + // because polyfills are handled separately via the Webpack `entry` property. + // Newer Angular versions use `include`/`exclude` instead; those projects do not + // need this transformation. if (!Array.isArray(tsConfig.files)) { return; } - // The "files" property will only contain path to `main.single-spa.ts` file, - // because we remove `polyfills` from Webpack `entry` property. tsConfig.files = [normalize('src/main.single-spa.ts')]; tree.overwrite(tsConfigPath, JSON.stringify(tsConfig, null, 2)); } diff --git a/schematics/ng-add/tests/add-scripts.spec.ts b/schematics/ng-add/tests/add-scripts.spec.ts index 552f1d9c..454c9596 100644 --- a/schematics/ng-add/tests/add-scripts.spec.ts +++ b/schematics/ng-add/tests/add-scripts.spec.ts @@ -1,6 +1,6 @@ import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createTestRunner, VERSION } from './utils'; +import { createTestRunner, skipConsoleLogging, VERSION } from './utils'; const workspaceOptions = { name: 'workspace', @@ -40,7 +40,9 @@ describe('ng-add', () => { await generateApplication('first-cool-app'); await generateApplication('second-cool-app'); - await testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, workspaceTree); + await skipConsoleLogging(() => { + return testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, workspaceTree); + }); const tree = await testRunner.runSchematic( 'ng-add', diff --git a/schematics/ng-add/tests/index.spec.ts b/schematics/ng-add/tests/index.spec.ts index 4f4c8f69..1f003bb3 100644 --- a/schematics/ng-add/tests/index.spec.ts +++ b/schematics/ng-add/tests/index.spec.ts @@ -2,7 +2,13 @@ import { normalize } from 'path'; import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { Schema as NgAddOptions } from '../schema'; -import { createTestRunner, createWorkspace, getFileContent, VERSION } from './utils'; +import { + createTestRunner, + createWorkspace, + getFileContent, + skipConsoleLogging, + VERSION, +} from './utils'; const workspaceOptions = { name: 'ss-workspace', @@ -30,21 +36,25 @@ describe('ng-add', () => { }); test('should run ng-add', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); expect(tree.files).toBeDefined(); }); test('should add single-spa and single-spa-angular to dependencies', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); const packageJSON = JSON.parse(getFileContent(tree, '/package.json')); expect(packageJSON.dependencies['single-spa']).toBeDefined(); @@ -52,33 +62,39 @@ describe('ng-add', () => { }); test('should add style-laoder to devDependencies', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); const packageJSON = JSON.parse(getFileContent(tree, '/package.json')); expect(packageJSON.devDependencies['style-loader']).toBeDefined(); }); test('should add @angular-builders/custom-webpack to devDependencies', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); const packageJSON = JSON.parse(getFileContent(tree, '/package.json')); expect(packageJSON.devDependencies['@angular-builders/custom-webpack']).toBeDefined(); }); test('should add main-single-spa.ts', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); expect( tree.files.indexOf('/projects/ss-angular-cli-app/src/main.single-spa.ts'), @@ -86,11 +102,13 @@ describe('ng-add', () => { }); test('should use correct prefix for root', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app' }, + appTree, + ); + }); const mainModuleContent = getFileContent( tree, @@ -100,11 +118,13 @@ describe('ng-add', () => { }); test('should not add router dependencies', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app', routing: false }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app', routing: false }, + appTree, + ); + }); const mainModuleContent = getFileContent( tree, @@ -114,11 +134,13 @@ describe('ng-add', () => { }); test('should add router dependencies', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app', routing: true }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app', routing: true }, + appTree, + ); + }); const mainModuleContent = getFileContent( tree, @@ -128,11 +150,13 @@ describe('ng-add', () => { }); test('should modify angular.json', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { routing: true, project: 'ss-angular-cli-app' }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { routing: true, project: 'ss-angular-cli-app' }, + appTree, + ); + }); const angularJSON = JSON.parse(getFileContent(tree, '/angular.json')); const ssApp = angularJSON.projects['ss-angular-cli-app']; @@ -155,11 +179,13 @@ describe('ng-add', () => { }); test('should add build:single-spa:PROJECT_NAME npm script', async () => { - const tree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app', routing: true }, - appTree, - ); + const tree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app', routing: true }, + appTree, + ); + }); const packageJSON = JSON.parse(getFileContent(tree, '/package.json')); expect(packageJSON.scripts['build:single-spa:ss-angular-cli-app']).toBeDefined(); diff --git a/schematics/ng-add/tests/issue-168.spec.ts b/schematics/ng-add/tests/issue-168.spec.ts index c472bd41..ba282503 100644 --- a/schematics/ng-add/tests/issue-168.spec.ts +++ b/schematics/ng-add/tests/issue-168.spec.ts @@ -1,6 +1,6 @@ import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createTestRunner, VERSION } from './utils'; +import { createTestRunner, skipConsoleLogging, VERSION } from './utils'; const workspaceOptions = { name: 'workspace', @@ -45,7 +45,9 @@ describe('https://github.com/single-spa/single-spa-angular/issues/168', () => { // Act appTree.overwrite('/angular.json', JSON.stringify(buildTarget)); - await testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, appTree); + await skipConsoleLogging(() => { + return testRunner.runSchematic('ng-add', { project: 'first-cool-app' }, appTree); + }); buildTarget = JSON.parse(`${appTree.get('/angular.json')!.content}`); configurations = buildTarget.projects['first-cool-app'].architect.build.configurations; @@ -73,7 +75,9 @@ describe('https://github.com/single-spa/single-spa-angular/issues/168', () => { // Act appTree.overwrite('/angular.json', JSON.stringify(buildTarget)); - await testRunner.runSchematic('ng-add', { project: 'second-cool-app' }, appTree); + await skipConsoleLogging(() => { + return testRunner.runSchematic('ng-add', { project: 'second-cool-app' }, appTree); + }); buildTarget = JSON.parse(`${appTree.get('/angular.json')!.content}`); configurations = buildTarget.projects['second-cool-app'].architect.build.configurations; diff --git a/schematics/ng-add/tests/issue-249.spec.ts b/schematics/ng-add/tests/issue-249.spec.ts index 0890f7ee..78a201da 100644 --- a/schematics/ng-add/tests/issue-249.spec.ts +++ b/schematics/ng-add/tests/issue-249.spec.ts @@ -3,7 +3,13 @@ import { UnitTestTree } from '@angular-devkit/schematics/testing'; import * as JSON5 from 'json5'; import { Schema as NgAddOptions } from '../schema'; -import { createWorkspace, createTestRunner, getFileContent, VERSION } from './utils'; +import { + createWorkspace, + createTestRunner, + getFileContent, + VERSION, + skipConsoleLogging, +} from './utils'; const workspaceOptions = { name: 'ss-workspace', @@ -23,12 +29,21 @@ const appOptions = { const angular10Comment = '/* Unexpected comment from Angular */'; -// Simulate what angular 10 is doing adding a comment to the tsconfig file +// Simulate what Angular 10 does: prepend a comment AND include a `files` array. +// Newer Angular versions dropped `files` in favour of `include`/`exclude`, but +// Angular 10–15 shipped both. The comment would break JSON.parse; JSON5 handles it. // https://github.com/single-spa/single-spa-angular/issues/249 -function appendCommentToTsConfig(tree: UnitTestTree) { - let content = getFileContent(tree, '/projects/ss-angular-cli-app/tsconfig.app.json'); - content = angular10Comment + '\n' + content; - tree.overwrite('/projects/ss-angular-cli-app/tsconfig.app.json', content); +function patchTsConfigToSimulateAngular10(tree: UnitTestTree) { + const tsConfig = JSON5.parse( + getFileContent(tree, '/projects/ss-angular-cli-app/tsconfig.app.json'), + ); + + // Inject the `files` entry that older Angular versions included. + tsConfig.files = ['src/main.ts', 'src/polyfills.ts']; + + // Prepend the comment that Angular 10 adds to every tsconfig. + const patched = angular10Comment + '\n' + JSON.stringify(tsConfig, null, 2); + tree.overwrite('/projects/ss-angular-cli-app/tsconfig.app.json', patched); } 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', () => { beforeEach(async () => { appTree = await createWorkspace(testRunner, appTree, workspaceOptions, appOptions); - appendCommentToTsConfig(appTree); + patchTsConfigToSimulateAngular10(appTree); }); test('should update `tsconfig.app.json` and add `main.single-spa.ts` to `files`', async () => { - appTree = await testRunner.runSchematic( - 'ng-add', - { project: 'ss-angular-cli-app', routing: true }, - appTree, - ); + appTree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'ss-angular-cli-app', routing: true }, + appTree, + ); + }); const expectedTsConfigPath = normalize('projects/ss-angular-cli-app/tsconfig.app.json'); const buffer: Buffer | null = appTree.read(expectedTsConfigPath); diff --git a/schematics/ng-add/tests/issue-341.spec.ts b/schematics/ng-add/tests/issue-341.spec.ts index aadf7d5c..f6345841 100644 --- a/schematics/ng-add/tests/issue-341.spec.ts +++ b/schematics/ng-add/tests/issue-341.spec.ts @@ -1,7 +1,7 @@ import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { Schema as NgAddOptions } from '../schema'; -import { createTestRunner, createWorkspace, VERSION } from './utils'; +import { createTestRunner, createWorkspace, skipConsoleLogging, VERSION } from './utils'; const workspaceOptions = { name: 'workspace', @@ -27,11 +27,13 @@ describe('https://github.com/single-spa/single-spa-angular/issues/341', () => { }); // Act - appTree = await testRunner.runSchematic( - 'ng-add', - { project: 'first-cool-app', routing: true }, - appTree, - ); + appTree = await skipConsoleLogging(() => { + return testRunner.runSchematic( + 'ng-add', + { project: 'first-cool-app', routing: true }, + appTree, + ); + }); subscription.unsubscribe(); diff --git a/schematics/ng-add/tests/utils.ts b/schematics/ng-add/tests/utils.ts index fc551ba5..c3254ab0 100644 --- a/schematics/ng-add/tests/utils.ts +++ b/schematics/ng-add/tests/utils.ts @@ -41,3 +41,42 @@ export function getFileContent(tree: Tree, path: string): string { return fileEntry.content.toString(); } + +export type ConsoleRecord = [string, any[]]; +export type ConsoleRecorder = ConsoleRecord[]; + +export function skipConsoleLogging any>( + fn: T, + consoleRecorder: ConsoleRecorder = [], +): ReturnType { + const consoleSpies = [ + jest.spyOn(console, 'log').mockImplementation((...args) => { + consoleRecorder.push(['log', args]); + }), + jest.spyOn(console, 'warn').mockImplementation((...args) => { + consoleRecorder.push(['warn', args]); + }), + jest.spyOn(console, 'error').mockImplementation((...args) => { + consoleRecorder.push(['error', args]); + }), + jest.spyOn(console, 'info').mockImplementation((...args) => { + consoleRecorder.push(['info', args]); + }), + ]; + function restoreSpies() { + consoleSpies.forEach(spy => spy.mockRestore()); + } + let restoreSpyAsync = false; + try { + const returnValue = fn(); + if (returnValue instanceof Promise) { + restoreSpyAsync = true; + return returnValue.finally(() => restoreSpies()) as ReturnType; + } + return returnValue; + } finally { + if (!restoreSpyAsync) { + restoreSpies(); + } + } +}