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:
Alan 2019-02-27 16:12:59 +01:00 committed by Hans
parent f2049109a9
commit 026ae8cefc
8 changed files with 94 additions and 156 deletions

View File

@ -6,7 +6,8 @@
},
"exclude": [
"test.ts",
"**/*.spec.ts"
"**/*.spec.ts",
"e2e/**"
]<% if (enableIvy) { %>,
"angularCompilerOptions": {
"enableIvy": true

View File

@ -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);

View File

@ -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": [

View File

@ -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),
])),
]);
};

View File

@ -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',
]);
});
});
});

View File

@ -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"
]
}

View File

@ -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();
});
});

View File

@ -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'));
}