Skip to content

build: add test cases for update schematics #12473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions src/lib/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

Expand All @@ -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"],
)

Expand All @@ -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",
Expand Down
5 changes: 0 additions & 5 deletions src/lib/schematics/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
5 changes: 4 additions & 1 deletion src/lib/schematics/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*"
]
}
76 changes: 76 additions & 0 deletions src/lib/schematics/update/test-cases/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});


Original file line number Diff line number Diff line change
@@ -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'};
14 changes: 14 additions & 0 deletions src/lib/schematics/update/test-cases/v5/ts-class-names_input.ts
Original file line number Diff line number Diff line change
@@ -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'};
10 changes: 1 addition & 9 deletions src/lib/schematics/update/update.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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');
});

});
70 changes: 19 additions & 51 deletions src/lib/schematics/update/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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);
}
36 changes: 33 additions & 3 deletions src/lib/schematics/utils/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
* 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');

/** 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);

Expand All @@ -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()
);
}
5 changes: 3 additions & 2 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I need to find a way to properly exclude the test cases here. Will work on that

],
"missing-rollup-globals": [
true,
Expand All @@ -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"
Expand Down