feat(core): add migration to opt out existing apps from new test module teardown behavior (#43353)

Since the `destroyAfterEach` teardown behavior is enabled by default now, existing tests that depended on the old behavior can start to fail. These changes add an automated migration that explicitly adds `destroyAfterEach: false` to existing tests.

The migration works by looking for `initTestEnvironment` calls across the entire app and adding the flag to them. If no calls were found, the migration will add the flag to all `configureTestingModule` and `withModule` calls instead.

PR Close #43353
This commit is contained in:
Kristiyan Kostadinov
2021-10-02 10:57:15 +02:00
committed by Dylan Hunn
parent 94ba59bc9d
commit 81c7eb813c
11 changed files with 1770 additions and 1 deletions
+1
View File
@@ -30,6 +30,7 @@ pkg_npm(
"//packages/core/schematics/migrations/router-preserve-query-params",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
"//packages/core/schematics/migrations/testbed-teardown",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
"//packages/core/schematics/migrations/undecorated-classes-with-di",
"//packages/core/schematics/migrations/wait-for-async",
+6 -1
View File
@@ -104,6 +104,11 @@
"version": "13.0.0-beta",
"description": "Migrates `[routerLink]=\"\"` in templates to `[routerLink]=\"[]\"` because these links are likely intended to route to the current page with updated fragment/query params.",
"factory": "./migrations/router-link-empty-expression/index"
},
"migration-v13-testbed-teardown": {
"version": "13.0.0-beta",
"description": "In Angular version 13, the `teardown` flag in `TestBed` will be enabled by default. This migration automatically opts out existing apps from the new teardown behavior.",
"factory": "./migrations/testbed-teardown/index"
}
}
}
}
@@ -24,6 +24,7 @@ ts_library(
"//packages/core/schematics/migrations/renderer-to-renderer2",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
"//packages/core/schematics/migrations/testbed-teardown",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/google3",
"//packages/core/schematics/migrations/wait-for-async",
@@ -0,0 +1,72 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Replacement, RuleFailure, Rules} from 'tslint';
import ts from 'typescript';
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, InitTestEnvironmentAnalysis, migrateInitTestEnvironment, migrateTestModuleMetadataLiteral} from '../testbed-teardown/util';
/** TSLint rule that adds the `teardown` flag to `TestBed` calls. */
export class Rule extends Rules.TypedRule {
private _analysis = new Map<ts.Program, InitTestEnvironmentAnalysis>();
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const typeChecker = program.getTypeChecker();
const printer = ts.createPrinter();
let initTestEnvironmentResult = this._analysis.get(program);
// The analysis for `initTestEnvironment` calls only needs to run once per program.
if (!initTestEnvironmentResult) {
const sourceFiles = program.getSourceFiles().filter(
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
initTestEnvironmentResult = findInitTestEnvironmentCalls(typeChecker, sourceFiles);
this._analysis.set(program, initTestEnvironmentResult);
}
return this._migrateFile(typeChecker, printer, sourceFile, initTestEnvironmentResult);
}
private _migrateFile(
typeChecker: ts.TypeChecker, printer: ts.Printer, sourceFile: ts.SourceFile,
initTestEnvironmentResult: InitTestEnvironmentAnalysis): RuleFailure[] {
const failures: RuleFailure[] = [];
// If we identified at least one call to `initTestEnvironment` (can be migrated or unmigrated),
// we don't need to migrate `configureTestingModule` or `withModule` calls, because they'll take
// the default teardown behavior from the environment. This is preferrable, because it'll result
// in the least number of changes to users' code.
if (initTestEnvironmentResult.totalCalls > 0) {
// Migrate all of the unmigrated calls `initTestEnvironment` in this file. This could be zero
// if the user has already opted into the new teardown behavior themselves.
initTestEnvironmentResult.callsToMigrate.forEach(call => {
// This analysis is global so we need to check that the call is within this file.
if (call.getSourceFile() === sourceFile) {
failures.push(this._getFailure(call, migrateInitTestEnvironment, printer));
}
});
} else {
// Otherwise migrate the metadata passed into the `configureTestingModule` and `withModule`
// calls. This scenario is less likely, but it could happen if `initTestEnvironment` has been
// abstracted away or is inside a .js file.
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(literal => {
failures.push(this._getFailure(literal, migrateTestModuleMetadataLiteral, printer));
});
}
return failures;
}
private _getFailure<T extends ts.Node>(node: T, migrator: (node: T) => T, printer: ts.Printer) {
const sourceFile = node.getSourceFile();
const migrated = migrator(node);
const replacementText = printer.printNode(ts.EmitHint.Unspecified, migrated, sourceFile);
return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(), 'Teardown behavior has to be configured.',
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
}
}
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "testbed-teardown",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)
@@ -0,0 +1,32 @@
## TestBed teardown behavior migration
In Angular version 13, the `teardown` flag in `TestBed` will be enabled by default. This migration
automatically opts out existing apps from the new teardown behavior. It works by looking through
the entire project for `initTestEnvironment` calls and adding the `teardown` flag to them.
If no `initTestEnvironment` calls were found, the migration will add the `teardown` flag to all
`configureTestingModule` and `withModule` calls instead.
#### Before
```ts
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
```
#### After
```ts
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
```
@@ -0,0 +1,75 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {relative} from 'path';
import ts from 'typescript';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, migrateInitTestEnvironment, migrateTestModuleMetadataLiteral} from './util';
/** Migration that adds the `teardown` flag to `TestBed` calls. */
export default function(): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot add `teardown` flag to `TestBed`.');
}
for (const tsconfigPath of allPaths) {
runTestbedTeardownMigration(tree, tsconfigPath, basePath);
}
};
}
function runTestbedTeardownMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
const typeChecker = program.getTypeChecker();
const sourceFiles =
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
const initTestEnvironmentResult = findInitTestEnvironmentCalls(typeChecker, sourceFiles);
const printer = ts.createPrinter();
// If we identified at least one call to `initTestEnvironment` (can be migrated or unmigrated),
// we don't need to migrate `configureTestingModule` or `withModule` calls, because they'll take
// the default teardown behavior from the environment. This is preferrable, because it'll result
// in the least number of changes to users' code.
if (initTestEnvironmentResult.totalCalls > 0) {
// Migrate all of the unmigrated calls `initTestEnvironment`. This could be zero
// if the user has already opted into the new teardown behavior themselves.
initTestEnvironmentResult.callsToMigrate.forEach(call => {
migrate(call, migrateInitTestEnvironment, tree, basePath, printer);
});
} else {
// Otherwise migrate the metadata passed into the `configureTestingModule` and `withModule`
// calls. This scenario is less likely, but it could happen if `initTestEnvironment` has been
// abstracted away or is inside a .js file.
sourceFiles.forEach(sourceFile => {
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(literal => {
migrate(literal, migrateTestModuleMetadataLiteral, tree, basePath, printer);
});
});
}
}
function migrate<T extends ts.Node>(
node: T, migrator: (node: T) => T, tree: Tree, basePath: string, printer: ts.Printer) {
const migrated = migrator(node);
const update = tree.beginUpdate(relative(basePath, node.getSourceFile().fileName));
update.remove(node.getStart(), node.getWidth());
update.insertRight(
node.getStart(), printer.printNode(ts.EmitHint.Unspecified, migrated, node.getSourceFile()));
tree.commitUpdate(update);
}
@@ -0,0 +1,193 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import ts from 'typescript';
import {getImportSpecifier} from '../../utils/typescript/imports';
import {isReferenceToImport} from '../../utils/typescript/symbol';
/** Result of a full-program analysis looking for `initTestEnvironment` calls. */
export interface InitTestEnvironmentAnalysis {
/** Total number of calls that were found. */
totalCalls: number;
/** Calls that need to be migrated. */
callsToMigrate: ts.CallExpression[];
}
/** Finds the `initTestEnvironment` calls that need to be migrated. */
export function findInitTestEnvironmentCalls(
typeChecker: ts.TypeChecker, allSourceFiles: ts.SourceFile[]): InitTestEnvironmentAnalysis {
const callsToMigrate = new Set<ts.CallExpression>();
let totalCalls = 0;
allSourceFiles.forEach(sourceFile => {
sourceFile.forEachChild(function walk(node: ts.Node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.name) &&
node.expression.name.text === 'initTestEnvironment' &&
isTestBedAccess(typeChecker, node.expression)) {
totalCalls++;
if (shouldMigrateInitTestEnvironment(node)) {
callsToMigrate.add(node);
}
}
node.forEachChild(walk);
});
});
return {
// Sort the nodes so that they will be migrated in reverse source order (nodes at the end of
// the file are migrated first). This avoids issues where a migrated node will offset the
// bounds of all nodes that come after it. Note that the nodes here are from all of the
// passed in source files, but that doesn't matter since the later nodes will still appear
// after the earlier ones.
callsToMigrate: sortInReverseSourceOrder(Array.from(callsToMigrate)),
totalCalls
};
}
/** Finds the `configureTestingModule` and `withModule` calls that need to be migrated. */
export function findTestModuleMetadataNodes(
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
const testModuleMetadataLiterals = new Set<ts.ObjectLiteralExpression>();
const withModuleImport = getImportSpecifier(sourceFile, '@angular/core/testing', 'withModule');
sourceFile.forEachChild(function walk(node: ts.Node) {
if (ts.isCallExpression(node)) {
const isConfigureTestingModuleCall = ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.name) &&
node.expression.name.text === 'configureTestingModule' &&
isTestBedAccess(typeChecker, node.expression) && shouldMigrateModuleConfigCall(node);
const isWithModuleCall = withModuleImport && ts.isIdentifier(node.expression) &&
isReferenceToImport(typeChecker, node.expression, withModuleImport) &&
shouldMigrateModuleConfigCall(node);
if (isConfigureTestingModuleCall || isWithModuleCall) {
testModuleMetadataLiterals.add(node.arguments[0] as ts.ObjectLiteralExpression);
}
}
node.forEachChild(walk);
});
// Sort the nodes so that they will be migrated in reverse source order (nodes at the end of
// the file are migrated first). This avoids issues where a migrated node will offset the
// bounds of all nodes that come after it.
return sortInReverseSourceOrder(Array.from(testModuleMetadataLiterals));
}
/** Migrates a call to `TestBed.initTestEnvironment`. */
export function migrateInitTestEnvironment(node: ts.CallExpression): ts.CallExpression {
const literalProperties: ts.ObjectLiteralElementLike[] = [];
if (node.arguments.length > 2) {
if (isFunction(node.arguments[2])) {
// If the last argument is a function, add the function as the `aotSummaries` property.
literalProperties.push(ts.createPropertyAssignment('aotSummaries', node.arguments[2]));
} else if (ts.isObjectLiteralExpression(node.arguments[2])) {
// If the property is an object literal, copy over all the properties.
literalProperties.push(...node.arguments[2].properties);
}
}
// Finally push the teardown object so that it appears last.
literalProperties.push(createTeardownAssignment());
return ts.createCall(
node.expression, node.typeArguments,
[...node.arguments.slice(0, 2), ts.createObjectLiteral(literalProperties, true)]);
}
/** Migrates an object literal that is passed into `configureTestingModule` or `withModule`. */
export function migrateTestModuleMetadataLiteral(node: ts.ObjectLiteralExpression):
ts.ObjectLiteralExpression {
return ts.createObjectLiteral(
[...node.properties, createTeardownAssignment()], node.properties.length > 0);
}
/** Returns whether a property access points to `TestBed`. */
function isTestBedAccess(typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean {
const symbolName = typeChecker.getTypeAtLocation(node.expression)?.getSymbol()?.getName();
return symbolName === 'TestBed' || symbolName === 'TestBedStatic';
}
/** Whether a call to `initTestEnvironment` should be migrated. */
function shouldMigrateInitTestEnvironment(node: ts.CallExpression): boolean {
// If there is no third argument, we definitely have to migrate it.
if (node.arguments.length === 2) {
return true;
}
// This is technically a type error so we shouldn't mess with it.
if (node.arguments.length < 2) {
return false;
}
// Otherwise we need to figure out if the `teardown` flag is set on the last argument.
const lastArg = node.arguments[2];
// Note: the checks below will identify something like `initTestEnvironment(..., ..., {})`,
// but they'll ignore a variable being passed in as the last argument like `const config = {};
// initTestEnvironment(..., ..., config)`. While we can resolve the variable to its declaration
// using `typeChecker.getTypeAtLocation(lastArg).getSymbol()?.valueDeclaration`, we deliberately
// don't, because it introduces some complexity and we may end up breaking user code. E.g.
// the `config` from the example above may be passed in to other functions or the `teardown`
// flag could be added later on by a function call.
// If the argument is an object literal and there are no
// properties called `teardown`, we have to migrate it.
if (isObjectLiteralWithoutTeardown(lastArg)) {
return true;
}
// If the last argument is an `aotSummaries` function, we also have to migrate.
if (isFunction(lastArg)) {
return true;
}
// Otherwise don't migrate if we couldn't identify the last argument.
return false;
}
/**
* Whether a call to a module configuration function should be migrated. This covers
* `TestBed.configureTestingModule` and `withModule` since they both accept `TestModuleMetadata`
* as their first argument.
*/
function shouldMigrateModuleConfigCall(node: ts.CallExpression): node is ts.CallExpression&
{arguments: [ts.ObjectLiteralExpression, ...ts.Expression[]]} {
return node.arguments.length > 0 && isObjectLiteralWithoutTeardown(node.arguments[0]);
}
/** Returns whether a node is a function literal. */
function isFunction(node: ts.Node): node is ts.ArrowFunction|ts.FunctionExpression|
ts.FunctionDeclaration {
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) ||
ts.isFunctionDeclaration(node);
}
/** Checks whether a node is an object literal that doesn't contain a property called `teardown`. */
function isObjectLiteralWithoutTeardown(node: ts.Node): node is ts.ObjectLiteralExpression {
return ts.isObjectLiteralExpression(node) && !node.properties.find(prop => {
return prop.name?.getText() === 'teardown';
});
}
/** Creates a teardown configuration property assignment. */
function createTeardownAssignment(): ts.PropertyAssignment {
// `teardown: {destroyAfterEach: false}`
return ts.createPropertyAssignment(
'teardown',
ts.createObjectLiteral([ts.createPropertyAssignment('destroyAfterEach', ts.createFalse())]));
}
/** Sorts an array of AST nodes in reverse source order. */
function sortInReverseSourceOrder<T extends ts.Node>(nodes: T[]): T[] {
return nodes.sort((a, b) => b.getEnd() - a.getEnd());
}
@@ -25,6 +25,7 @@ ts_library(
"//packages/core/schematics/migrations/router-preserve-query-params",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
"//packages/core/schematics/migrations/testbed-teardown",
"//packages/core/schematics/migrations/undecorated-classes-with-decorated-fields",
"//packages/core/schematics/migrations/undecorated-classes-with-di",
"//packages/core/schematics/migrations/wait-for-async",
@@ -0,0 +1,707 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync, writeFileSync} from 'fs';
import {dirname, join} from 'path';
import * as shx from 'shelljs';
import {Configuration, Linter} from 'tslint';
describe('Google3 TestBed teardown TSLint rule', () => {
const rulesDirectory = dirname(require.resolve('../../migrations/google3/testbedTeardownRule'));
let tmpDir: string;
beforeEach(() => {
tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test');
shx.mkdir('-p', tmpDir);
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
// Note that the declarations here are a little more convoluted than they need to be. It's
// because they've been copied over directly from `node_modules/@angular/core/testing.d.ts`,
// except for removing some methods that are irrelevant to these tests.
writeFile('testing.d.ts', `
export declare const getTestBed: () => TestBed;
export declare interface TestBed {
initTestEnvironment(ngModule: any, platform: any, options?: any): void;
configureTestingModule(moduleDef: any): void;
}
export declare const TestBed: TestBedStatic;
export declare interface TestBedStatic {
new (...args: any[]): TestBed;
initTestEnvironment(ngModule: any, platform: any, options?: any): TestBed;
configureTestingModule(moduleDef: any): TestBedStatic;
}
export declare function withModule(moduleDef: any, fn: Function): () => any;
`);
writeFile('tsconfig.json', JSON.stringify({
compilerOptions: {
module: 'es2015',
baseUrl: './',
paths: {
'@angular/core/testing': ['testing.d.ts'],
}
},
}));
});
afterEach(() => shx.rm('-r', tmpDir));
function runTSLint(fix: boolean) {
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
const config = Configuration.parseConfigFile({rules: {'testbedTeardown': true}});
program.getRootFileNames().forEach(fileName => {
linter.lint(fileName, program.getSourceFile(fileName)!.getFullText(), config);
});
return linter;
}
function writeFile(fileName: string, content: string) {
writeFileSync(join(tmpDir, fileName), content);
}
function getFile(fileName: string) {
return readFileSync(join(tmpDir, fileName), 'utf8');
}
function stripWhitespace(contents: string) {
return contents.replace(/\s/g, '');
}
it('should flag initTestEnvironment calls', () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
const linter = runTSLint(false);
const failures = linter.getResult().failures.map(failure => failure.getFailure());
expect(failures.length).toBe(1);
expect(failures[0]).toMatch(/Teardown behavior has to be configured\./);
});
it('should flag configureTestingModule and withModule calls', () => {
writeFile('/index.ts', `
import { Component } from '@angular/core';
import { TestBed, withModule } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
it('should also work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
const linter = runTSLint(false);
const failures = linter.getResult().failures.map(failure => failure.getFailure());
expect(failures.length).toBe(2);
expect(failures[0]).toMatch(/Teardown behavior has to be configured\./);
expect(failures[1]).toMatch(/Teardown behavior has to be configured\./);
});
it('should migrate calls to initTestEnvironment', () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment going through getTestBed()', () => {
writeFile('/index.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment going through a variable', () => {
writeFile('/index.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
const tb = getTestBed();
tb.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
tb.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate not calls to initTestEnvironment that already specify a teardown behavior',
() => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: {destroyAfterEach: true}
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: {destroyAfterEach: true}
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries as an arrow function',
() => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), () => [Foo]);
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo],
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries as an anonymous function',
() => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), function() {
return [Foo];
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: function() {
return [Foo];
},
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries via an object literal',
() => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo]
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo],
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate initTestEnvironment calls across multiple files', () => {
writeFile('/test-init.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
writeFile('/other-test-init.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/test-init.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
expect(stripWhitespace(getFile('/other-test-init.ts'))).toContain(stripWhitespace(`
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should not migrate calls to initTestEnvironment that pass in a variable', () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
const config = {aotSummaries: () => []};
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), config);
`);
runTSLint(true);
const content = stripWhitespace(getFile('/index.ts'));
expect(content).toContain(stripWhitespace(`const config = {aotSummaries: () => []};`));
expect(content).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), config);
`));
});
it('should not migrate invalid initTestEnvironment calls', () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
TestBed.initTestEnvironment();
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts')))
.toContain(stripWhitespace(`TestBed.initTestEnvironment();`));
});
it('should not migrate calls to initTestEnvironment not coming from Angular', () => {
writeFile('/index.ts', `
import { TestBed } from '@not-angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts')))
.toContain(stripWhitespace(
`TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());`));
});
it('should migrate calls to initTestEnvironment when TestBed is aliased', () => {
writeFile('/index.ts', `
import { TestBed as AliasOfTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
AliasOfTestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
AliasOfTestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate withModule calls', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should not migrate withModule calls that already pass in the teardown flag', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', withModule({
teardown: {destroyAfterEach: true},
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
teardown: {destroyAfterEach: true},
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should not migrate withModule calls that do not come from Angular', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
function withModule(...args: any[]) {}
@Component({template: ''})
class Comp {}
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should migrate aliased withModule calls', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule as aliasOfWithModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', aliasOfWithModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', aliasOfWithModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should migrate configureTestingModule calls', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should migrate multiple configureTestingModule calls within the same file', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
@Component({template: ''})
class BetterComp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
it('should work better', () => {
TestBed.configureTestingModule({
declarations: [BetterComp]
});
const fixture = TestBed.createComponent(BetterComp);
fixture.detectChanges();
});
`);
runTSLint(true);
const content = stripWhitespace(getFile('/index.spec.ts'));
expect(content).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
expect(content).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [BetterComp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should migrate configureTestingModule calls through getTestBed()', () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { getTestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
getTestBed().configureTestingModule({
declarations: [Comp]
});
const fixture = getTestBed().createComponent(Comp);
fixture.detectChanges();
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
getTestBed().configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should not migrate configureTestingModule calls that already pass in the teardown flag',
() => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
teardown: {destroyAfterEach: true},
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
`);
runTSLint(true);
expect(stripWhitespace(getFile('/index.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
teardown: {destroyAfterEach: true},
declarations: [Comp]
});
`));
});
it('should not migrate configureTestingModule or withModule calls if initTestEnvironment was migrated in another file',
() => {
writeFile('/test-init.ts', `
import { TestBed, withModule } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
writeFile('/comp.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
it('should also work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
runTSLint(true);
expect(stripWhitespace(getFile('/test-init.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
expect(stripWhitespace(getFile('/comp.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp]
});
`));
expect(stripWhitespace(getFile('/comp.spec.ts'))).toContain(stripWhitespace(`
it('should also work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
});
@@ -0,0 +1,664 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
import {HostTree} from '@angular-devkit/schematics';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import * as shx from 'shelljs';
describe('TestBed teardown migration', () => {
let runner: SchematicTestRunner;
let host: TempScopedNodeJsSyncHost;
let tree: UnitTestTree;
let tmpDirPath: string;
let previousWorkingDir: string;
beforeEach(() => {
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
host = new TempScopedNodeJsSyncHost();
tree = new UnitTestTree(new HostTree(host));
writeFile('/tsconfig.json', JSON.stringify({
compilerOptions: {
lib: ['es2015'],
strictNullChecks: true,
},
}));
writeFile('/angular.json', JSON.stringify({
version: 1,
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
}));
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
// Note that the declarations here are a little more convoluted than they need to be. It's
// because they've been copied over directly from `node_modules/@angular/core/testing.d.ts`,
// except for removing some methods that are irrelevant to these tests.
writeFile('/node_modules/@angular/core/testing.d.ts', `
export declare const getTestBed: () => TestBed;
export declare interface TestBed {
initTestEnvironment(ngModule: any, platform: any, options?: any): void;
configureTestingModule(moduleDef: any): void;
}
export declare const TestBed: TestBedStatic;
export declare interface TestBedStatic {
new (...args: any[]): TestBed;
initTestEnvironment(ngModule: any, platform: any, options?: any): TestBed;
configureTestingModule(moduleDef: any): TestBedStatic;
}
export declare function withModule(moduleDef: any, fn: Function): () => any;
`);
previousWorkingDir = shx.pwd();
tmpDirPath = getSystemPath(host.root);
// Switch into the temporary directory path. This allows us to run
// the schematic against our custom unit test tree.
shx.cd(tmpDirPath);
});
afterEach(() => {
shx.cd(previousWorkingDir);
shx.rm('-r', tmpDirPath);
});
it('should migrate calls to initTestEnvironment', async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment going through getTestBed()', async () => {
writeFile('/index.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment going through a variable', async () => {
writeFile('/index.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
const tb = getTestBed();
tb.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
tb.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate not calls to initTestEnvironment that already specify a teardown behavior',
async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: {destroyAfterEach: true}
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: {destroyAfterEach: true}
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries as an arrow function',
async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), () => [Foo]);
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo],
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries as an anonymous function',
async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), function() {
return [Foo];
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: function() {
return [Foo];
},
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate calls to initTestEnvironment that pass in aotSummaries via an object literal',
async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
class Foo {}
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo]
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
aotSummaries: () => [Foo],
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate initTestEnvironment calls across multiple files', async () => {
writeFile('/test-init.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
writeFile('/other-test-init.ts', `
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/test-init.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
expect(stripWhitespace(tree.readContent('/other-test-init.ts'))).toContain(stripWhitespace(`
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should not migrate calls to initTestEnvironment that pass in a variable', async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
const config = {aotSummaries: () => []};
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), config);
`);
await runMigration();
const content = stripWhitespace(tree.readContent('/index.ts'));
expect(content).toContain(stripWhitespace(`const config = {aotSummaries: () => []};`));
expect(content).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), config);
`));
});
it('should not migrate invalid initTestEnvironment calls', async () => {
writeFile('/index.ts', `
import { TestBed } from '@angular/core/testing';
TestBed.initTestEnvironment();
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts')))
.toContain(stripWhitespace(`TestBed.initTestEnvironment();`));
});
it('should not migrate calls to initTestEnvironment not coming from Angular', async () => {
writeFile('/index.ts', `
import { TestBed } from '@not-angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts')))
.toContain(stripWhitespace(
`TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());`));
});
it('should migrate calls to initTestEnvironment when TestBed is aliased', async () => {
writeFile('/index.ts', `
import { TestBed as AliasOfTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
AliasOfTestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
AliasOfTestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
});
it('should migrate withModule calls', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should not migrate withModule calls that already pass in the teardown flag', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', withModule({
teardown: {destroyAfterEach: true},
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
teardown: {destroyAfterEach: true},
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should not migrate withModule calls that do not come from Angular', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
function withModule(...args: any[]) {}
@Component({template: ''})
class Comp {}
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should migrate aliased withModule calls', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { withModule as aliasOfWithModule, TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', aliasOfWithModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
it('should work', aliasOfWithModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
it('should migrate configureTestingModule calls', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should migrate multiple configureTestingModule calls within the same file', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
@Component({template: ''})
class BetterComp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
it('should work better', () => {
TestBed.configureTestingModule({
declarations: [BetterComp]
});
const fixture = TestBed.createComponent(BetterComp);
fixture.detectChanges();
});
`);
await runMigration();
const content = stripWhitespace(tree.readContent('/index.spec.ts'));
expect(content).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
expect(content).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [BetterComp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should migrate configureTestingModule calls through getTestBed()', async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { getTestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
getTestBed().configureTestingModule({
declarations: [Comp]
});
const fixture = getTestBed().createComponent(Comp);
fixture.detectChanges();
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
getTestBed().configureTestingModule({
declarations: [Comp],
teardown: {destroyAfterEach: false}
});
`));
});
it('should not migrate configureTestingModule calls that already pass in the teardown flag',
async () => {
writeFile('/index.spec.ts', `
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
teardown: {destroyAfterEach: true},
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/index.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
teardown: {destroyAfterEach: true},
declarations: [Comp]
});
`));
});
it('should not migrate configureTestingModule or withModule calls if initTestEnvironment was migrated in another file',
async () => {
writeFile('/test-init.ts', `
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
`);
writeFile('/comp.spec.ts', `
import { Component } from '@angular/core';
import { TestBed, withModule } from '@angular/core/testing';
@Component({template: ''})
class Comp {}
it('should work', () => {
TestBed.configureTestingModule({
declarations: [Comp]
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
});
it('should also work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`);
await runMigration();
expect(stripWhitespace(tree.readContent('/test-init.ts'))).toContain(stripWhitespace(`
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
});
`));
expect(stripWhitespace(tree.readContent('/comp.spec.ts'))).toContain(stripWhitespace(`
TestBed.configureTestingModule({
declarations: [Comp]
});
`));
expect(stripWhitespace(tree.readContent('/comp.spec.ts'))).toContain(stripWhitespace(`
it('should also work', withModule({
declarations: [Comp],
}, () => {
TestBed.createComponent(Comp);
}));
`));
});
function writeFile(filePath: string, contents: string) {
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
}
function runMigration() {
return runner.runSchematicAsync('migration-v13-testbed-teardown', {}, tree).toPromise();
}
function stripWhitespace(contents: string) {
return contents.replace(/\s/g, '');
}
});