Skip to content
Permalink
Browse files
fix(protractor): support restarting the browser between test scenarios
  • Loading branch information
jan-molak committed Jan 19, 2020
1 parent fcba7b5 commit 21b5a4187dbbc9babf70e75cda8b42e5e2531d17
@@ -0,0 +1,34 @@
const
{ Actor } = require('@serenity-js/core'),
{ BrowseTheWeb, UseAngular, Navigate } = require('@serenity-js/protractor'),
{ protractor } = require('protractor');

describe('Jasmine', () => {

describe('A scenario', () => {

let Jasmine;

beforeEach(() => {
require('fs').appendFileSync(require('path').resolve(process.cwd(), 'jasmine.log'), ` >> [spec] new actor created\n`);
Jasmine = Actor.named('Jasmine').whoCan(
BrowseTheWeb.using(protractor.browser),
);
});

it('passes the first time', () => Jasmine.attemptsTo(
UseAngular.disableSynchronisation(),
Navigate.to('chrome://version/'),
));

it('passes the second time', () => Jasmine.attemptsTo(
UseAngular.disableSynchronisation(),
Navigate.to('chrome://accessibility/'),
));

it('passes the third time', () => Jasmine.attemptsTo(
UseAngular.disableSynchronisation(),
Navigate.to('chrome://chrome-urls/'),
));
});
});
@@ -1,6 +1,6 @@
const
{ Actor } = require('@serenity-js/core'),
{ BrowseTheWeb, Navigate } = require('@serenity-js/protractor'),
{ BrowseTheWeb, Navigate, UseAngular } = require('@serenity-js/protractor'),
{ protractor } = require('protractor');

