Skip to content

Commit

Permalink
feat(e2e): use protractor api (#4527)
Browse files Browse the repository at this point in the history
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 #4256
Fix #4478

BREAKING CHANGE: `ng e2e` no longer needs `ng serve` to be running.
  • Loading branch information
filipesilva authored and hansl committed Feb 9, 2017
1 parent 6e3186d commit 8d2d93a
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 78 deletions.
19 changes: 16 additions & 3 deletions docs/documentation/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
# ng e2e

## Overview
`ng e2e` executes end-to-end tests
`ng e2e` serves the application and runs end-to-end tests

### Running end-to-end tests

```bash
ng e2e
```

Before running the tests make sure you are serving the app via `ng serve`.

End-to-end tests are run via [Protractor](https://angular.github.io/protractor/).

## Options
`--config` (`-c`) use a specific config file. Defaults to the protractor config file in `angular-cli.json`.

`--specs` (`-sp`) override specs in the protractor config.
Can send in multiple specs by repeating flag (`ng e2e --specs=spec1.ts --specs=spec2.ts`).

`--element-explorer` (`-ee`) start Protractor's
[Element Explorer](https://github.com/angular/protractor/blob/master/docs/debugging.md#testing-out-protractor-interactively)
for debugging.

`--webdriver-update` (`-wu`) try to update webdriver.

`--serve` (`-s`) compile and serve the app.
All non-reload related serve options are also available (e.g. `--port=4400`).
3 changes: 1 addition & 2 deletions packages/@angular/cli/blueprints/ng2/files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"start": "ng serve",
"test": "ng test",
"lint": "ng lint",
"pree2e": "webdriver-manager update --standalone false --gecko false",
"e2e": "protractor"
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/@angular/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Version } from '../upgrade/version';
const Command = require('../ember-cli/lib/models/command');

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

availableOptions: BaseBuildCommandOptions.concat([
availableOptions: baseBuildCommandOptions.concat([
{ name: 'watch', type: Boolean, default: false, aliases: ['w'] }
]),

Expand Down
51 changes: 48 additions & 3 deletions packages/@angular/cli/commands/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
const Command = require('../ember-cli/lib/models/command');
const SilentError = require('silent-error');

import { CliConfig } from '../models/config';
import { ServeTaskOptions, baseServeCommandOptions } from './serve';
const Command = require('../ember-cli/lib/models/command');


export interface E2eTaskOptions extends ServeTaskOptions {
config: string;
serve: boolean;
webdriverUpdate: boolean;
specs: string[];
elementExplorer: boolean;
}

export const e2eCommandOptions = baseServeCommandOptions.concat([
{ name: 'config', type: String, aliases: ['c'] },
{ name: 'specs', type: Array, default: [], aliases: ['sp'] },
{ name: 'element-explorer', type: Boolean, default: false, aliases: ['ee'] },
{ name: 'webdriver-update', type: Boolean, default: true, aliases: ['wu'] },
{ name: 'serve', type: Boolean, default: true, aliases: ['s'] }
]);


const E2eCommand = Command.extend({
name: 'e2e',
aliases: ['e'],
description: 'Run e2e tests in existing project',
works: 'insideProject',
run: function () {
availableOptions: e2eCommandOptions,
run: function (commandOptions: E2eTaskOptions) {
const E2eTask = require('../tasks/e2e').E2eTask;
this.project.ngConfig = this.project.ngConfig || CliConfig.fromProject();

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

return e2eTask.run();
if (!commandOptions.config) {
const e2eConfig = CliConfig.fromProject().config.e2e;

if (!e2eConfig.protractor.config) {
throw new SilentError('No protractor config found in angular-cli.json.');
}

commandOptions.config = e2eConfig.protractor.config;
}

if (commandOptions.serve) {
const ServeTask = require('../tasks/serve').default;

const serve = new ServeTask({
ui: this.ui,
project: this.project,
});

// Protractor will end the proccess, so we don't need to kill the dev server
return serve.run(commandOptions, () => e2eTask.run(commandOptions));
} else {
return e2eTask.run(commandOptions);
}
}
});

Expand Down
46 changes: 25 additions & 21 deletions packages/@angular/cli/commands/serve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as denodeify from 'denodeify';
import { BuildOptions } from '../models/build-options';
import { BaseBuildCommandOptions } from './build';
import { baseBuildCommandOptions } from './build';
import { CliConfig } from '../models/config';
import { Version } from '../upgrade/version';
import { ServeTaskOptions } from './serve';
Expand Down Expand Up @@ -32,21 +32,35 @@ export interface ServeTaskOptions extends BuildOptions {
hmr?: boolean;
}

// Expose options unrelated to live-reload to other commands that need to run serve
export const baseServeCommandOptions: any = baseBuildCommandOptions.concat([
{ name: 'port', type: Number, default: defaultPort, aliases: ['p'] },
{
name: 'host',
type: String,
default: defaultHost,
aliases: ['H'],
description: `Listens only on ${defaultHost} by default`
},
{ name: 'proxy-config', type: 'Path', aliases: ['pc'] },
{ name: 'ssl', type: Boolean, default: false },
{ name: 'ssl-key', type: String, default: 'ssl/server.key' },
{ name: 'ssl-cert', type: String, default: 'ssl/server.crt' },
{
name: 'open',
type: Boolean,
default: false,
aliases: ['o'],
description: 'Opens the url in default browser',
}
]);

const ServeCommand = Command.extend({
name: 'serve',
description: 'Builds and serves your app, rebuilding on file changes.',
aliases: ['server', 's'],

availableOptions: BaseBuildCommandOptions.concat([
{ name: 'port', type: Number, default: defaultPort, aliases: ['p'] },
{
name: 'host',
type: String,
default: defaultHost,
aliases: ['H'],
description: `Listens only on ${defaultHost} by default`
},
{ name: 'proxy-config', type: 'Path', aliases: ['pc'] },
availableOptions: baseServeCommandOptions.concat([
{ name: 'live-reload', type: Boolean, default: true, aliases: ['lr'] },
{
name: 'live-reload-host',
Expand All @@ -72,16 +86,6 @@ const ServeCommand = Command.extend({
default: true,
description: 'Whether to live reload CSS (default true)'
},
{ name: 'ssl', type: Boolean, default: false },
{ name: 'ssl-key', type: String, default: 'ssl/server.key' },
{ name: 'ssl-cert', type: String, default: 'ssl/server.crt' },
{
name: 'open',
type: Boolean,
default: false,
aliases: ['o'],
description: 'Opens the url in default browser',
},
{
name: 'hmr',
type: Boolean,
Expand Down
60 changes: 42 additions & 18 deletions packages/@angular/cli/tasks/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
import * as url from 'url';

import { E2eTaskOptions } from '../commands/e2e';
import { requireProjectModule } from '../utilities/require-project-module';
const Task = require('../ember-cli/lib/models/task');
import * as chalk from 'chalk';
import {exec} from 'child_process';


export const E2eTask = Task.extend({
run: function () {
const ui = this.ui;
let exitCode = 0;

return new Promise((resolve) => {
exec(`npm run e2e -- ${this.project.ngConfig.config.e2e.protractor.config}`,
(err: NodeJS.ErrnoException, stdout: string, stderr: string) => {
ui.writeLine(stdout);
if (err) {
ui.writeLine(stderr);
ui.writeLine(chalk.red('Some end-to-end tests failed, see above.'));
exitCode = 1;
} else {
ui.writeLine(chalk.green('All end-to-end tests pass.'));
}
resolve(exitCode);
run: function (e2eTaskOptions: E2eTaskOptions) {
const projectRoot = this.project.root;
const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher');

return new Promise(function () {
let promise = Promise.resolve();
let additionalProtractorConfig: any = {
elementExplorer: e2eTaskOptions.elementExplorer
};

// use serve url as override for protractors baseUrl
if (e2eTaskOptions.serve) {
additionalProtractorConfig.baseUrl = url.format({
protocol: e2eTaskOptions.ssl ? 'https' : 'http',
hostname: e2eTaskOptions.host,
port: e2eTaskOptions.port.toString()
});
}

if (e2eTaskOptions.specs.length !== 0) {
additionalProtractorConfig['specs'] = e2eTaskOptions.specs;
}

if (e2eTaskOptions.webdriverUpdate) {
// webdriver-manager can only be accessed via a deep import from within
// protractor/node_modules. A double deep import if you will.
const webdriverUpdate = requireProjectModule(projectRoot,
'protractor/node_modules/webdriver-manager/built/lib/cmds/update');
// run `webdriver-manager update --standalone false --gecko false --quiet`
promise = promise.then(() => webdriverUpdate.program.run({
standalone: false,
gecko: false,
quiet: true
}));
}

// Don't call resolve(), protractor will manage exiting the process itself
return promise.then(() =>
protractorLauncher.init(e2eTaskOptions.config, additionalProtractorConfig));
});
}
});
4 changes: 2 additions & 2 deletions packages/@angular/cli/tasks/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as chalk from 'chalk';
import * as path from 'path';
import * as glob from 'glob';
import * as ts from 'typescript';
import { requireDependency } from '../utilities/require-project-module';
import { requireProjectModule } from '../utilities/require-project-module';
import { CliConfig } from '../models/config';
import { LintCommandOptions } from '../commands/lint';
import { oneLine } from 'common-tags';
Expand All @@ -30,7 +30,7 @@ export default Task.extend({
return Promise.resolve(0);
}

const tslint = requireDependency(projectRoot, 'tslint');
const tslint = requireProjectModule(projectRoot, 'tslint');
const Linter = tslint.Linter;
const Configuration = tslint.Configuration;

Expand Down
8 changes: 6 additions & 2 deletions packages/@angular/cli/tasks/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const SilentError = require('silent-error');
const opn = require('opn');

export default Task.extend({
run: function (serveTaskOptions: ServeTaskOptions) {
run: function (serveTaskOptions: ServeTaskOptions, rebuildDoneCb: any) {
const ui = this.ui;

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

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

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

if (rebuildDoneCb) {
webpackCompiler.plugin('done', rebuildDoneCb);
}

const statsConfig = getWebpackStatsConfig(serveTaskOptions.verbose);

let proxyConfig = {};
Expand Down
4 changes: 2 additions & 2 deletions packages/@angular/cli/tasks/test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const Task = require('../ember-cli/lib/models/task');
import { TestOptions } from '../commands/test';
import * as path from 'path';
import { requireDependency } from '../utilities/require-project-module';
import { requireProjectModule } from '../utilities/require-project-module';

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

let karmaOptions: any = Object.assign({}, options);
Expand Down
8 changes: 3 additions & 5 deletions packages/@angular/cli/utilities/require-project-module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as path from 'path';
const resolve = require('resolve');

// require dependencies within the target project
export function requireDependency(root: string, moduleName: string) {
const packageJson = require(path.join(root, 'node_modules', moduleName, 'package.json'));
const main = path.normalize(packageJson.main);
return require(path.join(root, 'node_modules', moduleName, main));
export function requireProjectModule(root: string, moduleName: string) {
return require(resolve.sync(moduleName, { basedir: root }));
}
6 changes: 3 additions & 3 deletions tests/e2e/tests/misc/minimal-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { writeFile, writeMultipleFiles } from '../../utils/fs';
import { runServeAndE2e } from '../test/e2e';
import { ng } from '../../utils/process';


export default function () {
Expand All @@ -15,7 +15,7 @@ export default function () {
}],
e2e: { protractor: { config: './protractor.conf.js' } }
})))
.then(() => runServeAndE2e())
.then(() => ng('e2e'))
.then(() => writeMultipleFiles({
'./src/script.js': `
document.querySelector('app-root').innerHTML = '<h1>app works!</h1>';
Expand All @@ -40,5 +40,5 @@ export default function () {
e2e: { protractor: { config: './protractor.conf.js' } }
}),
}))
.then(() => runServeAndE2e());
.then(() => ng('e2e'));
}
Loading

0 comments on commit 8d2d93a

Please sign in to comment.