From 8cbe8122c0f58b13f6335e13e141c3df18bbce20 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 2 Aug 2018 20:28:07 +0200 Subject: [PATCH] build: add test cases for update schematics (#12473) Adds a way to test that the update schematics properly update a developer's application. --- package-lock.json | 2 +- src/lib/schematics/BUILD.bazel | 10 ++- src/lib/schematics/migration.json | 5 -- src/lib/schematics/tsconfig.json | 5 +- .../update/test-cases/index.spec.ts | 76 +++++++++++++++++++ .../v5/ts-class-names_expected_output.ts | 14 ++++ .../test-cases/v5/ts-class-names_input.ts | 14 ++++ src/lib/schematics/update/update.spec.ts | 10 +-- src/lib/schematics/update/update.ts | 70 +++++------------ src/lib/schematics/utils/testing.ts | 36 ++++++++- tslint.json | 5 +- 11 files changed, 173 insertions(+), 74 deletions(-) create mode 100644 src/lib/schematics/update/test-cases/index.spec.ts create mode 100644 src/lib/schematics/update/test-cases/v5/ts-class-names_expected_output.ts create mode 100644 src/lib/schematics/update/test-cases/v5/ts-class-names_input.ts diff --git a/package-lock.json b/package-lock.json index f5f4d1b1747..4525aacca71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "material2-srcs", - "version": "6.4.1", + "version": "6.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/lib/schematics/BUILD.bazel b/src/lib/schematics/BUILD.bazel index 40c45f89c87..f9cfa39202d 100644 --- a/src/lib/schematics/BUILD.bazel +++ b/src/lib/schematics/BUILD.bazel @@ -11,7 +11,7 @@ filegroup( ts_library( name = "schematics", module_name = "@angular/material/schematics", - srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "**/files/**/*"]), + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "update/test-cases/**/*.ts", "**/files/**/*"]), tsconfig = ":tsconfig.json", ) @@ -27,7 +27,7 @@ npm_package( jasmine_node_test( name = "unit_tests", srcs = [":schematics_test_sources"], - data = [":schematics_assets"], + data = [":schematics_assets", ":schematics_test_cases"], deps = [":copy-collection-file", ":copy-migration-file"], ) @@ -39,6 +39,12 @@ ts_library( testonly = True, ) +filegroup( + name = "schematics_test_cases", + srcs = glob(["update/test-cases/**/*_input.ts", "update/test-cases/**/*_expected_output.ts"]), + testonly = True, +) + # Workaround for https://github.com/bazelbuild/rules_typescript/issues/154 genrule( name = "copy-collection-file", diff --git a/src/lib/schematics/migration.json b/src/lib/schematics/migration.json index fae65c8ced6..e3c9030dbd1 100644 --- a/src/lib/schematics/migration.json +++ b/src/lib/schematics/migration.json @@ -11,11 +11,6 @@ "description": "Performs cleanup after ng-update.", "factory": "./update/update#postUpdate", "private": true - }, - "ng-post-post-update": { - "description": "Logs completion message for ng-update after ng-post-update.", - "factory": "./update/update#postPostUpdate", - "private": true } } } diff --git a/src/lib/schematics/tsconfig.json b/src/lib/schematics/tsconfig.json index c35130d2d8d..088bd456d5e 100644 --- a/src/lib/schematics/tsconfig.json +++ b/src/lib/schematics/tsconfig.json @@ -15,7 +15,10 @@ ] }, "exclude": [ + "**/*.spec.ts", + // Exclude template files that will be copied by the schematics. Those are not valid TS. "*/files/**/*", - "**/*spec*" + // Exclude all test-case files because those should not be included in the schematics output. + "update/test-cases/**/*" ] } diff --git a/src/lib/schematics/update/test-cases/index.spec.ts b/src/lib/schematics/update/test-cases/index.spec.ts new file mode 100644 index 00000000000..5b0e5560586 --- /dev/null +++ b/src/lib/schematics/update/test-cases/index.spec.ts @@ -0,0 +1,76 @@ +import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; +import {readFileSync} from 'fs'; +import {createTestApp, migrationCollection, runPostScheduledTasks} from '../../utils/testing'; + +describe('test cases', () => { + + // Module name suffix for data files of the `jasmine_node_test` Bazel rule. + const bazelModuleSuffix = 'angular_material/src/lib/schematics/update/test-cases'; + + /** + * Name of test cases that will be used to verify that update schematics properly update + * a developers application. + */ + const testCases = [ + 'v5/ts-class-names' + ]; + + // Iterates through every test case directory and generates a jasmine test block that will + // verify that the update schematics properly update the test input to the expected output. + testCases.forEach(testCaseName => { + + // Adding the test case files to the data of the `jasmine_node_test` Bazel rule does not mean + // that the files are being copied over to the Bazel bin output. Bazel just patches the NodeJS + // resolve function and maps the module paths to the original file location. Since we + // need to load the content of those test cases, we need to resolve the original file path. + const inputPath = require.resolve(`${bazelModuleSuffix}/${testCaseName}_input.ts`); + const expectedOutputPath = require + .resolve(`${bazelModuleSuffix}/${testCaseName}_expected_output.ts`); + + it(`should apply update schematics to test case: ${testCaseName}`, () => { + const runner = new SchematicTestRunner('schematics', migrationCollection); + + runner.runSchematic('migration-01', {}, createTestAppWithTestCase(inputPath)); + + // Run the scheduled TSLint fix task from the update schematic. This task is responsible for + // identifying outdated code parts and performs the fixes. Since tasks won't run automatically + // within a `SchematicTestRunner`, we manually need to run the scheduled task. + return runPostScheduledTasks(runner, 'tslint-fix').toPromise().then(() => { + expect(readFileContent('projects/material/src/main.ts')) + .toBe(readFileContent(expectedOutputPath)); + }); + }); + }); + + /** Reads the UTF8 content of the specified file. Normalizes the path and ensures that */ + function readFileContent(filePath: string): string { + return readFileSync(filePath, 'utf8'); + } + + /** + * Creates a test app schematic tree that includes the specified test case as TypeScript + * entry point file. Also writes the app tree to a real file system location in order to be + * able to test the tslint fix rules. + */ + function createTestAppWithTestCase(testCaseInputPath: string) { + const tempFileSystemHost = new TempScopedNodeJsSyncHost(); + const appTree = createTestApp(); + + appTree.overwrite('/projects/material/src/main.ts', readFileContent(testCaseInputPath)); + + // Since the TSLint fix task expects all files to be present on the real file system, we + // map every file in the app tree to a temporary location on the file system. + appTree.files.map(f => normalize(f)).forEach(f => { + tempFileSystemHost.sync.write(f, virtualFs.stringToFileBuffer(appTree.readContent(f))); + }); + + // Switch to the new temporary directory because otherwise TSLint cannot read the files. + process.chdir(getSystemPath(tempFileSystemHost.root)); + + return appTree; + } +}); + + diff --git a/src/lib/schematics/update/test-cases/v5/ts-class-names_expected_output.ts b/src/lib/schematics/update/test-cases/v5/ts-class-names_expected_output.ts new file mode 100644 index 00000000000..4b0d6d3789c --- /dev/null +++ b/src/lib/schematics/update/test-cases/v5/ts-class-names_expected_output.ts @@ -0,0 +1,14 @@ +import {CdkConnectedOverlay, CdkOverlayOrigin} from '@angular/cdk/overlay'; +import {CdkObserveContent} from '@angular/cdk/observers'; +import {CdkTrapFocus} from '@angular/cdk/a11y'; +import {FloatLabelType, LabelOptions, MAT_LABEL_GLOBAL_OPTIONS} from '@angular/material'; + +const a = new CdkConnectedOverlay(); +const b = new CdkOverlayOrigin(); +const c = new CdkObserveContent(); +const d = new CdkTrapFocus(); + +const e: FloatLabelType = 'test'; +const f: LabelOptions = 'opt2'; + +const g = {provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: 'test-options'}; diff --git a/src/lib/schematics/update/test-cases/v5/ts-class-names_input.ts b/src/lib/schematics/update/test-cases/v5/ts-class-names_input.ts new file mode 100644 index 00000000000..11c7fe5d9e1 --- /dev/null +++ b/src/lib/schematics/update/test-cases/v5/ts-class-names_input.ts @@ -0,0 +1,14 @@ +import {ConnectedOverlayDirective, OverlayOrigin} from '@angular/cdk/overlay'; +import {ObserveContent} from '@angular/cdk/observers'; +import {FocusTrapDirective} from '@angular/cdk/a11y'; +import {FloatPlaceholderType, PlaceholderOptions, MAT_PLACEHOLDER_GLOBAL_OPTIONS} from '@angular/material'; + +const a = new ConnectedOverlayDirective(); +const b = new OverlayOrigin(); +const c = new ObserveContent(); +const d = new FocusTrapDirective(); + +const e: FloatPlaceholderType = 'test'; +const f: PlaceholderOptions = 'opt2'; + +const g = {provide: MAT_PLACEHOLDER_GLOBAL_OPTIONS, useValue: 'test-options'}; diff --git a/src/lib/schematics/update/update.spec.ts b/src/lib/schematics/update/update.spec.ts index 78ee03b2c6e..f70a5114072 100644 --- a/src/lib/schematics/update/update.spec.ts +++ b/src/lib/schematics/update/update.spec.ts @@ -1,5 +1,5 @@ import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; -import {migrationCollection, createTestApp} from '../utils/testing'; +import {migrationCollection} from '../utils/testing'; describe('material-nav-schematic', () => { let runner: SchematicTestRunner; @@ -8,12 +8,4 @@ describe('material-nav-schematic', () => { runner = new SchematicTestRunner('schematics', migrationCollection); }); - it('should remove the temp directory', () => { - const tree = runner.runSchematic('migration-01', {}, createTestApp()); - const files = tree.files; - - expect(files.find(file => file.includes('angular_material_temp_schematics'))) - .toBeFalsy('Expected the temporary directory for the schematics to be deleted'); - }); - }); diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 677eae330be..d33296f232d 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -6,46 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {FileEntry, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; -import { - NodePackageInstallTask, - RunSchematicTask, - TslintFixTask, -} from '@angular-devkit/schematics/tasks'; +import {Rule, SchematicContext, TaskId, Tree} from '@angular-devkit/schematics'; +import {RunSchematicTask, TslintFixTask} from '@angular-devkit/schematics/tasks'; import {getWorkspace} from '@schematics/angular/utility/config'; -import {existsSync, mkdtempSync} from 'fs'; import * as path from 'path'; -const schematicsSrcPath = 'node_modules/@angular/material/schematics'; -const schematicsTmpPath = mkdtempSync('angular_material_temp_schematics'); - /** Entry point for `ng update` from Angular CLI. */ export default function(): Rule { return (tree: Tree, context: SchematicContext) => { - // If this script failed in an earlier run, clear out the temporary files from that failed - // run before doing anything else. - tree.getDir(schematicsTmpPath).visit((_, entry) => tree.delete(entry.path)); - - // Copy the update schematics to a temporary directory. - const updateSrcs: FileEntry[] = []; - tree.getDir(schematicsSrcPath).visit((_, entry) => updateSrcs.push(entry)); - for (let src of updateSrcs) { - tree.create(src.path.replace(schematicsSrcPath, schematicsTmpPath), src.content); - } - - // Downgrade @angular/cdk and @angular/material to 5.x. This allows us to use the 5.x type - // information in the update script. - const downgradeTask = context.addTask(new NodePackageInstallTask({ - packageName: '@angular/cdk@">=5 <6" @angular/material@">=5 <6"' - })); const allTsConfigPaths = getTsConfigPaths(tree); - const allUpdateTasks = []; + const tslintFixTasks: TaskId[] = []; + + if (!allTsConfigPaths.length) { + throw new Error('Could not find any tsconfig file. Please submit an issue on the Angular ' + + 'Material repository that includes the name of your TypeScript configuration.'); + } for (const tsconfig of allTsConfigPaths) { // Run the update tslint rules. - allUpdateTasks.push(context.addTask(new TslintFixTask({ - rulesDirectory: path.join(schematicsTmpPath, 'update/rules'), + tslintFixTasks.push(context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules/'), rules: { // Automatic fixes. 'switch-identifiers': true, @@ -78,38 +59,25 @@ export default function(): Rule { silent: false, ignoreErrors: true, tsConfigPath: tsconfig, - }), [downgradeTask])); + }))); } - // Upgrade @angular/material back to 6.x. - const upgradeTask = context.addTask(new NodePackageInstallTask({ - // TODO(mmalerba): Change "next" to ">=6 <7". - packageName: '@angular/cdk@next @angular/material@next' - }), allUpdateTasks); - // Delete the temporary schematics directory. - context.addTask(new RunSchematicTask('ng-post-update', { - deletePath: schematicsTmpPath - }), [upgradeTask]); + context.addTask(new RunSchematicTask('ng-post-update', {}), tslintFixTasks); }; } -/** Post-update schematic to be called when ng update is finished. */ -export function postUpdate(options: {deletePath: string}): Rule { - return (tree: Tree, context: SchematicContext) => { - tree.delete(options.deletePath); - context.addTask(new RunSchematicTask('ng-post-post-update', {})); - }; -} - -/** Post-post-update schematic to be called when post-update is finished. */ -export function postPostUpdate(): Rule { +/** Post-update schematic to be called when update is finished. */ +export function postUpdate(): Rule { return () => console.log( '\nComplete! Please check the output above for any issues that were detected but could not' + ' be automatically fixed.'); } -/** Gets the first tsconfig path from possibile locations based on the history of the CLI. */ +/** + * Gets all tsconfig paths from a CLI project by reading the workspace configuration + * and looking for common tsconfig locations. + */ function getTsConfigPaths(tree: Tree): string[] { // Start with some tsconfig paths that are generally used. const tsconfigPaths = [ @@ -139,6 +107,6 @@ function getTsConfigPaths(tree: Tree): string[] { // Filter out tsconfig files that don't exist and remove any duplicates. return tsconfigPaths - .filter(p => existsSync(p)) + .filter(p => tree.exists(p)) .filter((value, index, self) => self.indexOf(value) === index); } diff --git a/src/lib/schematics/utils/testing.ts b/src/lib/schematics/utils/testing.ts index 9753e0815f1..7e5ef605713 100644 --- a/src/lib/schematics/utils/testing.ts +++ b/src/lib/schematics/utils/testing.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {EngineHost, TaskScheduler} from '@angular-devkit/schematics'; import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; import {join} from 'path'; +import {from as observableFrom, Observable} from 'rxjs'; +import {concatMap, filter, last} from 'rxjs/operators'; /** Path to the test collection file for the Material schematics */ export const collectionPath = join(__dirname, '..', 'test-collection.json'); @@ -15,9 +18,7 @@ export const collectionPath = join(__dirname, '..', 'test-collection.json'); /** Path to the test migration file for the Material update schematics */ export const migrationCollection = join(__dirname, '..', 'test-migration.json'); -/** - * Create a base app used for testing. - */ +/** Create a base app used for testing. */ export function createTestApp(): UnitTestTree { const baseRunner = new SchematicTestRunner('material-schematics', collectionPath); @@ -36,3 +37,32 @@ export function createTestApp(): UnitTestTree { skipTests: false, }, workspaceTree); } + +/** + * Due to the fact that the Angular devkit does not support running scheduled tasks from a + * schematic that has been launched through the TestRunner, we need to manually find the task + * executor for the given task name and run all scheduled instances. + * + * Note that this means that there can be multiple tasks with the same name. The observable emits + * only when all tasks finished executing. + */ +export function runPostScheduledTasks(runner: SchematicTestRunner, taskName: string) + : Observable { + + // Workaround until there is a public API to run scheduled tasks in the @angular-devkit. + // See: https://github.com/angular/angular-cli/issues/11739 + const host = runner.engine['_host'] as EngineHost<{}, {}>; + const tasks = runner.engine['_taskSchedulers'] as TaskScheduler[]; + + return observableFrom(tasks).pipe( + concatMap(scheduler => scheduler.finalize()), + filter(task => task.configuration.name === taskName), + concatMap(task => { + return host.createTaskExecutor(task.configuration.name) + .pipe(concatMap(executor => executor(task.configuration.options, task.context))); + }), + // Only emit the last emitted value because there can be multiple tasks with the same name. + // The observable should only emit a value if all tasks completed. + last() + ); +} diff --git a/tslint.json b/tslint.json index f955001c66d..e4192481325 100644 --- a/tslint.json +++ b/tslint.json @@ -119,7 +119,7 @@ }, "src/+(lib|cdk|material-experimental|cdk-experimental)/**/!(*.spec).ts"], "require-license-banner": [ true, - "src/+(lib|cdk|material-experimental|cdk-experimental|demo-app)/**/!(*.spec).ts" + "src/+(lib|cdk|material-experimental|cdk-experimental|demo-app)/**/!(*.spec|*.fixture).ts" ], "missing-rollup-globals": [ true, @@ -130,9 +130,10 @@ "no-unescaped-html-tag": true }, "linterOptions": { - // Exclude schematic template files that can't be linted. "exclude": [ + // Exclude schematic template files and test cases that can't be linted. "src/lib/schematics/**/files/**/*", + "src/lib/schematics/update/test-cases/**/*", // TODO(paul) re-renable specs once the devkit schematics properly work with Bazel and we // can remove the `xit` calls. "src/lib/schematics/**/*.spec.ts"