build: add test cases for update schematics (#12473)

Adds a way to test that the update schematics properly update a developer's application.
This commit is contained in:
Paul Gschwendtner
2018-08-02 20:28:07 +02:00
committed by Jeremy Elbourn
parent dcd2282295
commit 8cbe8122c0
11 changed files with 173 additions and 74 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "material2-srcs",
"version": "6.4.1",
"version": "6.4.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+8 -2
View File
@@ -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",
-5
View File
@@ -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
}
}
}
+4 -1
View File
@@ -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/**/*"
]
}
@@ -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;
}
});
@@ -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'};
@@ -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'};
+1 -9
View File
@@ -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');
});
});
+19 -51
View File
@@ -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);
}
+33 -3
View File
@@ -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<void> {
// 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()
);
}
+3 -2
View File
@@ -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"