describe('Jasmine', () => {
@@ -12,15 +12,16 @@ describe('Jasmine', () => {
describe('A screenplay scenario', () => {

beforeEach(() => Jasmine.attemptsTo(
Navigate.to('chrome://version/')
UseAngular.disableSynchronisation(),
Navigate.to('chrome://version/'),
));

it('passes', () => Jasmine.attemptsTo(
Navigate.to('chrome://accessibility/')
Navigate.to('chrome://accessibility/'),
));

afterEach(() => Jasmine.attemptsTo(
Navigate.to('chrome://chrome-urls/')
Navigate.to('chrome://chrome-urls/'),
));
});
});
@@ -0,0 +1,74 @@
import 'mocha';

import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools';
import { InteractionStarts, SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events';
import { ExecutionSuccessful, FeatureTag, Name } from '@serenity-js/core/lib/model';
import { protractor } from '../src/protractor';

describe('@serenity-js/jasmine', function () {

/*
* See:
* - https://github.com/angular/protractor/issues/3234
* - https://github.com/jan-molak/serenity-js/issues/56
*/

this.timeout(10000);

it('supports restarting the browser between test scenarios', () =>
protractor(
'./examples/protractor.conf.js',
'--specs=examples/multiple_passing_scenarios.spec.js',
'--restartBrowserBetweenTests',
)
.then(ifExitCodeIsOtherThan(0, logOutput))
.then(res => {

expect(res.exitCode).to.equal(0);

PickEvent.from(res.events)
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://version/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://accessibility/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://chrome-urls/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))
;
}));

it('produces the same result when the browser is not restarted between the tests', () =>
protractor(
'./examples/protractor.conf.js',
'--specs=examples/multiple_passing_scenarios.spec.js',
)
.then(ifExitCodeIsOtherThan(0, logOutput))
.then(res => {

expect(res.exitCode).to.equal(0);

PickEvent.from(res.events)
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://version/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://accessibility/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))

.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://chrome-urls/'`)))
.next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful()))
;
}));
});
@@ -23,6 +23,7 @@ describe('@serenity-js/jasmine', function () {
.next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A screenplay scenario passes')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Jasmine')))
.next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Jasmine')))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine disables synchronisation with Angular`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://version/'`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://accessibility/'`)))
.next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Jasmine navigates to 'chrome://chrome-urls/'`)))
@@ -1,5 +1,3 @@
import { match } from 'tiny-types';

import { AsyncOperationAttempted, AsyncOperationCompleted, AsyncOperationFailed, DomainEvent } from '../events';
import { CorrelationId, Description, Duration, Timestamp } from '../model';
import { StageCrewMember } from './StageCrewMember';
@@ -30,27 +28,9 @@ export class StageManager {
}

notifyOf(event: DomainEvent): void {
match<DomainEvent, void>(event)
.when(AsyncOperationAttempted, (evt: AsyncOperationAttempted) => {
this.wip.set(evt.correlationId, {
taskDescription: evt.taskDescription,
startedAt: evt.timestamp,
});
})
.when(AsyncOperationCompleted, (evt: AsyncOperationCompleted) => {
this.wip.delete(evt.correlationId);
})
.when(AsyncOperationFailed, (evt: AsyncOperationFailed) => {
const original = this.wip.get(evt.correlationId);
this.failedOperations.push({
taskDescription: original.taskDescription,
startedAt: original.startedAt,
duration: evt.timestamp.diff(original.startedAt),
error: evt.error,
});
this.wip.delete(evt.correlationId);
})
.else(_ => this.subscribers.forEach(crewMember => crewMember.notifyOf(event)));
this.handleAsyncOperation(event);

this.subscribers.forEach(crewMember => crewMember.notifyOf(event));
}

waitForNextCue(): Promise<void> {
@@ -100,4 +80,26 @@ export class StageManager {
currentTime(): Timestamp {
return this.clock.now();
}

private handleAsyncOperation(event: DomainEvent): void {
if (event instanceof AsyncOperationAttempted) {
this.wip.set(event.correlationId, {
taskDescription: event.taskDescription,
startedAt: event.timestamp,
});
}
else if (event instanceof AsyncOperationCompleted) {
this.wip.delete(event.correlationId);
}
else if (event instanceof AsyncOperationFailed) {
const original = this.wip.get(event.correlationId);
this.failedOperations.push({
taskDescription: original.taskDescription,
startedAt: original.startedAt,
duration: event.timestamp.diff(original.startedAt),
error: event.error,
});
this.wip.delete(event.correlationId);
}
}
}
@@ -15,9 +15,8 @@ describe('JasmineAdapter', () => {
Suite: () => void 0,
Spec: () => void 0,
getEnv() {
// todo: test invoking after each;
return {
afterEach: () => void 0,
beforeEach: () => void 0,
};
},
};
@@ -8,7 +8,7 @@ describe('@serenity-js/jasmine', () => {

beforeEach(() => {
const env = {
afterEach: sinon.spy(),
beforeEach: sinon.spy(),
};

jasmine = {
@@ -78,9 +78,9 @@ describe('@serenity-js/jasmine', () => {
* @test {bootstrap}
* @test {monkeyPatched}
*/
it('registers an afterEach hook to ensure Serenity/JS is synchronised with Jasmine', () => {
it('registers a beforeEach hook to ensure Serenity/JS is synchronised with Jasmine', () => {
serenityReporterForJasmine(jasmine);

expect(jasmine.getEnv().afterEach).to.have.been.calledOnce; // tslint:disable-line:no-unused-expression
expect(jasmine.getEnv().beforeEach).to.have.been.calledOnce; // tslint:disable-line:no-unused-expression
});
});
@@ -37,6 +37,17 @@ export class JasmineAdapter {
// tslint:disable-next-line:prefer-object-spread
runner.loadConfig(Object.assign(
{
/*
* Serenity/JS doesn't use Jasmine's assertions, so this mechanism can be disabled
*/
oneFailurePerSpec: true,

/*
* A spec should stop execution as soon as there's a hook or spec failure
* See https://github.com/angular/protractor/issues/3234
*/
stopSpecOnExpectationFailure: true,

/*
* Default to not executing tests at random.
* See https://github.com/angular/protractor/blob/4f74a4ec753c97adfe955fe468a39286a0a55837/lib/frameworks/jasmine.js#L76
@@ -48,6 +59,7 @@ export class JasmineAdapter {

runner.addReporter(reporter(jasmine));

// todo: wait for the wip queue to drain here again?
runner.onComplete((passed: boolean) => resolve());

runner.configureDefaultReporter(this.config);
@@ -25,7 +25,7 @@ export function bootstrap(jasmine = (global as any).jasmine) {
jasmine.Suite = monkeyPatched(jasmine.Suite);
jasmine.Spec = monkeyPatched(jasmine.Spec);

jasmine.getEnv().afterEach(() => serenity.waitForNextCue());
jasmine.getEnv().beforeEach(() => serenity.waitForNextCue());

return new SerenityReporterForJasmine(serenity);
}
@@ -36,12 +36,12 @@ export class ProtractorReporter implements StageCrewMember {
}],
});

this.runner.emit('testPass', {
name: e.value.name.value,
category: e.value.category.value,
this.afterEach().then(() => {
this.runner.emit('testPass', {
name: e.value.name.value,
category: e.value.category.value,
});
});

this.afterEach();
})
.when(ProblemIndication, (o: ProblemIndication) => {

@@ -57,12 +57,12 @@ export class ProtractorReporter implements StageCrewMember {
}],
});

this.runner.emit('testFail', {
name: e.value.name.value,
category: e.value.category.value,
this.afterEach().then(() => {
this.runner.emit('testFail', {
name: e.value.name.value,
category: e.value.category.value,
});
});

this.afterEach();
})
.else(() => /* ignore */ void 0),
)
@@ -73,24 +73,27 @@ export class ProtractorReporter implements StageCrewMember {
return this.reported;
}

private afterEach(): void {
if (this.runner.afterEach) {
private afterEach(): PromiseLike<void> {
if (! this.runner.afterEach) {
return Promise.resolve();
}

const id = CorrelationId.create();
const id = CorrelationId.create();

this.stage.announce(new AsyncOperationAttempted(
new Description(`[${ this.constructor.name }] Invoking ProtractorRunner.afterEach...`),
id,
));
this.stage.announce(new AsyncOperationAttempted(
new Description(`[${ this.constructor.name }] Invoking ProtractorRunner.afterEach...`),
id,
));

Promise.resolve(this.runner.afterEach() as PromiseLike<void>)
.then(
() => this.stage.announce(new AsyncOperationCompleted(
return Promise.resolve(this.runner.afterEach() as PromiseLike<void> | undefined)
.then(
() =>
this.stage.announce(new AsyncOperationCompleted(
new Description(`[${ this.constructor.name }] ProtractorRunner.afterEach succeeded`),
id,
)),
error => this.stage.announce(new AsyncOperationFailed(error, id)),
);
}
error =>
this.stage.announce(new AsyncOperationFailed(error, id)),
);
}
}

0 comments on commit 21b5a41

Please sign in to comment.