diff --git a/packages/schematics/angular/application/files/root/tsconfig.app.json.template b/packages/schematics/angular/application/files/root/tsconfig.app.json.template index e8911503de66..d3cef1c5aa03 100644 --- a/packages/schematics/angular/application/files/root/tsconfig.app.json.template +++ b/packages/schematics/angular/application/files/root/tsconfig.app.json.template @@ -6,7 +6,8 @@ }, "exclude": [ "test.ts", - "**/*.spec.ts" + "**/*.spec.ts", + "e2e/**" ]<% if (enableIvy) { %>, "angularCompilerOptions": { "enableIvy": true diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 6163f83c7ed1..bce0c4530d5e 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -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); diff --git a/packages/schematics/angular/e2e/files/protractor.conf.js.template b/packages/schematics/angular/e2e/files/protractor.conf.js.template index 86776a391a5b..9748e3d2930f 100644 --- a/packages/schematics/angular/e2e/files/protractor.conf.js.template +++ b/packages/schematics/angular/e2e/files/protractor.conf.js.template @@ -21,7 +21,7 @@ exports.config = { }, onPrepare() { require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.e2e.json') + project: require('path').join(__dirname, './tsconfig.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } diff --git a/packages/schematics/angular/e2e/files/tsconfig.e2e.json.template b/packages/schematics/angular/e2e/files/tsconfig.e2e.json.template deleted file mode 100644 index a5e0455e5ff4..000000000000 --- a/packages/schematics/angular/e2e/files/tsconfig.e2e.json.template +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "<%= appDir.split('/').map(x => '..').join('/') %>/tsconfig.json", - "compilerOptions": { - "outDir": "<%= appDir.split('/').map(x => '..').join('/') %>/out-tsc/app", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} \ No newline at end of file diff --git a/packages/schematics/angular/e2e/files/tsconfig.json.template b/packages/schematics/angular/e2e/files/tsconfig.json.template new file mode 100644 index 000000000000..5df5f432cbca --- /dev/null +++ b/packages/schematics/angular/e2e/files/tsconfig.json.template @@ -0,0 +1,13 @@ +{ + "extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/e2e", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/packages/schematics/angular/e2e/index.ts b/packages/schematics/angular/e2e/index.ts index e168a9e91507..9445f2288303 100644 --- a/packages/schematics/angular/e2e/index.ts +++ b/packages/schematics/angular/e2e/index.ts @@ -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 { - return (host: Tree, context: SchematicContext) => { - let projectRoot = options.projectRoot !== undefined - ? options.projectRoot - : `${workspace.newProjectRoot}/${options.name}`; +function getE2eRoot(projectRoot: string): string { + const root = projectRoot.split('/').filter(x => x).join('/'); - if (projectRoot !== '' && !projectRoot.endsWith('/')) { - projectRoot += '/'; - } + return root ? root + '/e2e' : 'e2e'; +} - if (getProject(workspace, options.name)) { - throw new SchematicsException(`Project name "${options.name}" already exists.`); - } +function AddBuilderToWorkspace(options: E2eOptions, workspace: WorkspaceSchema): Rule { + return (host: Tree, context: SchematicContext) => { + const appProject = options.relatedAppName; + const project = getProject(workspace, appProject); + const architect = project.architect; - 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`, - }, - }, + const projectRoot = getE2eRoot(project.root); + + if (architect) { + architect.e2e = { + builder: Builders.Protractor, + options: { + protractorConfig: `${projectRoot}/protractor.conf.js`, + devServerTarget: `${options.relatedAppName}:serve`, }, - lint: { - builder: Builders.TsLint, - options: { - tsConfig: `${projectRoot}tsconfig.e2e.json`, - exclude: [ - '**/node_modules/**', - ], + configurations: { + production: { + devServerTarget: `${options.relatedAppName}:serve:production`, }, }, - }, - }; - - workspace.projects[options.name] = 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[] = []; + const lintConfig = architect.lint; + if (lintConfig) { + lintConfig.options.tsConfig = + lintConfig.options.tsConfig.concat(`${projectRoot}/tsconfig.json`); + } - parts.forEach(part => { - if (part.match(projectNameRegexp)) { - matched.push(part); + workspace.projects[options.relatedAppName] = project; } - }); - - 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.`); - } + return updateWorkspace(workspace); + }; } 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), ])), ]); }; diff --git a/packages/schematics/angular/e2e/index_spec.ts b/packages/schematics/angular/e2e/index_spec.ts index 6ac8e78036aa..9cc765bdecb5 100644 --- a/packages/schematics/angular/e2e/index_spec.ts +++ b/packages/schematics/angular/e2e/index_spec.ts @@ -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 = { + relatedAppName: 'foo', + }; + + const defaultAppOptions: ApplicationOptions = { name: 'foo', - relatedAppName: 'app', + inlineStyle: true, + inlineTemplate: true, + routing: false, + skipPackageJson: false, + minimal: true, }; - let workspaceTree: UnitTestTree; + 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.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.json', + ]); }); }); }); diff --git a/packages/schematics/angular/e2e/schema.json b/packages/schematics/angular/e2e/schema.json index 89201ad387e9..a65ba1364f0f 100644 --- a/packages/schematics/angular/e2e/schema.json +++ b/packages/schematics/angular/e2e/schema.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" ] } diff --git a/packages/schematics/angular/ng-new/index_spec.ts b/packages/schematics/angular/ng-new/index_spec.ts index dfd5a3824066..772ab5b1fd39 100644 --- a/packages/schematics/angular/ng-new/index_spec.ts +++ b/packages/schematics/angular/ng-new/index_spec.ts @@ -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.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(); }); }); diff --git a/tests/legacy-cli/e2e/tests/basic/e2e.ts b/tests/legacy-cli/e2e/tests/basic/e2e.ts index 1051adc17996..fa3100915d46 100644 --- a/tests/legacy-cli/e2e/tests/basic/e2e.ts +++ b/tests/legacy-cli/e2e/tests/basic/e2e.ts @@ -19,18 +19,18 @@ export default function () { scripts['wd:clean'] = 'webdriver-manager clean'; }) .then(() => npm('run', 'wd:clean')) - .then(() => expectToFail(() => ng('e2e', 'test-project-e2e', '--no-webdriver-update', '--devServerTarget='))) + .then(() => expectToFail(() => ng('e2e', 'test-project', '--no-webdriver-update', '--devServerTarget='))) // Add back the pre-defined version of webdriver. This script is defined when making projects. .then(() => npm('run', 'webdriver-update')) // Should fail without serving - .then(() => expectToFail(() => ng('e2e', 'test-project-e2e', '--devServerTarget='))) + .then(() => expectToFail(() => ng('e2e', 'test-project', '--devServerTarget='))) // These should work. - .then(() => ng('e2e', 'test-project-e2e')) - .then(() => ng('e2e', 'test-project-e2e', '--devServerTarget=test-project:serve:production')) + .then(() => ng('e2e', 'test-project')) + .then(() => ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production')) // Should accept different config file .then(() => moveFile('./e2e/protractor.conf.js', './e2e/renamed-protractor.conf.js')) - .then(() => ng('e2e', 'test-project-e2e', + .then(() => ng('e2e', 'test-project', '--protractorConfig=e2e/renamed-protractor.conf.js')) .then(() => moveFile('./e2e/renamed-protractor.conf.js', './e2e/protractor.conf.js')) // Should accept different multiple spec files @@ -38,7 +38,7 @@ export default function () { './e2e/src/renamed-app.e2e-spec.ts')) .then(() => copyFile('./e2e/src/renamed-app.e2e-spec.ts', './e2e/src/another-app.e2e-spec.ts')) - .then(() => ng('e2e', 'test-project-e2e', '--specs', './e2e/renamed-app.e2e-spec.ts', + .then(() => ng('e2e', 'test-project', '--specs', './e2e/renamed-app.e2e-spec.ts', '--specs', './e2e/another-app.e2e-spec.ts')) // Rename the spec back to how it was. .then(() => moveFile('./e2e/src/renamed-app.e2e-spec.ts', @@ -50,7 +50,7 @@ export default function () { app: './e2e/src/app.e2e-spec.ts' }, `)) - .then(() => ng('e2e', 'test-project-e2e', '--suite=app')) + .then(() => ng('e2e', 'test-project', '--suite=app')) // Remove suites block from protractor.conf.js file after testing suites .then(() => replaceInFile('e2e/protractor.conf.js', `allScriptsTimeout: 11000, suites: { @@ -59,7 +59,7 @@ export default function () { `, `allScriptsTimeout: 11000,` )) // Should start up Element Explorer - .then(() => execAndWaitForOutputToMatch('ng', ['e2e', 'test-project-e2e', '--element-explorer'], + .then(() => execAndWaitForOutputToMatch('ng', ['e2e', 'test-project', '--element-explorer'], /Element Explorer/)) .then(() => killAllProcesses(), (err: any) => { killAllProcesses(); @@ -68,7 +68,7 @@ export default function () { // Should run side-by-side with `ng serve` .then(() => execAndWaitForOutputToMatch('ng', ['serve'], /: Compiled successfully./)) - .then(() => ng('e2e', 'test-project-e2e', '--devServerTarget=')) + .then(() => ng('e2e', 'test-project', '--devServerTarget=')) .then(() => killAllProcesses(), (err: any) => { killAllProcesses(); throw err; diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts b/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts index 340f4ac4807b..e5459bb9e185 100644 --- a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts +++ b/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts @@ -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')); } diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts index 1f480e69cdef..127b88b18868 100644 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts +++ b/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts @@ -16,15 +16,16 @@ export default function() { .then(() => ng('generate', 'directive', 'test2-directive')) .then(() => expectFileToMatch(join(directiveDir, 'test2-directive.directive.ts'), /selector: '\[preW/)) + .then(() => ng('generate', 'application', 'app-two', '--skip-install')) .then(() => updateJsonFile('angular.json', configJson => { configJson.projects['test-project'].schematics = { '@schematics/angular:directive': { prefix: 'preP' } }; })) - .then(() => process.chdir('e2e/src')) + .then(() => process.chdir('projects/app-two')) .then(() => ng('generate', 'directive', '--skip-import', 'test3-directive')) .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(join('e2e', 'src', 'test3-directive.directive.ts'), + .then(() => expectFileToMatch(join('projects', 'app-two', 'test3-directive.directive.ts'), /selector: '\[preW/)) .then(() => process.chdir('src/app')) .then(() => ng('generate', 'directive', 'test-directive')) diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index 6d63c6792f7c..1a64118a99c4 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -48,7 +48,7 @@ export async function createProject(name: string, ...args: string[]) { } export async function prepareProjectForE2e(name) { - const argv: any = getGlobalVariable('argv'); + const argv: string[] = getGlobalVariable('argv'); await git('config', 'user.email', 'angular-core+e2e@google.com'); await git('config', 'user.name', 'Angular CLI E2e'); @@ -209,9 +209,9 @@ export function useCIDefaults(projectName = 'test-project') { // Use the CI chrome setup in karma. appTargets.test.options.browsers = 'ChromeHeadlessCI'; // Disable auto-updating webdriver in e2e. - const e2eProject = workspaceJson.projects[projectName + '-e2e']; - const e2eTargets = e2eProject.targets || e2eProject.architect; - e2eTargets.e2e.options.webdriverUpdate = false; + if (appTargets.e2e) { + appTargets.e2e.options.webdriverUpdate = false; + } }) .then(() => updateJsonFile('package.json', json => { // Use matching versions of Chrome and Webdriver.