Skip to content

Commit 8cbe812

Browse files
devversionjelbourn
authored andcommitted
build: add test cases for update schematics (#12473)
Adds a way to test that the update schematics properly update a developer's application.
1 parent dcd2282 commit 8cbe812

11 files changed

+173
-74
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/schematics/BUILD.bazel

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ filegroup(
1111
ts_library(
1212
name = "schematics",
1313
module_name = "@angular/material/schematics",
14-
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "**/files/**/*"]),
14+
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "update/test-cases/**/*.ts", "**/files/**/*"]),
1515
tsconfig = ":tsconfig.json",
1616
)
1717

@@ -27,7 +27,7 @@ npm_package(
2727
jasmine_node_test(
2828
name = "unit_tests",
2929
srcs = [":schematics_test_sources"],
30-
data = [":schematics_assets"],
30+
data = [":schematics_assets", ":schematics_test_cases"],
3131
deps = [":copy-collection-file", ":copy-migration-file"],
3232
)
3333

@@ -39,6 +39,12 @@ ts_library(
3939
testonly = True,
4040
)
4141

42+
filegroup(
43+
name = "schematics_test_cases",
44+
srcs = glob(["update/test-cases/**/*_input.ts", "update/test-cases/**/*_expected_output.ts"]),
45+
testonly = True,
46+
)
47+
4248
# Workaround for https://github.com/bazelbuild/rules_typescript/issues/154
4349
genrule(
4450
name = "copy-collection-file",

src/lib/schematics/migration.json

-5
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@
1111
"description": "Performs cleanup after ng-update.",
1212
"factory": "./update/update#postUpdate",
1313
"private": true
14-
},
15-
"ng-post-post-update": {
16-
"description": "Logs completion message for ng-update after ng-post-update.",
17-
"factory": "./update/update#postPostUpdate",
18-
"private": true
1914
}
2015
}
2116
}

