Skip to content

Commit 98d1714

Browse files
committed
feat(e2e): use protractor api
Uses existing Protractor API to run it directly instead of using `npm run e2e`. Also adds support for the following flags: `--serve`, `--config`, `--specs`, `--element-explorer`, `--webdriver-update`. Fix angular#4256 Fix angular#4478 BREAKING CHANGE: `ng e2e` no longer needs `ng serve` to be running.
1 parent 6e3186d commit 98d1714

File tree

12 files changed

+181
-78
lines changed

12 files changed

+181
-78
lines changed

docs/documentation/e2e.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@
33
# ng e2e
44

55
## Overview
6-
`ng e2e` executes end-to-end tests
6+
`ng e2e` serves the application and runs end-to-end tests
77

88
### Running end-to-end tests
99

1010
```bash
1111
ng e2e
1212
```
1313

14-
Before running the tests make sure you are serving the app via `ng serve`.
15-
1614
End-to-end tests are run via [Protractor](https://angular.github.io/protractor/).
15+
16+
## Options
17+
`--config` (`-c`) use a specific config file. Defaults to the protractor config file in `angular-cli.json`.
18+
19+
`--specs` (`-sp`) override specs in the protractor config.
20+
Can send in multiple specs by repeating flag (`ng e2e --specs=spec1.ts --specs=spec2.ts`).
21+
22+
`--element-explorer` (`-ee`) start Protractor's
23+
[Element Explorer](https://github.com/angular/protractor/blob/master/docs/debugging.md#testing-out-protractor-interactively)
24+
for debugging.
25+
26+
`--webdriver-update` (`-wu`) try to update webdriver.
27+
28+
`--serve` (`-s`) compile and serve the app.
29+
All non-reload related serve options are also available (e.g. `--port=4400`).

packages/@angular/cli/blueprints/ng2/files/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
"start": "ng serve",
99
"test": "ng test",
1010
"lint": "ng lint",
11-
"pree2e": "webdriver-manager update --standalone false --gecko false",
12-
"e2e": "protractor"
11+
"e2e": "ng e2e"
1312
},
1413
"private": true,
1514
"dependencies": {

packages/@angular/cli/commands/build.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Version } from '../upgrade/version';
44
const Command = require('../ember-cli/lib/models/command');
55

66
// defaults for BuildOptions
7-
export const BaseBuildCommandOptions: any = [
7+
export const baseBuildCommandOptions: any = [
88
{
99
name: 'target',
1010
type: String,
@@ -42,7 +42,7 @@ const BuildCommand = Command.extend({
4242
description: 'Builds your app and places it into the output path (dist/ by default).',
4343
aliases: ['b'],
4444

45-
availableOptions: BaseBuildCommandOptions.concat([
45+
availableOptions: baseBuildCommandOptions.concat([
4646
{ name: 'watch', type: Boolean, default: false, aliases: ['w'] }
4747
]),
4848

packages/@angular/cli/commands/e2e.ts

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
1-
const Command = require('../ember-cli/lib/models/command');
1+
const SilentError = require('silent-error');
2+
23
import { CliConfig } from '../models/config';
4+
import { ServeTaskOptions, baseServeCommandOptions } from './serve';
5+
const Command = require('../ember-cli/lib/models/command');
6+
7+
8+
export interface E2eTaskOptions extends ServeTaskOptions {
9+
config: string;
10+
serve: boolean;
11+
webdriverUpdate: boolean;
12+
specs: string[];
13+
elementExplorer: boolean;
14+
}
15+
16+
export const e2eCommandOptions = baseServeCommandOptions.concat([
17+
{ name: 'config', type: String, aliases: ['c'] },
18+
{ name: 'specs', type: Array, default: [], aliases: ['sp'] },
19+
{ name: 'element-explorer', type: Boolean, default: false, aliases: ['ee'] },
20+
{ name: 'webdriver-update', type: Boolean, default: true, aliases: ['wu'] },
21+
{ name: 'serve', type: Boolean, default: true, aliases: ['s'] }
22+
]);
23+
324

425
const E2eCommand = Command.extend({
526
name: 'e2e',
27+
aliases: ['e'],
628
description: 'Run e2e tests in existing project',
729
works: 'insideProject',
8-
run: function () {
30+
availableOptions: e2eCommandOptions,
31+
run: function (commandOptions: E2eTaskOptions) {
932
const E2eTask = require('../tasks/e2e').E2eTask;
1033
this.project.ngConfig = this.project.ngConfig || CliConfig.fromProject();
1134

@@ -14,7 +37,29 @@ const E2eCommand = Command.extend({
1437
project: this.project
1538
});
1639

17-
return e2eTask.run();
40+
if (!commandOptions.config) {
41+
const e2eConfig = CliConfig.fromProject().config.e2e;
42+
43+
if (!e2eConfig.protractor.config) {
44+
throw new SilentError('No protractor config found in angular-cli.json.');
45+
}
46+
47+
commandOptions.config = e2eConfig.protractor.config;
48+
}
49+
50+
if (commandOptions.serve) {
51+
const ServeTask = require('../tasks/serve').default;
52+
53+
const serve = new ServeTask({
54+
ui: this.ui,
55+
project: this.project,
56+
});
57+
58+
// Protractor will end the proccess, so we don't need to kill the dev server
59+
return serve.run(commandOptions, () => e2eTask.run(commandOptions));
60+
} else {
61+
return e2eTask.run(commandOptions);
62+
}
1863
}
1964
});
2065

packages/@angular/cli/commands/serve.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as denodeify from 'denodeify';
22
import { BuildOptions } from '../models/build-options';
3-
import { BaseBuildCommandOptions } from './build';
3+
import { baseBuildCommandOptions } from './build';
44
import { CliConfig } from '../models/config';
55
import { Version } from '../upgrade/version';
66
import { ServeTaskOptions } from './serve';
@@ -32,21 +32,35 @@ export interface ServeTaskOptions extends BuildOptions {
3232
hmr?: boolean;
3333
}
3434

35+
// Expose options unrelated to live-reload to other commands that need to run serve
36+
export const baseServeCommandOptions: any = baseBuildCommandOptions.concat([
37+
{ name: 'port', type: Number, default: defaultPort, aliases: ['p'] },
38+
{
39+
name: 'host',
40+
type: String,
41+
default: defaultHost,
42+
aliases: ['H'],
43+
description: `Listens only on ${defaultHost} by default`
44+
},
45+
{ name: 'proxy-config', type: 'Path', aliases: ['pc'] },
46+
{ name: 'ssl', type: Boolean, default: false },
47+
{ name: 'ssl-key', type: String, default: 'ssl/server.key' },
48+
{ name: 'ssl-cert', type: String, default: 'ssl/server.crt' },
49+
{
50+
name: 'open',
51+
type: Boolean,
52+
default: false,
53+
aliases: ['o'],
54+
description: 'Opens the url in default browser',
55+
}
56+
]);
57+
3558
const ServeCommand = Command.extend({
3659
name: 'serve',
3760
description: 'Builds and serves your app, rebuilding on file changes.',
3861
aliases: ['server', 's'],
3962

40-
availableOptions: BaseBuildCommandOptions.concat([
41-
{ name: 'port', type: Number, default: defaultPort, aliases: ['p'] },
42-
{
43-
name: 'host',
44-
type: String,
45-
default: defaultHost,
46-
aliases: ['H'],
47-
description: `Listens only on ${defaultHost} by default`
48-
},
49-
{ name: 'proxy-config', type: 'Path', aliases: ['pc'] },
63+
availableOptions: baseServeCommandOptions.concat([
5064
{ name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] },
5165
{
5266
name: 'live-reload-host',
@@ -72,16 +86,6 @@ const ServeCommand = Command.extend({
7286
default: true,
7387
description: 'Whether to live reload CSS (default true)'
7488
},
75-
{ name: 'ssl', type: Boolean, default: false },
76-
{ name: 'ssl-key', type: String, default: 'ssl/server.key' },
77-
{ name: 'ssl-cert', type: String, default: 'ssl/server.crt' },
78-
{
79-
name: 'open',
80-
type: Boolean,
81-
default: false,
82-
aliases: ['o'],
83-
description: 'Opens the url in default browser',
84-
},
8589
{
8690
name: 'hmr',
8791
type: Boolean,

packages/@angular/cli/tasks/e2e.ts

+42-18
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,50 @@
1+
import * as url from 'url';
2+
3+
import { E2eTaskOptions } from '../commands/e2e';
4+
import { requireProjectModule } from '../utilities/require-project-module';
15
const Task = require('../ember-cli/lib/models/task');
2-
import * as chalk from 'chalk';
3-
import {exec} from 'child_process';
46

57

68
export const E2eTask = Task.extend({
7-
run: function () {
8-
const ui = this.ui;
9-
let exitCode = 0;
10-
11-
return new Promise((resolve) => {
12-
exec(`npm run e2e -- ${this.project.ngConfig.config.e2e.protractor.config}`,
13-
(err: NodeJS.ErrnoException, stdout: string, stderr: string) => {
14-
ui.writeLine(stdout);
15-
if (err) {
16-
ui.writeLine(stderr);
17-
ui.writeLine(chalk.red('Some end-to-end tests failed, see above.'));
18-
exitCode = 1;
19-
} else {
20-
ui.writeLine(chalk.green('All end-to-end tests pass.'));
21-
}
22-
resolve(exitCode);
9+
run: function (e2eTaskOptions: E2eTaskOptions) {
10+
const projectRoot = this.project.root;
11+
const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher');
12+
13+
return new Promise(function () {
14+
let promise = Promise.resolve();
15+
let additionalProtractorConfig: any = {
16+
elementExplorer: e2eTaskOptions.elementExplorer
17+
};
18+
19+
// use serve url as override for protractors baseUrl
20+
if (e2eTaskOptions.serve) {
21+
additionalProtractorConfig.baseUrl = url.format({
22+
protocol: e2eTaskOptions.ssl ? 'https' : 'http',
23+
hostname: e2eTaskOptions.host,
24+
port: e2eTaskOptions.port.toString()
2325
});
26+
}
27+
28+
if (e2eTaskOptions.specs.length !== 0) {
29+
additionalProtractorConfig['specs'] = e2eTaskOptions.specs;
30+
}
31+
32+
if (e2eTaskOptions.webdriverUpdate) {
33+
// webdriver-manager can only be accessed via a deep import from within
34+
// protractor/node_modules. A double deep import if you will.
35+
const webdriverUpdate = requireProjectModule(projectRoot,
36+
'protractor/node_modules/webdriver-manager/built/lib/cmds/update');
37+
// run `webdriver-manager update --standalone false --gecko false --quiet`
38+
promise = promise.then(() => webdriverUpdate.program.run({
39+
standalone: false,
40+
gecko: false,
41+
quiet: true
42+
}));
43+
}
44+
45+
// Don't call resolve(), protractor will manage exiting the process itself
46+
return promise.then(() =>
47+
protractorLauncher.init(e2eTaskOptions.config, additionalProtractorConfig));
2448
});
2549
}
2650
});

packages/@angular/cli/tasks/lint.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as chalk from 'chalk';
33
import * as path from 'path';
44
import * as glob from 'glob';
55
import * as ts from 'typescript';
6-
import { requireDependency } from '../utilities/require-project-module';
6+
import { requireProjectModule } from '../utilities/require-project-module';
77
import { CliConfig } from '../models/config';
88
import { LintCommandOptions } from '../commands/lint';
99
import { oneLine } from 'common-tags';
@@ -30,7 +30,7 @@ export default Task.extend({
3030
return Promise.resolve(0);
3131
}
3232

33-
const tslint = requireDependency(projectRoot, 'tslint');
33+
const tslint = requireProjectModule(projectRoot, 'tslint');
3434
const Linter = tslint.Linter;
3535
const Configuration = tslint.Configuration;
3636

packages/@angular/cli/tasks/serve.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const SilentError = require('silent-error');
1616
const opn = require('opn');
1717

1818
export default Task.extend({
19-
run: function (serveTaskOptions: ServeTaskOptions) {
19+
run: function (serveTaskOptions: ServeTaskOptions, rebuildDoneCb: any) {
2020
const ui = this.ui;
2121

2222
let webpackCompiler: any;
@@ -25,7 +25,7 @@ export default Task.extend({
2525

2626
const outputPath = serveTaskOptions.outputPath || appConfig.outDir;
2727
if (this.project.root === outputPath) {
28-
throw new SilentError ('Output path MUST not be project root directory!');
28+
throw new SilentError('Output path MUST not be project root directory!');
2929
}
3030
rimraf.sync(path.resolve(this.project.root, outputPath));
3131

@@ -67,6 +67,10 @@ export default Task.extend({
6767
webpackConfig.entry.main.unshift(...entryPoints);
6868
webpackCompiler = webpack(webpackConfig);
6969

70+
if (rebuildDoneCb) {
71+
webpackCompiler.plugin('done', rebuildDoneCb);
72+
}
73+
7074
const statsConfig = getWebpackStatsConfig(serveTaskOptions.verbose);
7175

7276
let proxyConfig = {};

packages/@angular/cli/tasks/test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
const Task = require('../ember-cli/lib/models/task');
22
import { TestOptions } from '../commands/test';
33
import * as path from 'path';
4-
import { requireDependency } from '../utilities/require-project-module';
4+
import { requireProjectModule } from '../utilities/require-project-module';
55

66
export default Task.extend({
77
run: function (options: TestOptions) {
88
const projectRoot = this.project.root;
99
return new Promise((resolve) => {
10-
const karma = requireDependency(projectRoot, 'karma');
10+
const karma = requireProjectModule(projectRoot, 'karma');
1111
const karmaConfig = path.join(projectRoot, this.project.ngConfig.config.test.karma.config);
1212

1313
let karmaOptions: any = Object.assign({}, options);
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import * as path from 'path';
1+
const resolve = require('resolve');
22

33
// require dependencies within the target project
4-
export function requireDependency(root: string, moduleName: string) {
5-
const packageJson = require(path.join(root, 'node_modules', moduleName, 'package.json'));
6-
const main = path.normalize(packageJson.main);
7-
return require(path.join(root, 'node_modules', moduleName, main));
4+
export function requireProjectModule(root: string, moduleName: string) {
5+
return require(resolve.sync(moduleName, { basedir: root }));
86
}

tests/e2e/tests/misc/minimal-config.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { writeFile, writeMultipleFiles } from '../../utils/fs';
2-
import { runServeAndE2e } from '../test/e2e';
2+
import { ng } from '../../utils/process';
33

44

55
export default function () {
@@ -15,7 +15,7 @@ export default function () {
1515
}],
1616
e2e: { protractor: { config: './protractor.conf.js' } }
1717
})))
18-
.then(() => runServeAndE2e())
18+
.then(() => ng('e2e'))
1919
.then(() => writeMultipleFiles({
2020
'./src/script.js': `
2121
document.querySelector('app-root').innerHTML = '<h1>app works!</h1>';
@@ -40,5 +40,5 @@ export default function () {
4040
e2e: { protractor: { config: './protractor.conf.js' } }
4141
}),
4242
}))
43-
.then(() => runServeAndE2e());
43+
.then(() => ng('e2e'));
4444
}

0 commit comments

Comments
 (0)