mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 19:13:34 +08:00
feat(@schematics/angular): change layout of e2e files
With this change E2E files will be relocated inside an existing application instead of creating a seperate E2E project. This will also remove a lot of extra boilerplating inside the workspace configuration file. File layout: ``` │ browserslist │ karma.conf.js │ tsconfig.app.json │ tsconfig.spec.json │ tslint.json │ ├───e2e │ │ protractor.conf.js │ │ tsconfig.e2e.json │ │ │ └───src │ app.e2e-spec.ts │ app.po.ts │ └───src │ favicon.ico │ index.html │ main.po.ts │ main.ts │ polyfills.ts │ styles.css │ test.ts │ ├───app │ app.component.css │ app.component.html │ app.component.spec.ts │ app.component.ts │ app.module.ts │ ├───assets │ .gitkeep │ └───environments environment.prod.ts environment.ts ``` Ref: TOOL-699
This commit is contained in:
parent
f2049109a9
commit
026ae8cefc
@ -6,7 +6,8 @@
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
"**/*.spec.ts",
|
||||
"e2e/**"
|
||||
]<% if (enableIvy) { %>,
|
||||
"angularCompilerOptions": {
|
||||
"enableIvy": true
|
||||
|
@ -345,10 +345,8 @@ export default function (options: ApplicationOptions): Rule {
|
||||
const tsLintRoot = appDir;
|
||||
|
||||
const e2eOptions: E2eOptions = {
|
||||
name: `${options.name}-e2e`,
|
||||
relatedAppName: options.name,
|
||||
rootSelector: appRootSelector,
|
||||
projectRoot: newProjectRoot ? `${newProjectRoot}/${options.name}-e2e` : 'e2e',
|
||||
};
|
||||
|
||||
const styleExt = styleToFileExtention(options.style);
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "<%= appDir.split('/').map(x => '..').join('/') %>/tsconfig.json",
|
||||
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "<%= appDir.split('/').map(x => '..').join('/') %>/out-tsc/app",
|
||||
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
|
@ -5,7 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import { strings, tags } from '@angular-devkit/core';
|
||||
import { strings } from '@angular-devkit/core';
|
||||
import {
|
||||
Rule,
|
||||
SchematicContext,
|
||||
@ -20,120 +20,73 @@ import {
|
||||
} from '@angular-devkit/schematics';
|
||||
import { getWorkspace, updateWorkspace } from '../utility/config';
|
||||
import { getProject } from '../utility/project';
|
||||
import {
|
||||
Builders,
|
||||
ProjectType,
|
||||
WorkspaceProject,
|
||||
WorkspaceSchema,
|
||||
} from '../utility/workspace-models';
|
||||
import { Builders, WorkspaceSchema } from '../utility/workspace-models';
|
||||
import { Schema as E2eOptions } from './schema';
|
||||
|
||||
function addAppToWorkspaceFile(options: E2eOptions, workspace: WorkspaceSchema): Rule {
|
||||
function getE2eRoot(projectRoot: string): string {
|
||||
const root = projectRoot.split('/').filter(x => x).join('/');
|
||||
|
||||
return root ? root + '/e2e' : 'e2e';
|
||||
}
|
||||
|
||||
function AddBuilderToWorkspace(options: E2eOptions, workspace: WorkspaceSchema): Rule {
|
||||
return (host: Tree, context: SchematicContext) => {
|
||||
let projectRoot = options.projectRoot !== undefined
|
||||
? options.projectRoot
|
||||
: `${workspace.newProjectRoot}/${options.name}`;
|
||||
const appProject = options.relatedAppName;
|
||||
const project = getProject(workspace, appProject);
|
||||
const architect = project.architect;
|
||||
|
||||
if (projectRoot !== '' && !projectRoot.endsWith('/')) {
|
||||
projectRoot += '/';
|
||||
}
|
||||
const projectRoot = getE2eRoot(project.root);
|
||||
|
||||
if (getProject(workspace, options.name)) {
|
||||
throw new SchematicsException(`Project name "${options.name}" already exists.`);
|
||||
}
|
||||
|
||||
const project: WorkspaceProject = {
|
||||
root: projectRoot,
|
||||
projectType: ProjectType.Application,
|
||||
prefix: '',
|
||||
architect: {
|
||||
e2e: {
|
||||
builder: Builders.Protractor,
|
||||
options: {
|
||||
protractorConfig: `${projectRoot}protractor.conf.js`,
|
||||
devServerTarget: `${options.relatedAppName}:serve`,
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: `${options.relatedAppName}:serve:production`,
|
||||
},
|
||||
if (architect) {
|
||||
architect.e2e = {
|
||||
builder: Builders.Protractor,
|
||||
options: {
|
||||
protractorConfig: `${projectRoot}/protractor.conf.js`,
|
||||
devServerTarget: `${options.relatedAppName}:serve`,
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: `${options.relatedAppName}:serve:production`,
|
||||
},
|
||||
},
|
||||
lint: {
|
||||
builder: Builders.TsLint,
|
||||
options: {
|
||||
tsConfig: `${projectRoot}tsconfig.e2e.json`,
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
workspace.projects[options.name] = project;
|
||||
const lintConfig = architect.lint;
|
||||
if (lintConfig) {
|
||||
lintConfig.options.tsConfig =
|
||||
lintConfig.options.tsConfig.concat(`${projectRoot}/tsconfig.e2e.json`);
|
||||
}
|
||||
|
||||
workspace.projects[options.relatedAppName] = project;
|
||||
}
|
||||
|
||||
return updateWorkspace(workspace);
|
||||
};
|
||||
}
|
||||
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
|
||||
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
|
||||
|
||||
function getRegExpFailPosition(str: string): number | null {
|
||||
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
|
||||
const matched: string[] = [];
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.match(projectNameRegexp)) {
|
||||
matched.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
const compare = matched.join('-');
|
||||
|
||||
return (str !== compare) ? compare.length : null;
|
||||
}
|
||||
|
||||
function validateProjectName(projectName: string) {
|
||||
const errorIndex = getRegExpFailPosition(projectName);
|
||||
if (errorIndex !== null) {
|
||||
const firstMessage = tags.oneLine`
|
||||
Project name "${projectName}" is not valid. New project names must
|
||||
start with a letter, and must contain only alphanumeric characters or dashes.
|
||||
When adding a dash the segment after the dash must also start with a letter.
|
||||
`;
|
||||
const msg = tags.stripIndent`
|
||||
${firstMessage}
|
||||
${projectName}
|
||||
${Array(errorIndex + 1).join(' ') + '^'}
|
||||
`;
|
||||
throw new SchematicsException(msg);
|
||||
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
|
||||
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function (options: E2eOptions): Rule {
|
||||
return (host: Tree) => {
|
||||
validateProjectName(options.name);
|
||||
|
||||
const appProject = options.relatedAppName;
|
||||
const workspace = getWorkspace(host);
|
||||
const appDir = options.projectRoot !== undefined
|
||||
? options.projectRoot
|
||||
: `${workspace.newProjectRoot}/${options.name}`;
|
||||
const project = getProject(workspace, appProject);
|
||||
|
||||
if (!project) {
|
||||
throw new SchematicsException(`Project name "${appProject}" doesn't not exist.`);
|
||||
}
|
||||
|
||||
const root = getE2eRoot(project.root);
|
||||
const relativePathToWorkspaceRoot = root.split('/').map(() => '..').join('/');
|
||||
|
||||
return chain([
|
||||
addAppToWorkspaceFile(options, workspace),
|
||||
AddBuilderToWorkspace(options, workspace),
|
||||
mergeWith(
|
||||
apply(url('./files'), [
|
||||
applyTemplates({
|
||||
utils: strings,
|
||||
...options,
|
||||
'dot': '.',
|
||||
appDir,
|
||||
relativePathToWorkspaceRoot,
|
||||
}),
|
||||
move(appDir),
|
||||
move(root),
|
||||
])),
|
||||
]);
|
||||
};
|
||||
|
@ -6,10 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
|
||||
import { Schema as ApplicationOptions } from '../application/schema';
|
||||
import { Schema as WorkspaceOptions } from '../workspace/schema';
|
||||
import { Schema as E2eOptions } from './schema';
|
||||
|
||||
// tslint:disable:max-line-length
|
||||
describe('Application Schematic', () => {
|
||||
const schematicRunner = new SchematicTestRunner(
|
||||
'@schematics/angular',
|
||||
@ -23,81 +23,81 @@ describe('Application Schematic', () => {
|
||||
};
|
||||
|
||||
const defaultOptions: E2eOptions = {
|
||||
name: 'foo',
|
||||
relatedAppName: 'app',
|
||||
relatedAppName: 'foo',
|
||||
};
|
||||
|
||||
let workspaceTree: UnitTestTree;
|
||||
const defaultAppOptions: ApplicationOptions = {
|
||||
name: 'foo',
|
||||
inlineStyle: true,
|
||||
inlineTemplate: true,
|
||||
routing: false,
|
||||
skipPackageJson: false,
|
||||
minimal: true,
|
||||
};
|
||||
|
||||
let applicationTree: UnitTestTree;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions);
|
||||
const workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions);
|
||||
applicationTree = schematicRunner.runSchematic('application', defaultAppOptions, workspaceTree);
|
||||
});
|
||||
|
||||
it('should create all files of an e2e application', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
it('should create all files of e2e in an application', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
|
||||
const files = tree.files;
|
||||
expect(files).toEqual(jasmine.arrayContaining([
|
||||
'/projects/foo/protractor.conf.js',
|
||||
'/projects/foo/tsconfig.e2e.json',
|
||||
'/projects/foo/src/app.e2e-spec.ts',
|
||||
'/projects/foo/src/app.po.ts',
|
||||
'/projects/foo/e2e/protractor.conf.js',
|
||||
'/projects/foo/e2e/tsconfig.e2e.json',
|
||||
'/projects/foo/e2e/src/app.e2e-spec.ts',
|
||||
'/projects/foo/e2e/src/app.po.ts',
|
||||
]));
|
||||
});
|
||||
|
||||
it('should create all files of an e2e application', () => {
|
||||
const options = {...defaultOptions, projectRoot: 'e2e'};
|
||||
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
|
||||
const files = tree.files;
|
||||
expect(files).not.toContain('/projects/foo/protractor.conf.js');
|
||||
expect(files).toContain('/e2e/protractor.conf.js');
|
||||
});
|
||||
|
||||
it('should set the rootSelector in the app.po.ts', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
const content = tree.readContent('/projects/foo/src/app.po.ts');
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
|
||||
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
|
||||
expect(content).toMatch(/app\-root/);
|
||||
});
|
||||
|
||||
it('should set the rootSelector in the app.po.ts from the option', () => {
|
||||
const options = {...defaultOptions, rootSelector: 't-a-c-o'};
|
||||
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
|
||||
const content = tree.readContent('/projects/foo/src/app.po.ts');
|
||||
const tree = schematicRunner.runSchematic('e2e', options, applicationTree);
|
||||
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
|
||||
expect(content).toMatch(/t\-a\-c\-o/);
|
||||
});
|
||||
|
||||
it('should set the rootSelector in the app.po.ts from the option with emoji', () => {
|
||||
const options = {...defaultOptions, rootSelector: '🌮-🌯'};
|
||||
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
|
||||
const content = tree.readContent('/projects/foo/src/app.po.ts');
|
||||
const tree = schematicRunner.runSchematic('e2e', options, applicationTree);
|
||||
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
|
||||
expect(content).toMatch(/🌮-🌯/);
|
||||
});
|
||||
|
||||
describe('workspace config', () => {
|
||||
it('should create the e2e app', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
const workspace = JSON.parse(tree.readContent('/angular.json'));
|
||||
expect(workspace.projects.foo).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set 2 targets for the app', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
it('should add e2e targets for the app', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
|
||||
const workspace = JSON.parse(tree.readContent('/angular.json'));
|
||||
const targets = workspace.projects.foo.architect;
|
||||
expect(Object.keys(targets)).toEqual(['e2e', 'lint']);
|
||||
expect(targets.e2e).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set the e2e options', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
|
||||
const workspace = JSON.parse(tree.readContent('/angular.json'));
|
||||
const e2eOptions = workspace.projects.foo.architect.e2e.options;
|
||||
expect(e2eOptions.protractorConfig).toEqual('projects/foo/protractor.conf.js');
|
||||
expect(e2eOptions.devServerTarget).toEqual('app:serve');
|
||||
expect(e2eOptions.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js');
|
||||
expect(e2eOptions.devServerTarget).toEqual('foo:serve');
|
||||
});
|
||||
|
||||
it('should set the lint options', () => {
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
|
||||
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
|
||||
const workspace = JSON.parse(tree.readContent('/angular.json'));
|
||||
const lintOptions = workspace.projects.foo.architect.lint.options;
|
||||
expect(lintOptions.tsConfig).toEqual('projects/foo/tsconfig.e2e.json');
|
||||
expect(lintOptions.tsConfig).toEqual([
|
||||
'projects/foo/tsconfig.app.json',
|
||||
'projects/foo/tsconfig.spec.json',
|
||||
'projects/foo/e2e/tsconfig.e2e.json',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,20 +6,6 @@
|
||||
"description": "Generates a new, generic end-to-end test definition for the given or default project.",
|
||||
"long-description": "e2e-long.md",
|
||||
"properties": {
|
||||
"projectRoot": {
|
||||
"description": "The root folder for the new test app.",
|
||||
"type": "string",
|
||||
"visible": false
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the new e2e app.",
|
||||
"type": "string",
|
||||
"format": "html-selector",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"rootSelector": {
|
||||
"description": "The HTML selector for the root component of the test app.",
|
||||
"type": "string",
|
||||
@ -31,7 +17,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"relatedAppName"
|
||||
]
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ describe('Ng New Schematic', () => {
|
||||
'/bar/src/tsconfig.app.json',
|
||||
'/bar/src/main.ts',
|
||||
'/bar/src/app/app.module.ts',
|
||||
'/bar/e2e/src/app.po.ts',
|
||||
'/bar/e2e/src/app.e2e-spec.ts',
|
||||
'/bar/e2e/tsconfig.e2e.json',
|
||||
'/bar/e2e/protractor.conf.js',
|
||||
]));
|
||||
});
|
||||
|
||||
@ -68,13 +72,11 @@ describe('Ng New Schematic', () => {
|
||||
expect(files).not.toContain('/bar/src');
|
||||
});
|
||||
|
||||
it('minimal=true should not create e2e project', () => {
|
||||
it('minimal=true should not create an e2e target', () => {
|
||||
const options = { ...defaultOptions, minimal: true };
|
||||
|
||||
const tree = schematicRunner.runSchematic('ng-new', options);
|
||||
const files = tree.files;
|
||||
expect(files).not.toContain('/bar/e2e');
|
||||
const confContent = JSON.parse(tree.readContent('/bar/angular.json'));
|
||||
expect(confContent.projects['foo-e2e']).toBeUndefined();
|
||||
expect(confContent.projects.foo.e2e).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ng} from '../../../utils/process';
|
||||
import { expectFileToMatch } from '../../../utils/fs';
|
||||
import { ng } from '../../../utils/process';
|
||||
import { useCIChrome } from '../../../utils/project';
|
||||
|
||||
|
||||
@ -7,6 +7,5 @@ export default function() {
|
||||
return ng('generate', 'application', 'app2')
|
||||
.then(() => expectFileToMatch('angular.json', /\"app2\":/))
|
||||
.then(() => useCIChrome('projects/app2'))
|
||||
.then(() => useCIChrome('projects/app2-e2e'))
|
||||
.then(() => ng('test', 'app2', '--watch=false', '--browsers=ChromeHeadlessCI'));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user