src/lib/schematics/tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
]
1616
},
1717
"exclude": [
18+
"**/*.spec.ts",
19+
// Exclude template files that will be copied by the schematics. Those are not valid TS.
1820
"*/files/**/*",
19-
"**/*spec*"
21+
// Exclude all test-case files because those should not be included in the schematics output.
22+
"update/test-cases/**/*"
2023
]
2124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
2+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
3+
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
4+
import {readFileSync} from 'fs';
5+
import {createTestApp, migrationCollection, runPostScheduledTasks} from '../../utils/testing';
6+
7+
describe('test cases', () => {
8+
9+
// Module name suffix for data files of the `jasmine_node_test` Bazel rule.
10+
const bazelModuleSuffix = 'angular_material/src/lib/schematics/update/test-cases';
11+
12+
/**
13+
* Name of test cases that will be used to verify that update schematics properly update
14+
* a developers application.
15+
*/
16+
const testCases = [
17+
'v5/ts-class-names'
18+
];
19+
20+
// Iterates through every test case directory and generates a jasmine test block that will
21+
// verify that the update schematics properly update the test input to the expected output.
22+
testCases.forEach(testCaseName => {
23+
24+
// Adding the test case files to the data of the `jasmine_node_test` Bazel rule does not mean
25+
// that the files are being copied over to the Bazel bin output. Bazel just patches the NodeJS
26+
// resolve function and maps the module paths to the original file location. Since we
27+
// need to load the content of those test cases, we need to resolve the original file path.
28+
const inputPath = require.resolve(`${bazelModuleSuffix}/${testCaseName}_input.ts`);
29+
const expectedOutputPath = require
30+
.resolve(`${bazelModuleSuffix}/${testCaseName}_expected_output.ts`);
31+
32+
it(`should apply update schematics to test case: ${testCaseName}`, () => {
33+
const runner = new SchematicTestRunner('schematics', migrationCollection);
34+
35+
runner.runSchematic('migration-01', {}, createTestAppWithTestCase(inputPath));
36+
37+
// Run the scheduled TSLint fix task from the update schematic. This task is responsible for
38+
// identifying outdated code parts and performs the fixes. Since tasks won't run automatically
39+
// within a `SchematicTestRunner`, we manually need to run the scheduled task.
40+
return runPostScheduledTasks(runner, 'tslint-fix').toPromise().then(() => {
41+
expect(readFileContent('projects/material/src/main.ts'))
42+
.toBe(readFileContent(expectedOutputPath));
43+
});
44+
});
45+
});
46+
47+
/** Reads the UTF8 content of the specified file. Normalizes the path and ensures that */
48+
function readFileContent(filePath: string): string {
49+
return readFileSync(filePath, 'utf8');
50+
}
51+
52+
/**
53+
* Creates a test app schematic tree that includes the specified test case as TypeScript
54+
* entry point file. Also writes the app tree to a real file system location in order to be
55+
* able to test the tslint fix rules.
56+
*/
57+
function createTestAppWithTestCase(testCaseInputPath: string) {
58+
const tempFileSystemHost = new TempScopedNodeJsSyncHost();
59+
const appTree = createTestApp();
60+
61+
appTree.overwrite('/projects/material/src/main.ts', readFileContent(testCaseInputPath));
62+
63+
// Since the TSLint fix task expects all files to be present on the real file system, we
64+
// map every file in the app tree to a temporary location on the file system.
65+
appTree.files.map(f => normalize(f)).forEach(f => {
66+
tempFileSystemHost.sync.write(f, virtualFs.stringToFileBuffer(appTree.readContent(f)));
67+
});
68+
69+
// Switch to the new temporary directory because otherwise TSLint cannot read the files.
70+
process.chdir(getSystemPath(tempFileSystemHost.root));
71+
72+
return appTree;
73+
}
74+
});
75+
76+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {CdkConnectedOverlay, CdkOverlayOrigin} from '@angular/cdk/overlay';
2+
import {CdkObserveContent} from '@angular/cdk/observers';
3+
import {CdkTrapFocus} from '@angular/cdk/a11y';
4+
import {FloatLabelType, LabelOptions, MAT_LABEL_GLOBAL_OPTIONS} from '@angular/material';
5+
6+
const a = new CdkConnectedOverlay();
7+
const b = new CdkOverlayOrigin();
8+
const c = new CdkObserveContent();
9+
const d = new CdkTrapFocus();
10+
11+
const e: FloatLabelType = 'test';
12+
const f: LabelOptions = 'opt2';
13+
14+
const g = {provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: 'test-options'};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {ConnectedOverlayDirective, OverlayOrigin} from '@angular/cdk/overlay';
2+
import {ObserveContent} from '@angular/cdk/observers';
3+
import {FocusTrapDirective} from '@angular/cdk/a11y';
4+
import {FloatPlaceholderType, PlaceholderOptions, MAT_PLACEHOLDER_GLOBAL_OPTIONS} from '@angular/material';
5+
6+
const a = new ConnectedOverlayDirective();
7+
const b = new OverlayOrigin();
8+
const c = new ObserveContent();
9+
const d = new FocusTrapDirective();
10+
11+
const e: FloatPlaceholderType = 'test';
12+
const f: PlaceholderOptions = 'opt2';
13+
14+
const g = {provide: MAT_PLACEHOLDER_GLOBAL_OPTIONS, useValue: 'test-options'};
+1-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
2-
import {migrationCollection, createTestApp} from '../utils/testing';
2+
import {migrationCollection} from '../utils/testing';
33

44
describe('material-nav-schematic', () => {
55
let runner: SchematicTestRunner;
@@ -8,12 +8,4 @@ describe('material-nav-schematic', () => {
88
runner = new SchematicTestRunner('schematics', migrationCollection);
99
});
1010

11-
it('should remove the temp directory', () => {
12-
const tree = runner.runSchematic('migration-01', {}, createTestApp());
13-
const files = tree.files;
14-
15-
expect(files.find(file => file.includes('angular_material_temp_schematics')))
16-
.toBeFalsy('Expected the temporary directory for the schematics to be deleted');
17-
});
18-
1911
});

src/lib/schematics/update/update.ts

