mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
dcd2282295
commit
8cbe8122c0
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "material2-srcs",
|
||||
"version": "6.4.1",
|
||||
"version": "6.4.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user