From 81c7eb813c27f08d2d640f34e165a1b5e487bac2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 2 Oct 2021 10:57:15 +0200 Subject: [PATCH] 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 --- packages/core/schematics/BUILD.bazel | 1 + packages/core/schematics/migrations.json | 7 +- .../schematics/migrations/google3/BUILD.bazel | 1 + .../migrations/google3/testbedTeardownRule.ts | 72 ++ .../migrations/testbed-teardown/BUILD.bazel | 18 + .../migrations/testbed-teardown/README.md | 32 + .../migrations/testbed-teardown/index.ts | 75 ++ .../migrations/testbed-teardown/util.ts | 193 +++++ packages/core/schematics/test/BUILD.bazel | 1 + .../test/google3/testbed_teardown_spec.ts | 707 ++++++++++++++++++ .../schematics/test/testbed_teardown_spec.ts | 664 ++++++++++++++++ 11 files changed, 1770 insertions(+), 1 deletion(-) create mode 100644 packages/core/schematics/migrations/google3/testbedTeardownRule.ts create mode 100644 packages/core/schematics/migrations/testbed-teardown/BUILD.bazel create mode 100644 packages/core/schematics/migrations/testbed-teardown/README.md create mode 100644 packages/core/schematics/migrations/testbed-teardown/index.ts create mode 100644 packages/core/schematics/migrations/testbed-teardown/util.ts create mode 100644 packages/core/schematics/test/google3/testbed_teardown_spec.ts create mode 100644 packages/core/schematics/test/testbed_teardown_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index a1397b8bb0a..d7611840e0f 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 81ed065a9df..5c85a231027 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -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" } } -} \ No newline at end of file +} diff --git a/packages/core/schematics/migrations/google3/BUILD.bazel b/packages/core/schematics/migrations/google3/BUILD.bazel index 74bfee1edb4..3a71b955b48 100644 --- a/packages/core/schematics/migrations/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/google3/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/migrations/google3/testbedTeardownRule.ts b/packages/core/schematics/migrations/google3/testbedTeardownRule.ts new file mode 100644 index 00000000000..b30da384d84 --- /dev/null +++ b/packages/core/schematics/migrations/google3/testbedTeardownRule.ts @@ -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(); + + 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(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)); + } +} diff --git a/packages/core/schematics/migrations/testbed-teardown/BUILD.bazel b/packages/core/schematics/migrations/testbed-teardown/BUILD.bazel new file mode 100644 index 00000000000..4b1dde93eca --- /dev/null +++ b/packages/core/schematics/migrations/testbed-teardown/BUILD.bazel @@ -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", + ], +) diff --git a/packages/core/schematics/migrations/testbed-teardown/README.md b/packages/core/schematics/migrations/testbed-teardown/README.md new file mode 100644 index 00000000000..e4742e9acda --- /dev/null +++ b/packages/core/schematics/migrations/testbed-teardown/README.md @@ -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 } +}); +``` diff --git a/packages/core/schematics/migrations/testbed-teardown/index.ts b/packages/core/schematics/migrations/testbed-teardown/index.ts new file mode 100644 index 00000000000..898ae1613ac --- /dev/null +++ b/packages/core/schematics/migrations/testbed-teardown/index.ts @@ -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( + 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); +} diff --git a/packages/core/schematics/migrations/testbed-teardown/util.ts b/packages/core/schematics/migrations/testbed-teardown/util.ts new file mode 100644 index 00000000000..3a062afcb98 --- /dev/null +++ b/packages/core/schematics/migrations/testbed-teardown/util.ts @@ -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(); + 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(); + 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(nodes: T[]): T[] { + return nodes.sort((a, b) => b.getEnd() - a.getEnd()); +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index 30f0b69f4f9..ae590010cef 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/test/google3/testbed_teardown_spec.ts b/packages/core/schematics/test/google3/testbed_teardown_spec.ts new file mode 100644 index 00000000000..79d45f84b75 --- /dev/null +++ b/packages/core/schematics/test/google3/testbed_teardown_spec.ts @@ -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); + })); + `)); + }); +}); diff --git a/packages/core/schematics/test/testbed_teardown_spec.ts b/packages/core/schematics/test/testbed_teardown_spec.ts new file mode 100644 index 00000000000..4448fcc2581 --- /dev/null +++ b/packages/core/schematics/test/testbed_teardown_spec.ts @@ -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, ''); + } +});