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 {
wrapAngularDevkitSchematic,
overrideCollectionResolutionForTesting,
mockSchematicsForTesting,
NxScopedHost,
} from '@nrwl/tao/src/commands/ngcli-adapter';

View File

@ -289,13 +289,7 @@ export class NxScopedHost extends virtualFs.ScopedHost<any> {
actualConfigFileName: any;
isNewFormat: boolean;
}> {
const p = path.toString();
if (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
) {
if (isWorkspaceConfigPath(path)) {
return super.exists('/angular.json' as any).pipe(
switchMap((isAngularJson) => {
const actualConfigFileName = isAngularJson
@ -340,7 +334,7 @@ export class NxScopedHostForMigrations extends NxScopedHost {
}
read(path: Path): Observable<FileBuffer> {
if (this.isWorkspaceConfig(path)) {
if (isWorkspaceConfigPath(path)) {
return super.read(path).pipe(map(processConfigWhenReading));
} else {
return super.read(path);
@ -348,22 +342,12 @@ export class NxScopedHostForMigrations extends NxScopedHost {
}
write(path: Path, content: FileBuffer) {
if (this.isWorkspaceConfig(path)) {
if (isWorkspaceConfigPath(path)) {
return super.write(path, processConfigWhenWriting(content));
} else {
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 {
@ -372,46 +356,77 @@ export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
}
read(path: Path): Observable<FileBuffer> {
return this.context(path).pipe(
switchMap((r) => {
// if it is a workspace config, handle it in a special way
if (r.isWorkspaceConfig) {
const match = this.host
.listChanges()
.find(
(f) => f.path == 'workspace.json' || f.path == 'angular.json'
);
if (isWorkspaceConfigPath(path)) {
const match = findWorkspaceConfigFileChange(this.host);
// no match, default to existing behavior
if (!match) {
return super.read(path);
}
// no match, default to existing behavior
if (!match) {
return super.read(path);
}
// we try to format it, if it changes, return it, otherwise return the original change
try {
const w = JSON.parse(Buffer.from(match.content).toString());
const formatted = toOldFormatOrNull(w);
return of(
formatted
? Buffer.from(JSON.stringify(formatted, null, 2))
: Buffer.from(match.content)
);
} catch (e) {
return 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);
}
})
);
// we try to format it, if it changes, return it, otherwise return the original change
try {
const w = JSON.parse(Buffer.from(match.content).toString());
const formatted = toOldFormatOrNull(w);
return of(
formatted
? Buffer.from(JSON.stringify(formatted, null, 2))
: Buffer.from(match.content)
);
} catch (e) {
return super.read(path);
}
} else {
// found a matching change in the host
const match = findMatchingFileChange(this.host, path);
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) {
@ -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(
collectionName: string,
generatorName: string
) {
return async (host: Tree, generatorOptions: { [k: string]: any }) => {
if (
mockedSchematics &&
mockedSchematics[`${collectionName}:${generatorName}`]
) {
return await mockedSchematics[`${collectionName}:${generatorName}`](
host,
generatorOptions
);
}
const emptyLogger = {
log: (e) => {},
info: (e) => {},
@ -694,6 +775,19 @@ export function wrapAngularDevkitSchematic(
defaults: false,
};
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 schematic = collection.createSchematic(generatorName, true);
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 { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter';
import { presetGenerator } from './preset';
import * as path from 'path';
describe('preset', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
describe('--preset', () => {
// TODO: reenable. This doesn't work because wrapAngularDevkit uses the fs
xdescribe('angular', () => {
it('should create files (preset = angular)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'angular',
cli: 'nx',
});
expect(tree.children('apps/proj')).toMatchSnapshot();
expect(tree.children('apps/proj/src/')).toMatchSnapshot();
expect(tree.children('apps/proj/src/app')).toMatchSnapshot();
console.log(tree.children(''));
expect(
JSON.parse(tree.read('/workspace.json').toString()).cli
.defaultCollection
).toBe('@nrwl/angular');
});
});
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'
);
});
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'
),
'@nrwl/nest': path.join(__dirname, '../../../../nest/collection.json'),
'@nrwl/node': path.join(__dirname, '../../../../node/collection.json'),
'@nrwl/jest': path.join(__dirname, '../../../../jest/collection.json'),
'@nrwl/cypress': path.join(
__dirname,
'../../../../cypress/collection.json'
),
'@nrwl/express': path.join(
__dirname,
'../../../../express/collection.json'
),
});
});
// it('should create files (preset = react)', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'proj', preset: 'react' },
// tree
// );
// expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true);
// expect(
// JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection
// ).toBe('@nrwl/react');
// });
//
//
// it('should create files (preset = next)', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'proj', preset: 'next' },
// tree
// );
// expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true);
// expect(
// JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection
// ).toBe('@nrwl/next');
// });
//
// describe('--preset angular-nest', () => {
// it('should create files', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'proj', preset: 'angular-nest' },
// tree
// );
// expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true);
// expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true);
// expect(
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')
// ).toBe(true);
// });
//
// it('should work with unnormalized names', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'myProj', preset: 'angular-nest' },
// tree
// );
//
// expect(tree.exists('/apps/my-proj/src/app/app.component.ts')).toBe(true);
// expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true);
// expect(
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')
// ).toBe(true);
// });
// });
//
// describe('--preset react-express', () => {
// it('should create files', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'proj', preset: 'react-express' },
// tree
// );
// 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);
// });
//
// it('should work with unnormalized names', async () => {
// const tree = await runSchematic(
// 'preset',
// { name: 'myProj', preset: 'react-express' },
// tree
// );
//
// expect(tree.exists('/apps/my-proj/src/app/app.tsx')).toBe(true);
// expect(
// tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')
// ).toBe(true);
// });
// });
afterEach(() => {
overrideCollectionResolutionForTesting(null);
});
it('should create files (preset = angular)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'angular',
cli: 'nx',
});
expect(tree.children('apps/proj')).toMatchSnapshot();
expect(tree.children('apps/proj/src/')).toMatchSnapshot();
expect(tree.children('apps/proj/src/app')).toMatchSnapshot();
console.log(tree.children(''));
expect(
JSON.parse(tree.read('/workspace.json').toString()).cli.defaultCollection
).toBe('@nrwl/angular');
});
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 () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'react',
style: 'css',
linter: 'eslint',
cli: 'nx',
});
expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true);
expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
'@nrwl/react'
);
});
it('should create files (preset = next)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'next',
style: 'css',
linter: 'eslint',
cli: 'nx',
});
expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true);
expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe(
'@nrwl/next'
);
});
it('should create files (preset = angular-nest)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'angular-nest',
style: 'css',
linter: 'eslint',
cli: 'nx',
});
expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true);
expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true);
expect(tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')).toBe(
true
);
});
it('should create files (preset react-express)', async () => {
await presetGenerator(tree, {
name: 'proj',
preset: 'react-express',
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> {
// 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;
},
async isFile(path: string): Promise<boolean> {