Skip to content

Commit 1d546c0

Browse files
author
Alan
committed
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
1 parent 214e8ef commit 1d546c0

File tree

8 files changed

+92
-154
lines changed

8 files changed

+92
-154
lines changed

packages/schematics/angular/application/files/root/tsconfig.app.json.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
},
77
"exclude": [
88
"test.ts",
9-
"**/*.spec.ts"
9+
"**/*.spec.ts",
10+
"e2e/**"
1011
]<% if (enableIvy) { %>,
1112
"angularCompilerOptions": {
1213
"enableIvy": true

packages/schematics/angular/application/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,8 @@ export default function (options: ApplicationOptions): Rule {
345345
const tsLintRoot = appDir;
346346

347347
const e2eOptions: E2eOptions = {
348-
name: `${options.name}-e2e`,
349348
relatedAppName: options.name,
350349
rootSelector: appRootSelector,
351-
projectRoot: newProjectRoot ? `${newProjectRoot}/${options.name}-e2e` : 'e2e',
352350
};
353351

354352
const styleExt = styleToFileExtention(options.style);

packages/schematics/angular/e2e/files/tsconfig.e2e.json.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"extends": "<%= appDir.split('/').map(x => '..').join('/') %>/tsconfig.json",
2+
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
33
"compilerOptions": {
4-
"outDir": "<%= appDir.split('/').map(x => '..').join('/') %>/out-tsc/app",
4+
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/e2e",
55
"module": "commonjs",
66
"target": "es5",
77
"types": [

packages/schematics/angular/e2e/index.ts

Lines changed: 43 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import { strings, tags } from '@angular-devkit/core';
8+
import { strings } from '@angular-devkit/core';
99
import {
1010
Rule,
1111
SchematicContext,
@@ -20,120 +20,73 @@ import {
2020
} from '@angular-devkit/schematics';
2121
import { getWorkspace, updateWorkspace } from '../utility/config';
2222
import { getProject } from '../utility/project';
23-
import {
24-
Builders,
25-
ProjectType,
26-
WorkspaceProject,
27-
WorkspaceSchema,
28-
} from '../utility/workspace-models';
23+
import { Builders, WorkspaceSchema } from '../utility/workspace-models';
2924
import { Schema as E2eOptions } from './schema';
3025

31-
function addAppToWorkspaceFile(options: E2eOptions, workspace: WorkspaceSchema): Rule {
32-
return (host: Tree, context: SchematicContext) => {
33-
let projectRoot = options.projectRoot !== undefined
34-
? options.projectRoot
35-
: `${workspace.newProjectRoot}/${options.name}`;
26+
function getE2eRoot(projectRoot: string): string {
27+
const root = projectRoot.split('/').filter(x => x).join('/');
3628

37-
if (projectRoot !== '' && !projectRoot.endsWith('/')) {
38-
projectRoot += '/';
39-
}
29+
return root ? root + '/e2e' : 'e2e';
30+
}
4031

41-
if (getProject(workspace, options.name)) {
42-
throw new SchematicsException(`Project name "${options.name}" already exists.`);
43-
}
32+
function AddBuilderToWorkspace(options: E2eOptions, workspace: WorkspaceSchema): Rule {
33+
return (host: Tree, context: SchematicContext) => {
34+
const appProject = options.relatedAppName;
35+
const project = getProject(workspace, appProject);
36+
const architect = project.architect;
4437

45-
const project: WorkspaceProject = {
46-
root: projectRoot,
47-
projectType: ProjectType.Application,
48-
prefix: '',
49-
architect: {
50-
e2e: {
51-
builder: Builders.Protractor,
52-
options: {
53-
protractorConfig: `${projectRoot}protractor.conf.js`,
54-
devServerTarget: `${options.relatedAppName}:serve`,
55-
},
56-
configurations: {
57-
production: {
58-
devServerTarget: `${options.relatedAppName}:serve:production`,
59-
},
60-
},
38+
const projectRoot = getE2eRoot(project.root);
39+
40+
if (architect) {
41+
architect.e2e = {
42+
builder: Builders.Protractor,
43+
options: {
44+
protractorConfig: `${projectRoot}/protractor.conf.js`,
45+
devServerTarget: `${options.relatedAppName}:serve`,
6146
},
62-
lint: {
63-
builder: Builders.TsLint,
64-
options: {
65-
tsConfig: `${projectRoot}tsconfig.e2e.json`,
66-
exclude: [
67-
'**/node_modules/**',
68-
],
47+
configurations: {
48+
production: {
49+
devServerTarget: `${options.relatedAppName}:serve:production`,
6950
},
7051
},
71-
},
72-
};
73-
74-
workspace.projects[options.name] = project;
75-
76-
return updateWorkspace(workspace);
77-
};
78-
}
79-
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
80-
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
52+
};
8153

82-
function getRegExpFailPosition(str: string): number | null {
83-
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
84-
const matched: string[] = [];
54+
const lintConfig = architect.lint;
55+
if (lintConfig) {
56+
lintConfig.options.tsConfig =
57+
lintConfig.options.tsConfig.concat(`${projectRoot}/tsconfig.e2e.json`);
58+
}
8559

86-
parts.forEach(part => {
87-
if (part.match(projectNameRegexp)) {
88-
matched.push(part);
60+
workspace.projects[options.relatedAppName] = project;
8961
}
90-
});
91-
92-
const compare = matched.join('-');
93-
94-
return (str !== compare) ? compare.length : null;
95-
}
96-
97-
function validateProjectName(projectName: string) {
98-
const errorIndex = getRegExpFailPosition(projectName);
99-
if (errorIndex !== null) {
100-
const firstMessage = tags.oneLine`
101-
Project name "${projectName}" is not valid. New project names must
102-
start with a letter, and must contain only alphanumeric characters or dashes.
103-
When adding a dash the segment after the dash must also start with a letter.
104-
`;
105-
const msg = tags.stripIndent`
106-
${firstMessage}
107-
${projectName}
108-
${Array(errorIndex + 1).join(' ') + '^'}
109-
`;
110-
throw new SchematicsException(msg);
111-
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
112-
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
113-
}
11462

63+
return updateWorkspace(workspace);
64+
};
11565
}
11666

11767
export default function (options: E2eOptions): Rule {
11868
return (host: Tree) => {
119-
validateProjectName(options.name);
120-
69+
const appProject = options.relatedAppName;
12170
const workspace = getWorkspace(host);
122-
const appDir = options.projectRoot !== undefined
123-
? options.projectRoot
124-
: `${workspace.newProjectRoot}/${options.name}`;
71+
const project = getProject(workspace, appProject);
72+
73+
if (!project) {
74+
throw new SchematicsException(`Project name "${appProject}" doesn't not exist.`);
75+
}
76+
77+
const root = getE2eRoot(project.root);
78+
const relativePathToWorkspaceRoot = root.split('/').map(() => '..').join('/');
12579

12680
return chain([
127-
addAppToWorkspaceFile(options, workspace),
81+
AddBuilderToWorkspace(options, workspace),
12882
mergeWith(
12983
apply(url('./files'), [
13084
applyTemplates({
13185
utils: strings,
13286
...options,
133-
'dot': '.',
134-
appDir,
87+
relativePathToWorkspaceRoot,
13588
}),
136-
move(appDir),
89+
move(root),
13790
])),
13891
]);
13992
};

packages/schematics/angular/e2e/index_spec.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
9+
import { Schema as ApplicationOptions } from '../application/schema';
910
import { Schema as WorkspaceOptions } from '../workspace/schema';
1011
import { Schema as E2eOptions } from './schema';
1112

12-
// tslint:disable:max-line-length
1313
describe('Application Schematic', () => {
1414
const schematicRunner = new SchematicTestRunner(
1515
'@schematics/angular',
@@ -23,81 +23,81 @@ describe('Application Schematic', () => {
2323
};
2424

2525
const defaultOptions: E2eOptions = {
26+
relatedAppName: 'foo',
27+
};
28+
29+
const defaultAppOptions: ApplicationOptions = {
2630
name: 'foo',
27-
relatedAppName: 'app',
31+
inlineStyle: true,
32+
inlineTemplate: true,
33+
routing: false,
34+
skipPackageJson: false,
35+
minimal: true,
2836
};
2937

30-
let workspaceTree: UnitTestTree;
38+
let applicationTree: UnitTestTree;
39+
3140
beforeEach(() => {
32-
workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions);
41+
const workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions);
42+
applicationTree = schematicRunner.runSchematic('application', defaultAppOptions, workspaceTree);
3343
});
3444

35-
it('should create all files of an e2e application', () => {
36-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
45+
it('should create all files of e2e in an application', () => {
46+
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
3747
const files = tree.files;
3848
expect(files).toEqual(jasmine.arrayContaining([
39-
'/projects/foo/protractor.conf.js',
40-
'/projects/foo/tsconfig.e2e.json',
41-
'/projects/foo/src/app.e2e-spec.ts',
42-
'/projects/foo/src/app.po.ts',
49+
'/projects/foo/e2e/protractor.conf.js',
50+
'/projects/foo/e2e/tsconfig.e2e.json',
51+
'/projects/foo/e2e/src/app.e2e-spec.ts',
52+
'/projects/foo/e2e/src/app.po.ts',
4353
]));
4454
});
4555

46-
it('should create all files of an e2e application', () => {
47-
const options = {...defaultOptions, projectRoot: 'e2e'};
48-
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
49-
const files = tree.files;
50-
expect(files).not.toContain('/projects/foo/protractor.conf.js');
51-
expect(files).toContain('/e2e/protractor.conf.js');
52-
});
53-
5456
it('should set the rootSelector in the app.po.ts', () => {
55-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
56-
const content = tree.readContent('/projects/foo/src/app.po.ts');
57+
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
58+
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
5759
expect(content).toMatch(/app\-root/);
5860
});
5961

6062
it('should set the rootSelector in the app.po.ts from the option', () => {
6163
const options = {...defaultOptions, rootSelector: 't-a-c-o'};
62-
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
63-
const content = tree.readContent('/projects/foo/src/app.po.ts');
64+
const tree = schematicRunner.runSchematic('e2e', options, applicationTree);
65+
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
6466
expect(content).toMatch(/t\-a\-c\-o/);
6567
});
6668

6769
it('should set the rootSelector in the app.po.ts from the option with emoji', () => {
6870
const options = {...defaultOptions, rootSelector: '🌮-🌯'};
69-
const tree = schematicRunner.runSchematic('e2e', options, workspaceTree);
70-
const content = tree.readContent('/projects/foo/src/app.po.ts');
71+
const tree = schematicRunner.runSchematic('e2e', options, applicationTree);
72+
const content = tree.readContent('/projects/foo/e2e/src/app.po.ts');
7173
expect(content).toMatch(/🌮-🌯/);
7274
});
7375

7476
describe('workspace config', () => {
75-
it('should create the e2e app', () => {
76-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
77-
const workspace = JSON.parse(tree.readContent('/angular.json'));
78-
expect(workspace.projects.foo).toBeDefined();
79-
});
80-
81-
it('should set 2 targets for the app', () => {
82-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
77+
it('should add e2e targets for the app', () => {
78+
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
8379
const workspace = JSON.parse(tree.readContent('/angular.json'));
8480
const targets = workspace.projects.foo.architect;
85-
expect(Object.keys(targets)).toEqual(['e2e', 'lint']);
81+
expect(targets.e2e).toBeDefined();
8682
});
8783

8884
it('should set the e2e options', () => {
89-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
85+
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
9086
const workspace = JSON.parse(tree.readContent('/angular.json'));
9187
const e2eOptions = workspace.projects.foo.architect.e2e.options;
92-
expect(e2eOptions.protractorConfig).toEqual('projects/foo/protractor.conf.js');
93-
expect(e2eOptions.devServerTarget).toEqual('app:serve');
88+
expect(e2eOptions.protractorConfig).toEqual('projects/foo/e2e/protractor.conf.js');
89+
expect(e2eOptions.devServerTarget).toEqual('foo:serve');
9490
});
9591

9692
it('should set the lint options', () => {
97-
const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree);
93+
const tree = schematicRunner.runSchematic('e2e', defaultOptions, applicationTree);
9894
const workspace = JSON.parse(tree.readContent('/angular.json'));
9995
const lintOptions = workspace.projects.foo.architect.lint.options;
100-
expect(lintOptions.tsConfig).toEqual('projects/foo/tsconfig.e2e.json');
96+
expect(lintOptions.tsConfig).toEqual([
97+
'projects/foo/tsconfig.app.json',
98+
'projects/foo/tsconfig.spec.json',
99+
'projects/foo/e2e/tsconfig.e2e.json',
100+
]);
101101
});
102102
});
103103
});

packages/schematics/angular/e2e/schema.json

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@
66
"description": "Generates a new, generic end-to-end test definition for the given or default project.",
77
"long-description": "e2e-long.md",
88
"properties": {
9-
"projectRoot": {
10-
"description": "The root folder for the new test app.",
11-
"type": "string",
12-
"visible": false
13-
},
14-
"name": {
15-
"description": "The name of the new e2e app.",
16-
"type": "string",
17-
"format": "html-selector",
18-
"$default": {
19-
"$source": "argv",
20-
"index": 0
21-
}
22-
},
239
"rootSelector": {
2410
"description": "The HTML selector for the root component of the test app.",
2511
"type": "string",
@@ -31,7 +17,6 @@
3117
}
3218
},
3319
"required": [
34-
"name",
3520
"relatedAppName"
3621
]
3722
}

packages/schematics/angular/ng-new/index_spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ describe('Ng New Schematic', () => {
3737
'/bar/src/tsconfig.app.json',
3838
'/bar/src/main.ts',
3939
'/bar/src/app/app.module.ts',
40+
'/bar/e2e/src/app.po.ts',
41+
'/bar/e2e/src/app.e2e-spec.ts',
42+
'/bar/e2e/tsconfig.e2e.json',
43+
'/bar/e2e/protractor.conf.js',
4044
]));
4145
});
4246

@@ -68,13 +72,11 @@ describe('Ng New Schematic', () => {
6872
expect(files).not.toContain('/bar/src');
6973
});
7074

71-
it('minimal=true should not create e2e project', () => {
75+
it('minimal=true should not create an e2e target', () => {
7276
const options = { ...defaultOptions, minimal: true };
7377

7478
const tree = schematicRunner.runSchematic('ng-new', options);
75-
const files = tree.files;
76-
expect(files).not.toContain('/bar/e2e');
7779
const confContent = JSON.parse(tree.readContent('/bar/angular.json'));
78-
expect(confContent.projects['foo-e2e']).toBeUndefined();
80+
expect(confContent.projects.foo.e2e).toBeUndefined();
7981
});
8082
});

0 commit comments

Comments
 (0)