Permalink
Browse files

fix(cucumber,mocha): the stageCue timeout is configurable

affects: serenity-js

Protractor requires Serenity/JS to notify it as soon as a test scenario is completed, which means
that Serenity/JS needs to wait for any outstanding tasks, such as saving screenshots to disk, before
it can do that.

The previous timeout of this operation was hard-coded to 10s, but when Serenity/JS
is used with BrowserStack, SauceLabs or any other remote Selenium grid, it might take a bit longer
for the screenshots to be downloaded, which means that the timeout was not sufficient.

This change introduces a new configurable property, which can be specified in Protractor config:

```
import { Duration } from 'serenity-js/lib/serenity/duration';

{
  serenity: {
    timeouts: {
      stageCue: Duration.ofSeconds(s: number)  // or Duration.ofMillis(ms: number)
    }
  }
}
```

The `stageCue` timeout defaults to 30s maximum.

ISSUES CLOSED: #34
  • Loading branch information...
jan-molak committed Apr 1, 2017
1 parent d0fa17e commit 256d29b7ee41173782b65571d9df250e50146d61
@@ -0,0 +1,101 @@
import expect = require('../expect');
import { Config } from '../../src/serenity/config';
interface Example {
path?: string;
mode?: 'dev' | 'prod';
extensions?: string[];
}
describe('Config', () => {
it('is a thin wrapper around the underlying object', () => {
const defaults = { path: '/temp' };
const config = new Config(defaults);
expect(config.get).to.deep.equal(defaults);
});
it('is immutable', () => {
const config = new Config({ path: '/temp' });
config.get.path = '/sys';
expect(config.get).to.deep.equal({
path: '/temp',
});
});
describe('merging', () => {
it('merges config objects', () => {
const config = new Config<Example>({ path: '/temp' }).mergedWith({ mode: 'dev' });
expect(config.get).to.deep.equal({
path: '/temp',
mode: 'dev',
});
});
it('creates a new Config object when two are merged', () => {
const config = new Config<Example>({ path: '/temp' });
const merged = config.mergedWith({ mode: 'dev' });
expect(config.get).to.not.deep.equal(merged.get);
expect(merged.get).to.deep.equal({
path: '/temp',
mode: 'dev',
});
});
it('concatenates list of properties', () => {
const config = new Config({ extensions: ['js', 'jsx'] }).mergedWith({ extensions: ['ts', 'tsx'] });
expect(config.get).to.deep.equal({
extensions: [
'js',
'jsx',
'ts',
'tsx',
],
});
});
});
describe('fallback', () => {
it('provides defaults', () => {
const config = new Config({ }).withFallback({ items: [] });
expect(config.get).to.deep.equal({
items: [],
});
});
it('creates a new Config object when two are merged', () => {
const config = new Config<Example>({ path: '/temp' });
const merged = config.withFallback({ mode: 'dev' });
expect(config.get).to.not.deep.equal(merged.get);
expect(merged.get).to.deep.equal({
path: '/temp',
mode: 'dev',
});
});
it('merges values, but overrides lists', () => {
const config = new Config({ extensions: ['js', 'jsx'] }).withFallback({ extensions: ['ts', 'tsx'] });
expect(config.get).to.deep.equal({
extensions: ['js', 'jsx'],
});
});
it('falls back to the defaults if no config is provided', () => {
expect(new Config(undefined).withFallback({items: []}).get).to.deep.equal({
items: [],
});
});
});
});
@@ -0,0 +1,20 @@
import expect = require('../expect');
import { Duration } from '../../src/serenity/duration';
describe('Duration', () => {
it('can represent a time interval in milliseconds', () => {
const duration = Duration.ofMillis(1000);
expect(duration.toMillis()).to.equal(1000);
});
it('can represent a time interval in seconds', () => {
const duration = Duration.ofSeconds(1);
expect(duration.toMillis()).to.equal(1000);
});
it('represents the interval in [ms] when converted to string', () => {
const duration = Duration.ofSeconds(1);
expect(duration.toString()).to.equal('1000ms');
});
});
@@ -12,18 +12,18 @@ import { ConsoleReporter } from '../../../../src/serenity/stage/console_reporter
import sinon = require('sinon');
import expect = require('../../../expect');
import { Duration } from '../../../../src/serenity/duration';
describe('serenity-protractor', () => {
describe('framework', () => {
describe('SerenityProtractorFramework', () => {
let serenity;
const serenity = new Serenity();
beforeEach(() => {
serenity = sinon.createStubInstance(Serenity);
});
beforeEach(() => { sinon.spy(serenity, 'configure'); });
afterEach(() => { (serenity.configure as SinonStub).restore(); });
it('can be instantiated with a default crew', () => {
@@ -34,14 +34,16 @@ describe('serenity-protractor', () => {
}));
expect(framework).to.be.instanceOf(SerenityProtractorFramework);
expect(serenity.assignCrewMembers).to.have.been.calledWith(
some(SerenityBDDReporter),
some(Photographer),
// core crew:
some(ProtractorReporter),
some(StandIns),
some(ProtractorNotifier),
);
expect(serenity.configure).to.have.been.calledWith(sinon.match({
crew: [
some(SerenityBDDReporter),
some(Photographer),
// core crew:
some(ProtractorReporter),
some(StandIns),
some(ProtractorNotifier),
],
}));
});
it('can be instantiated with a custom crew, which overrides the default one (except the "protractor core" crew members)', () => {
@@ -56,13 +58,39 @@ describe('serenity-protractor', () => {
}));
expect(framework).to.be.instanceOf(SerenityProtractorFramework);
expect(serenity.assignCrewMembers).to.have.been.calledWith(
some(ConsoleReporter),
// core crew:
some(ProtractorReporter),
some(StandIns),
some(ProtractorNotifier),
);
expect(serenity.configure).to.have.been.calledWith(sinon.match({
crew: [
some(ConsoleReporter),
// core crew:
some(ProtractorReporter),
some(StandIns),
some(ProtractorNotifier),
],
}));
});
it('provides some sensible timeout defaults', () => {
const framework = new SerenityProtractorFramework(serenity, protractorRunner.withConfiguration({
serenity: {
dialect: 'mocha',
},
}));
expect(serenity.configure).to.have.been.calledWith(sinon.match({
timeouts: {
stageCue: some(Duration),
},
}));
});
it('advises the developer what to do if the dialect could not be detected automatically', () => {
expect(() => {
const framework = new SerenityProtractorFramework(serenity, protractorRunner.withConfiguration({}));
}).to.throw([
'Serenity/JS could not determine the test dialect you wish to use. ',
'Please add `serenity: { dialect: \'...\' }` to your Protractor configuration ',
'file and choose one of the following options: cucumber, mocha.',
].join(''));
});
});
@@ -0,0 +1,37 @@
import { Serenity } from '../../src/serenity/serenity';
import expect = require('../expect');
import { DomainEvent } from '../../src/serenity/domain/events';
import { Stage } from '../../src/serenity/stage/stage';
import { StageCrewMember } from '../../src/serenity/stage/stage_manager';
class BestBoy implements StageCrewMember {
private assignToCalled = false;
assigned = (): boolean => this.assignToCalled;
assignTo = (stage: Stage) => this.assignToCalled = true;
notifyOf = (event: DomainEvent<any>) => undefined; // no-op
}
describe('serenity', () => {
it('is initialised with no stage crew', () => {
const serenity = new Serenity();
expect(serenity.config.crew).to.be.empty;
});
it('can have the stage crew configured', () => {
const crewMember = new BestBoy();
const serenity = new Serenity();
serenity.configure({
crew: [
crewMember,
],
});
expect(serenity.config.crew).to.have.lengthOf(1);
expect(serenity.config.crew).to.contain(crewMember);
expect(crewMember.assigned()).to.be.true;
});
});
@@ -3,5 +3,5 @@ import { serenity } from '../index';
// wait for any tasks outstanding after the previous scenario
// see https://github.com/angular/protractor/issues/4087
export = function() {
this.Before({timeout: 30 * 1000}, () => serenity.stageManager().waitForNextCue());
this.Before({ timeout: serenity.config.timeouts.stageCue.toMillis() }, () => serenity.stageManager().waitForNextCue());
};
@@ -3,6 +3,6 @@ import { serenity } from '../index';
// wait for any tasks outstanding after the previous scenario
// see https://github.com/angular/protractor/issues/4087
beforeEach(function() {
this.timeout(30 * 1000);
this.timeout(serenity.config.timeouts.stageCue.toMillis());
return serenity.stageManager().waitForNextCue();
});

This file was deleted.

Oops, something went wrong.
@@ -1,5 +1,5 @@
import { SerenityProtractorFramework } from './serenity_protractor_framework';
export { run, SerenityProtractorFramework } from './serenity_protractor_framework';
export { Config } from './config';
export { SerenityFrameworkConfig } from './serenity_framework_config';
export { TestFrameworkAdapter } from './test_framework_adapter';
@@ -1,5 +1,5 @@
import { Config, Runner } from 'protractor';
import { serenity, Serenity } from '../..';
import { Config as ProtractorConfig, Runner } from 'protractor';
import { Config, serenity, Serenity } from '../..';
import { serenityBDDReporter } from '../../serenity/reporting';
import { ProtractorReport, ProtractorReporter } from '../reporting';
import { ProtractorNotifier } from '../reporting/protractor_notifier';
@@ -10,6 +10,7 @@ import { TestFrameworkAdapter } from './test_framework_adapter';
import { TestFrameworkDetector } from './test_framework_detector';
import _ = require('lodash');
import { Duration } from '../../serenity/duration';
// spec: https://github.com/angular/protractor/blob/master/lib/frameworks/README.md
@@ -25,20 +26,25 @@ export class SerenityProtractorFramework {
private framework: TestFrameworkAdapter;
private reporter: ProtractorReporter;
private onComplete = noop;
private detect = new TestFrameworkDetector();
constructor(private serenity: Serenity, private runner: Runner) {
this.config = this.defaultsWith(runner.getConfig());
const protractorConfig = runner.getConfig() as SerenityFrameworkConfig;
this.reporter = new ProtractorReporter(runner);
this.framework = this.detect.frameworkFor(this.config);
this.framework = this.detect.frameworkFor(protractorConfig);
this.serenity.assignCrewMembers(...this.config.serenity.crew.concat([
this.reporter,
new StandIns(),
new ProtractorNotifier(runner),
]));
this.onComplete = protractorConfig.onComplete || noop;
serenity.configure(this.withFallback(protractorConfig).mergedWith({
crew: [
this.reporter,
new StandIns(),
new ProtractorNotifier(runner),
],
}).get);
}
run = (specs: string[]): PromiseLike<ProtractorReport> => this.runner.runTestPreparer(this.detect.supportedCLIParams()).
@@ -56,24 +62,18 @@ export class SerenityProtractorFramework {
// so we don't need any additional error handling here.
})
private waitForOtherProtractorPlugins = () => Promise.resolve(this.config.onComplete || noop);
private defaultsWith = (overrides: Config): SerenityFrameworkConfig => _.mergeWith(this.defaults(), overrides, this.mergeButOverrideLists);
private waitForOtherProtractorPlugins = () => Promise.resolve(this.onComplete);
private mergeButOverrideLists = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue;
}
}
private defaults = () => ({
serenity: {
crew: [
/// [default-stage-crew-members]
serenityBDDReporter(),
photographer(),
/// [default-stage-crew-members]
],
// tslint:disable-next-line:no-string-literal that's how, by design, you access custom properties in Protractor
private withFallback = (pc: ProtractorConfig) => new Config(pc['serenity']).withFallback({
timeouts: {
stageCue: Duration.ofSeconds(30),
},
crew: [
/// [default-stage-crew-members]
serenityBDDReporter(),
photographer(),
/// [default-stage-crew-members]
],
})
}
@@ -0,0 +1,30 @@
import _ = require('lodash');
export class Config<T> {
constructor(private config: T) {
}
get get(): T {
return _.cloneDeep(this.config);
}
mergedWith(otherConfig: T): Config<T> {
return new Config(_.mergeWith({}, this.config, otherConfig, this.mergeValuesAndConcatenateLists));
}
withFallback(defaults: T): Config<T> {
return new Config(_.mergeWith({}, defaults, this.config, this.mergeValuesButOverrideLists));
}
private mergeValuesAndConcatenateLists = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return objValue.concat(srcValue);
}
}
private mergeValuesButOverrideLists = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue;
}
}
}
Oops, something went wrong.

0 comments on commit 256d29b

Please sign in to comment.