Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(e2e): use protractor api #4527

Merged
merged 2 commits into from
Feb 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
Copy link
Contributor

@vikerman vikerman Feb 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be ok for now - This interface is not changing for now. Will see how we can make this public for the future.

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