fix(core): support unit testing of nx generators invoking wrapped schematics

This commit is contained in:
vsavkin 2021-02-23 11:14:57 -05:00 committed by Victor Savkin
parent fd18b5edec
commit 3157293752
5 changed files with 312 additions and 175 deletions

View File

@ -1,4 +1,6 @@
export { export {
wrapAngularDevkitSchematic, wrapAngularDevkitSchematic,
overrideCollectionResolutionForTesting,
mockSchematicsForTesting,
NxScopedHost, NxScopedHost,
} from '@nrwl/tao/src/commands/ngcli-adapter'; } from '@nrwl/tao/src/commands/ngcli-adapter';

View File

@ -289,13 +289,7 @@ export class NxScopedHost extends virtualFs.ScopedHost<any> {
actualConfigFileName: any; actualConfigFileName: any;
isNewFormat: boolean; isNewFormat: boolean;
}> { }> {
const p = path.toString(); if (isWorkspaceConfigPath(path)) {
if (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
) {
return super.exists('/angular.json' as any).pipe( return super.exists('/angular.json' as any).pipe(
switchMap((isAngularJson) => { switchMap((isAngularJson) => {
const actualConfigFileName = isAngularJson const actualConfigFileName = isAngularJson
@ -340,7 +334,7 @@ export class NxScopedHostForMigrations extends NxScopedHost {
} }
read(path: Path): Observable<FileBuffer> { read(path: Path): Observable<FileBuffer> {
if (this.isWorkspaceConfig(path)) { if (isWorkspaceConfigPath(path)) {
return super.read(path).pipe(map(processConfigWhenReading)); return super.read(path).pipe(map(processConfigWhenReading));
} else { } else {
return super.read(path); return super.read(path);
@ -348,22 +342,12 @@ export class NxScopedHostForMigrations extends NxScopedHost {
} }
write(path: Path, content: FileBuffer) { write(path: Path, content: FileBuffer) {
if (this.isWorkspaceConfig(path)) { if (isWorkspaceConfigPath(path)) {
return super.write(path, processConfigWhenWriting(content)); return super.write(path, processConfigWhenWriting(content));
} else { } else {
return super.write(path, content); return super.write(path, content);
} }
} }
protected isWorkspaceConfig(path: Path) {
const p = path.toString();
return (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
);
}
} }
export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost { export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
@ -372,46 +356,77 @@ export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
} }
read(path: Path): Observable<FileBuffer> { read(path: Path): Observable<FileBuffer> {
return this.context(path).pipe( if (isWorkspaceConfigPath(path)) {
switchMap((r) => { const match = findWorkspaceConfigFileChange(this.host);
// if it is a workspace config, handle it in a special way // no match, default to existing behavior
if (r.isWorkspaceConfig) { if (!match) {
const match = this.host return super.read(path);
.listChanges() }
.find(
(f) => f.path == 'workspace.json' || f.path == 'angular.json'
);
// no match, default to existing behavior // we try to format it, if it changes, return it, otherwise return the original change
if (!match) { try {
return super.read(path); const w = JSON.parse(Buffer.from(match.content).toString());
} const formatted = toOldFormatOrNull(w);
return of(
// we try to format it, if it changes, return it, otherwise return the original change formatted
try { ? Buffer.from(JSON.stringify(formatted, null, 2))
const w = JSON.parse(Buffer.from(match.content).toString()); : Buffer.from(match.content)
const formatted = toOldFormatOrNull(w); );
return of( } catch (e) {
formatted return super.read(path);
? Buffer.from(JSON.stringify(formatted, null, 2)) }
: Buffer.from(match.content) } else {
); // found a matching change in the host
} catch (e) { const match = findMatchingFileChange(this.host, path);
return super.read(path); return match ? of(Buffer.from(match.content)) : super.read(path);
} }
} else {
const targetPath = path.startsWith('/') ? path.substring(1) : path;
// found a matching change in the host
const match = this.host
.listChanges()
.find(
(f) => f.path == targetPath.toString() && f.type !== 'DELETE'
);
return match ? of(Buffer.from(match.content)) : super.read(path);
}
})
);
} }
exists(path: Path): Observable<boolean> {
if (isWorkspaceConfigPath(path)) {
return findWorkspaceConfigFileChange(this.host)
? of(true)
: super.exists(path);
} else {
return findMatchingFileChange(this.host, path)
? of(true)
: super.exists(path);
}
}
isFile(path: Path): Observable<boolean> {
if (isWorkspaceConfigPath(path)) {
return findWorkspaceConfigFileChange(this.host)
? of(true)
: super.isFile(path);
} else {
return findMatchingFileChange(this.host, path)
? of(true)
: super.isFile(path);
}
}
}
function findWorkspaceConfigFileChange(host: Tree) {
return host
.listChanges()
.find((f) => f.path == 'workspace.json' || f.path == 'angular.json');
}
function findMatchingFileChange(host: Tree, path: Path) {
const targetPath = path.startsWith('/') ? path.substring(1) : path.toString;
return host
.listChanges()
.find((f) => f.path == targetPath.toString() && f.type !== 'DELETE');
}
function isWorkspaceConfigPath(p: Path | string) {
return (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
);
} }
function processConfigWhenReading(content: ArrayBuffer) { function processConfigWhenReading(content: ArrayBuffer) {
@ -637,11 +652,77 @@ function convertEventTypeToHandleMultipleConfigNames(
} }
} }
let collectionResolutionOverrides = null;
let mockedSchematics = null;
/**
* By default, Angular Devkit schematic collections will be resolved using the Node resolution.
* This doesn't work if you are testing schematics that refer to other schematics in the
* same repo.
*
* This function can can be used to override the resolution behaviour.
*
* Example:
*
* ```
* overrideCollectionResolutionForTesting({
* '@nrwl/workspace': path.join(__dirname, '../../../../workspace/collection.json'),
* '@nrwl/angular': path.join(__dirname, '../../../../angular/collection.json'),
* '@nrwl/linter': path.join(__dirname, '../../../../linter/collection.json')
* });
*
* ```
*/
export function overrideCollectionResolutionForTesting(collections: {
[name: string]: string;
}) {
collectionResolutionOverrides = collections;
}
/**
* If you have an Nx Devkit generator invoking the wrapped Angular Devkit schematic,
* and you don't want the Angular Devkit schematic to run, you can mock it up using this function.
*
* Unfortunately, there are some edge cases in the Nx-Angular devkit integration that
* can be seen in the unit tests context. This function is useful for handling that as well.
*
* In this case, you can mock it up.
*
* Example:
*
* ```
* mockSchematicsForTesting({
* 'mycollection:myschematic': (tree, params) => {
* tree.write('README.md');
* }
* });
*
* ```
*/
export function mockSchematicsForTesting(schematics: {
[name: string]: (
host: Tree,
generatorOptions: { [k: string]: any }
) => Promise<void>;
}) {
mockedSchematics = schematics;
}
export function wrapAngularDevkitSchematic( export function wrapAngularDevkitSchematic(
collectionName: string, collectionName: string,
generatorName: string generatorName: string
) { ) {
return async (host: Tree, generatorOptions: { [k: string]: any }) => { return async (host: Tree, generatorOptions: { [k: string]: any }) => {
if (
mockedSchematics &&
mockedSchematics[`${collectionName}:${generatorName}`]
) {
return await mockedSchematics[`${collectionName}:${generatorName}`](
host,
generatorOptions
);
}
const emptyLogger = { const emptyLogger = {
log: (e) => {}, log: (e) => {},
info: (e) => {}, info: (e) => {},
@ -694,6 +775,19 @@ export function wrapAngularDevkitSchematic(
defaults: false, defaults: false,
}; };
const workflow = createWorkflow(fsHost, host.root, options); const workflow = createWorkflow(fsHost, host.root, options);
// used for testing
if (collectionResolutionOverrides) {
const r = workflow.engineHost.resolve;
workflow.engineHost.resolve = (collection, b, c) => {
if (collectionResolutionOverrides[collection]) {
return collectionResolutionOverrides[collection];
} else {
return r.apply(workflow.engineHost, [collection, b, c]);
}
};
}
const collection = getCollection(workflow, collectionName); const collection = getCollection(workflow, collectionName);
const schematic = collection.createSchematic(generatorName, true); const schematic = collection.createSchematic(generatorName, true);
const res = await runSchematic( const res = await runSchematic(

View File

@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`preset should create files (preset = angular) 1`] = `
Array [
"tsconfig.editor.json",
"tsconfig.json",
"src",
".browserslistrc",
"tsconfig.app.json",
".eslintrc.json",
"jest.config.js",
"tsconfig.spec.json",
]
`;
exports[`preset should create files (preset = angular) 2`] = `
Array [
"favicon.ico",
"index.html",
"main.ts",
"polyfills.ts",
"styles.css",
"assets",
"environments",
"app",
"test-setup.ts",
]
`;
exports[`preset should create files (preset = angular) 3`] = `
Array [
"app.module.ts",
"app.component.css",
"app.component.html",
"app.component.spec.ts",
"app.component.ts",
]
`;

View File

@ -1,131 +1,132 @@
import { readJson, Tree } from '@nrwl/devkit'; import { readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter';
import { presetGenerator } from './preset'; import { presetGenerator } from './preset';
import * as path from 'path';
describe('preset', () => { describe('preset', () => {
let tree: Tree; let tree: Tree;
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
}); overrideCollectionResolutionForTesting({
'@nrwl/workspace': path.join(
describe('--preset', () => { __dirname,
// TODO: reenable. This doesn't work because wrapAngularDevkit uses the fs '../../../../workspace/collection.json'
xdescribe('angular', () => { ),
it('should create files (preset = angular)', async () => { '@nrwl/angular': path.join(
await presetGenerator(tree, { __dirname,
name: 'proj', '../../../../angular/collection.json'
preset: 'angular', ),
cli: 'nx', '@nrwl/linter': path.join(
}); __dirname,
expect(tree.children('apps/proj')).toMatchSnapshot(); '../../../../linter/collection.json'
expect(tree.children('apps/proj/src/')).toMatchSnapshot(); ),
expect(tree.children('apps/proj/src/app')).toMatchSnapshot(); '@nrwl/nest': path.join(__dirname, '../../../../nest/collection.json'),
console.log(tree.children('')); '@nrwl/node': path.join(__dirname, '../../../../node/collection.json'),
'@nrwl/jest': path.join(__dirname, '../../../../jest/collection.json'),
expect( '@nrwl/cypress': path.join(
JSON.parse(tree.read('/workspace.json').toString()).cli __dirname,
.defaultCollection '../../../../cypress/collection.json'
).toBe('@nrwl/angular'); ),
}); '@nrwl/express': path.join(
}); __dirname,
'../../../../express/collection.json'
describe('web-components', () => { ),
it('should create files (preset = web-components)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'web-components',
cli: 'nx',
});
expect(tree.exists('/apps/proj/src/main.ts')).toBe(true);
expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
'@nrwl/web'
);
});
}); });
}); });
// it('should create files (preset = react)', async () => { afterEach(() => {
// const tree = await runSchematic( overrideCollectionResolutionForTesting(null);
// 'preset', });
// { name: 'proj', preset: 'react' },
// tree it('should create files (preset = angular)', async () => {
// ); await presetGenerator(tree, {
// expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true); name: 'proj',
// expect( preset: 'angular',
// JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection cli: 'nx',
// ).toBe('@nrwl/react'); });
// }); expect(tree.children('apps/proj')).toMatchSnapshot();
// expect(tree.children('apps/proj/src/')).toMatchSnapshot();
// expect(tree.children('apps/proj/src/app')).toMatchSnapshot();
// it('should create files (preset = next)', async () => { console.log(tree.children(''));
// const tree = await runSchematic(
// 'preset', expect(
// { name: 'proj', preset: 'next' }, JSON.parse(tree.read('/workspace.json').toString()).cli.defaultCollection
// tree ).toBe('@nrwl/angular');
// ); });
// expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true);
// expect( it('should create files (preset = web-components)', async () => {
// JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection await presetGenerator(tree, {
// ).toBe('@nrwl/next'); name: 'proj',
// }); preset: 'web-components',
// cli: 'nx',
// describe('--preset angular-nest', () => { });
// it('should create files', async () => { expect(tree.exists('/apps/proj/src/main.ts')).toBe(true);
// const tree = await runSchematic( expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
// 'preset', '@nrwl/web'
// { name: 'proj', preset: 'angular-nest' }, );
// tree });
// );
// expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true); it('should create files (preset = react)', async () => {
// expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true); await presetGenerator(tree, {
// expect( name: 'proj',
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') preset: 'react',
// ).toBe(true); style: 'css',
// }); linter: 'eslint',
// cli: 'nx',
// it('should work with unnormalized names', async () => { });
// const tree = await runSchematic( expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true);
// 'preset', expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
// { name: 'myProj', preset: 'angular-nest' }, '@nrwl/react'
// tree );
// ); });
//
// expect(tree.exists('/apps/my-proj/src/app/app.component.ts')).toBe(true); it('should create files (preset = next)', async () => {
// expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true); await presetGenerator(tree, {
// expect( name: 'proj',
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') preset: 'next',
// ).toBe(true); style: 'css',
// }); linter: 'eslint',
// }); cli: 'nx',
// });
// describe('--preset react-express', () => { expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true);
// it('should create files', async () => { expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
// const tree = await runSchematic( '@nrwl/next'
// 'preset', );
// { name: 'proj', preset: 'react-express' }, });
// tree
// ); it('should create files (preset = angular-nest)', async () => {
// expect(tree.exists('/apps/proj/src/app/app.tsx')).toBe(true); await presetGenerator(tree, {
// expect( name: 'proj',
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') preset: 'angular-nest',
// ).toBe(true); style: 'css',
// expect(tree.exists('/apps/proj/.eslintrc.json')).toBe(true); linter: 'eslint',
// expect(tree.exists('/apps/api/.eslintrc.json')).toBe(true); cli: 'nx',
// expect(tree.exists('/libs/api-interfaces/.eslintrc.json')).toBe(true); });
// });
// expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true);
// it('should work with unnormalized names', async () => { expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true);
// const tree = await runSchematic( expect(tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')).toBe(
// 'preset', true
// { name: 'myProj', preset: 'react-express' }, );
// tree });
// );
// it('should create files (preset react-express)', async () => {
// expect(tree.exists('/apps/my-proj/src/app/app.tsx')).toBe(true); await presetGenerator(tree, {
// expect( name: 'proj',
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') preset: 'react-express',
// ).toBe(true); style: 'css',
// }); linter: 'eslint',
// }); cli: 'nx',
});
expect(tree.exists('/apps/proj/src/app/app.tsx')).toBe(true);
expect(tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')).toBe(
true
);
expect(tree.exists('/apps/proj/.eslintrc.json')).toBe(true);
expect(tree.exists('/apps/api/.eslintrc.json')).toBe(true);
expect(tree.exists('/libs/api-interfaces/.eslintrc.json')).toBe(true);
});
}); });

View File

@ -20,6 +20,8 @@ function createHost(tree: Tree): workspaces.WorkspaceHost {
}, },
async isDirectory(path: string): Promise<boolean> { async isDirectory(path: string): Promise<boolean> {
// approximate a directory check // approximate a directory check
// special case needed when testing wrapped schematics
if (path === '/') return true;
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
}, },
async isFile(path: string): Promise<boolean> { async isFile(path: string): Promise<boolean> {