+19-51
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,27 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {FileEntry, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
10-
import {
11-
NodePackageInstallTask,
12-
RunSchematicTask,
13-
TslintFixTask,
14-
} from '@angular-devkit/schematics/tasks';
9+
import {Rule, SchematicContext, TaskId, Tree} from '@angular-devkit/schematics';
10+
import {RunSchematicTask, TslintFixTask} from '@angular-devkit/schematics/tasks';
1511
import {getWorkspace} from '@schematics/angular/utility/config';
16-
import {existsSync, mkdtempSync} from 'fs';
1712
import * as path from 'path';
1813

19-
const schematicsSrcPath = 'node_modules/@angular/material/schematics';
20-
const schematicsTmpPath = mkdtempSync('angular_material_temp_schematics');
21-
2214
/** Entry point for `ng update` from Angular CLI. */
2315
export default function(): Rule {
2416
return (tree: Tree, context: SchematicContext) => {
25-
// If this script failed in an earlier run, clear out the temporary files from that failed
26-
// run before doing anything else.
27-
tree.getDir(schematicsTmpPath).visit((_, entry) => tree.delete(entry.path));
28-
29-
// Copy the update schematics to a temporary directory.
30-
const updateSrcs: FileEntry[] = [];
31-
tree.getDir(schematicsSrcPath).visit((_, entry) => updateSrcs.push(entry));
32-
for (let src of updateSrcs) {
33-
tree.create(src.path.replace(schematicsSrcPath, schematicsTmpPath), src.content);
34-
}
35-
36-
// Downgrade @angular/cdk and @angular/material to 5.x. This allows us to use the 5.x type
37-
// information in the update script.
38-
const downgradeTask = context.addTask(new NodePackageInstallTask({
39-
packageName: '@angular/cdk@">=5 <6" @angular/material@">=5 <6"'
40-
}));
4117

4218
const allTsConfigPaths = getTsConfigPaths(tree);
43-
const allUpdateTasks = [];
19+
const tslintFixTasks: TaskId[] = [];
20+
21+
if (!allTsConfigPaths.length) {
22+
throw new Error('Could not find any tsconfig file. Please submit an issue on the Angular ' +
23+
'Material repository that includes the name of your TypeScript configuration.');
24+
}
4425

4526
for (const tsconfig of allTsConfigPaths) {
4627
// Run the update tslint rules.
47-
allUpdateTasks.push(context.addTask(new TslintFixTask({
48-
rulesDirectory: path.join(schematicsTmpPath, 'update/rules'),
28+
tslintFixTasks.push(context.addTask(new TslintFixTask({
29+
rulesDirectory: path.join(__dirname, 'rules/'),
4930
rules: {
5031
// Automatic fixes.
5132
'switch-identifiers': true,
@@ -78,38 +59,25 @@ export default function(): Rule {
7859
silent: false,
7960
ignoreErrors: true,
8061
tsConfigPath: tsconfig,
81-
}), [downgradeTask]));
62+
})));
8263
}
8364

84-
// Upgrade @angular/material back to 6.x.
85-
const upgradeTask = context.addTask(new NodePackageInstallTask({
86-
// TODO(mmalerba): Change "next" to ">=6 <7".
87-
packageName: '@angular/cdk@next @angular/material@next'
88-
}), allUpdateTasks);
89-
9065
// Delete the temporary schematics directory.
91-
context.addTask(new RunSchematicTask('ng-post-update', {
92-
deletePath: schematicsTmpPath
93-
}), [upgradeTask]);
66+
context.addTask(new RunSchematicTask('ng-post-update', {}), tslintFixTasks);
9467
};
9568
}
9669

97-
/** Post-update schematic to be called when ng update is finished. */
98-
export function postUpdate(options: {deletePath: string}): Rule {
99-
return (tree: Tree, context: SchematicContext) => {
100-
tree.delete(options.deletePath);
101-
context.addTask(new RunSchematicTask('ng-post-post-update', {}));
102-
};
103-
}
104-
105-
/** Post-post-update schematic to be called when post-update is finished. */
106-
export function postPostUpdate(): Rule {
70+
/** Post-update schematic to be called when update is finished. */
71+
export function postUpdate(): Rule {
10772
return () => console.log(
10873
'\nComplete! Please check the output above for any issues that were detected but could not' +
10974
' be automatically fixed.');
11075
}
11176

112-
/** Gets the first tsconfig path from possibile locations based on the history of the CLI. */
77+
/**
78+
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
79+
* and looking for common tsconfig locations.
80+
*/
11381
function getTsConfigPaths(tree: Tree): string[] {
11482
// Start with some tsconfig paths that are generally used.
11583
const tsconfigPaths = [
@@ -139,6 +107,6 @@ function getTsConfigPaths(tree: Tree): string[] {
139107

140108
// Filter out tsconfig files that don't exist and remove any duplicates.
141109
return tsconfigPaths
142-
.filter(p => existsSync(p))
110+
.filter(p => tree.exists(p))
143111
.filter((value, index, self) => self.indexOf(value) === index);
144112
}

src/lib/schematics/utils/testing.ts

+33-3
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {EngineHost, TaskScheduler} from '@angular-devkit/schematics';
910
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
1011
import {join} from 'path';
12+
import {from as observableFrom, Observable} from 'rxjs';
13+
import {concatMap, filter, last} from 'rxjs/operators';
1114

1215
/** Path to the test collection file for the Material schematics */
1316
export const collectionPath = join(__dirname, '..', 'test-collection.json');
1417

1518
/** Path to the test migration file for the Material update schematics */
1619
export const migrationCollection = join(__dirname, '..', 'test-migration.json');
1720

18-
/**
19-
* Create a base app used for testing.
20-
*/
21+
/** Create a base app used for testing. */
2122
export function createTestApp(): UnitTestTree {
2223
const baseRunner = new SchematicTestRunner('material-schematics', collectionPath);
2324

@@ -36,3 +37,32 @@ export function createTestApp(): UnitTestTree {
3637
skipTests: false,
3738
}, workspaceTree);
3839
}
40+
41+
/**
42+
* Due to the fact that the Angular devkit does not support running scheduled tasks from a
43+
* schematic that has been launched through the TestRunner, we need to manually find the task
44+
* executor for the given task name and run all scheduled instances.
45+
*
46+
* Note that this means that there can be multiple tasks with the same name. The observable emits
47+
* only when all tasks finished executing.
48+
*/
49+
export function runPostScheduledTasks(runner: SchematicTestRunner, taskName: string)
50+
: Observable<void> {
51+
52+
// Workaround until there is a public API to run scheduled tasks in the @angular-devkit.
53+
// See: https://github.com/angular/angular-cli/issues/11739
54+
const host = runner.engine['_host'] as EngineHost<{}, {}>;
55+
const tasks = runner.engine['_taskSchedulers'] as TaskScheduler[];
56+
57+
return observableFrom(tasks).pipe(
58+
concatMap(scheduler => scheduler.finalize()),
59+
filter(task => task.configuration.name === taskName),
60+
concatMap(task => {
61+
return host.createTaskExecutor(task.configuration.name)
62+
.pipe(concatMap(executor => executor(task.configuration.options, task.context)));
63+
}),
64+
// Only emit the last emitted value because there can be multiple tasks with the same name.
65+
// The observable should only emit a value if all tasks completed.
66+
last()
67+
);
68+
}

tslint.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
}, "src/+(lib|cdk|material-experimental|cdk-experimental)/**/!(*.spec).ts"],
120120
"require-license-banner": [
121121
true,
122-
"src/+(lib|cdk|material-experimental|cdk-experimental|demo-app)/**/!(*.spec).ts"
122+
"src/+(lib|cdk|material-experimental|cdk-experimental|demo-app)/**/!(*.spec|*.fixture).ts"
123123
],
124124
"missing-rollup-globals": [
125125
true,
@@ -130,9 +130,10 @@
130130
"no-unescaped-html-tag": true
131131
},
132132
"linterOptions": {
133-
// Exclude schematic template files that can't be linted.
134133
"exclude": [
134+
// Exclude schematic template files and test cases that can't be linted.
135135
"src/lib/schematics/**/files/**/*",
136+
"src/lib/schematics/update/test-cases/**/*",
136137
// TODO(paul) re-renable specs once the devkit schematics properly work with Bazel and we
137138
// can remove the `xit` calls.
138139
"src/lib/schematics/**/*.spec.ts"

0 commit comments

Comments
 (0)