Permalink
Browse files

fix(core): JSONReporter superseded by SerenityBDDReporter

BREAKING CHANGE: JSONReporter is now superseded by a cleaner and more focused implementation -

SerenityBDDReporter, which better handles gathering results from tests executed in parallel.
  • Loading branch information...
jan-molak committed Jan 31, 2017
1 parent ed6e518 commit 0b93ff0d117cd0195b50e3dd45202808fb7165f3
@@ -13,7 +13,7 @@ exports.config = {
serenity: {
dialect: 'cucumber',
crew: [
crew.jsonReporter(),
crew.serenityBDDReporter(),
crew.consoleReporter(),
crew.Photographer.who(_ => _
.takesPhotosOf(_.Tasks_and_Interactions)
@@ -37,7 +37,11 @@ exports.config = {
'disable-extensions',
// 'show-fps-counter=true'
]
}
},
// execute tests using 2 browsers running in parallel
shardTestFiles: true,
maxInstances: 2
},
restartBrowserBetweenTests: true,
@@ -13,7 +13,7 @@ exports.config = {
serenity: {
dialect: 'mocha',
crew: [
crew.jsonReporter(),
crew.serenityBDDReporter(),
crew.consoleReporter(),
crew.Photographer.who(_ => _
.takesPhotosOf(_.Tasks_and_Interactions)
@@ -1,7 +1,6 @@
import * as fs from 'fs';
import * as mockfs from 'mock-fs';
import { Md5 } from 'ts-md5/dist/md5';
import {
Activity,
ActivityFinished,
@@ -18,14 +17,15 @@ import {
Tag,
} from '../../../../src/serenity/domain';
import { FileSystem } from '../../../../src/serenity/io/file_system';
import { Journal, JsonReporter, Stage, StageManager } from '../../../../src/serenity/stage';
import { jsonReporter } from '../../../../src/stage_crew';
import { Journal, Stage, StageManager } from '../../../../src/serenity/stage';
import { SerenityBDDReporter, serenityBDDReporter } from '../../../../src/serenity/reporting/serenity_bdd_reporter';
import expect = require('../../../expect');
describe('When reporting on what happened during the rehearsal', () => {
describe ('JSON Reporter', () => {
describe ('SerenityBDDReporter', () => {
const
startTime = 1467201010000,
@@ -38,26 +38,26 @@ describe('When reporting on what happened during the rehearsal', () => {
let stageManager: StageManager,
stage: Stage,
fileSystem: FileSystem,
reporter: JsonReporter;
reporter: SerenityBDDReporter;
beforeEach( () => {
fileSystem = new FileSystem(rootDir);
stageManager = new StageManager(new Journal());
stage = new Stage(stageManager);
reporter = new JsonReporter(fileSystem);
reporter = new SerenityBDDReporter(fileSystem);
reporter.assignTo(stage);
});
beforeEach(() => mockfs({ '/Users/jan/projects/serenityjs': {} }));
afterEach (() => mockfs.restore());
it ('can be instantiated using a default path to the reports directory', () => {
expect(jsonReporter()).to.be.instanceOf(JsonReporter);
expect(serenityBDDReporter()).to.be.instanceOf(SerenityBDDReporter);
});
it ('can be instantiated using a factory method so that explicit instantiation of the File System can be avoided', () => {
expect(jsonReporter('/some/path/to/reports')).to.be.instanceOf(JsonReporter);
expect(serenityBDDReporter('/some/path/to/reports')).to.be.instanceOf(SerenityBDDReporter);
});
describe ('the Rehearsal Report', () => {
@@ -79,8 +79,8 @@ describe('When reporting on what happened during the rehearsal', () => {
it('includes the details of what happened during specific activities', () => {
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Opens a browser', startTime + 1),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 2),
activityStarted('Opens a browser', startTime + 1),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 2),
sceneFinished(scene, Result.SUCCESS, startTime + 3),
);
@@ -100,10 +100,10 @@ describe('When reporting on what happened during the rehearsal', () => {
it('covers multiple activities', () => {
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Opens a browser', startTime + 1),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 2),
activityStarted('Navigates to amazon.com', startTime + 3),
activityFinished('Navigates to amazon.com', Result.SUCCESS, startTime + 4),
activityStarted('Opens a browser', startTime + 1),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 2),
activityStarted('Navigates to amazon.com', startTime + 3),
activityFinished('Navigates to amazon.com', Result.SUCCESS, startTime + 4),
sceneFinished(scene, Result.SUCCESS, startTime + 5),
);
@@ -129,14 +129,14 @@ describe('When reporting on what happened during the rehearsal', () => {
it('covers activities in detail, including sub-activities', () => {
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Buys a discounted e-book reader', startTime + 1),
activityStarted('Opens a browser', startTime + 2),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 3),
activityStarted('Searches for discounted e-book readers', startTime + 4),
activityStarted('Navigates to amazon.com', startTime + 5),
activityFinished('Navigates to amazon.com', Result.SUCCESS, startTime + 6),
activityFinished('Searches for discounted e-book readers', Result.SUCCESS, startTime + 7),
activityFinished('Buys a discounted e-book reader', Result.SUCCESS, startTime + 8),
activityStarted('Buys a discounted e-book reader', startTime + 1),
activityStarted('Opens a browser', startTime + 2),
activityFinished('Opens a browser', Result.SUCCESS, startTime + 3),
activityStarted('Searches for discounted e-book readers', startTime + 4),
activityStarted('Navigates to amazon.com', startTime + 5),
activityFinished('Navigates to amazon.com', Result.SUCCESS, startTime + 6),
activityFinished('Searches for discounted e-book readers', Result.SUCCESS, startTime + 7),
activityFinished('Buys a discounted e-book reader', Result.SUCCESS, startTime + 8),
sceneFinished(scene, Result.SUCCESS, startTime + 9),
);
@@ -176,10 +176,10 @@ describe('When reporting on what happened during the rehearsal', () => {
it('contains pictures', () => {
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Specifies the default email address', startTime + 1),
photoTaken('Specifies the default email address', 'picture1.png', startTime + 1),
activityFinished('Specifies the default email address', Result.SUCCESS, startTime + 2),
photoTaken('Specifies the default email address', 'picture2.png', startTime + 2),
activityStarted('Specifies the default email address', startTime + 1),
photoTaken('Specifies the default email address', 'picture1.png', startTime + 1),
activityFinished('Specifies the default email address', Result.SUCCESS, startTime + 2),
photoTaken('Specifies the default email address', 'picture2.png', startTime + 2),
sceneFinished(scene, Result.SUCCESS, startTime + 3),
);
@@ -204,9 +204,9 @@ describe('When reporting on what happened during the rehearsal', () => {
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Buys a discounted e-book reader', startTime + 1),
activityFinished('Buys a discounted e-book reader', Result.SUCCESS, startTime + 2),
photoFailed('Buys a discounted e-book reader', startTime + 2),
activityStarted('Buys a discounted e-book reader', startTime + 1),
activityFinished('Buys a discounted e-book reader', Result.SUCCESS, startTime + 2),
photoFailed('Buys a discounted e-book reader', startTime + 2),
sceneFinished(scene, Result.SUCCESS, startTime + 3),
);
@@ -409,6 +409,39 @@ describe('When reporting on what happened during the rehearsal', () => {
})));
});
it('describes test infrastructure problems encountered during the test', () => {
let error = new Error('Timeout of 1000ms exceeded.');
error.stack = ''; // we don't care about the stack in this test
givenFollowingEvents(
sceneStarted(scene, startTime),
activityStarted('Buys a discounted e-book reader', startTime + 1),
sceneFinished(scene, Result.ERROR, startTime + 1001, error),
);
return stageManager.allDone().then(_ =>
expect(producedReport()).to.deep.equal(expectedReportWith({
duration: 1001,
testSteps: [ {
description: 'Buys a discounted e-book reader',
startTime: startTime + 1,
duration: 1000,
result: 'ERROR',
children: [],
exception: {
errorType: 'Error',
message: 'Timeout of 1000ms exceeded.',
stackTrace: [ ],
},
} ],
result: 'ERROR',
testFailureCause: {
errorType: 'Error',
message: 'Timeout of 1000ms exceeded.',
stackTrace: [],
},
})));
});
});
describe('When scenarios are tagged', () => {
@@ -595,7 +628,7 @@ describe('When reporting on what happened during the rehearsal', () => {
name: 'Checkout',
type: 'feature',
}],
// driver: 'chrome:jane',
driver: 'unknown',
manual: false,
startTime,
duration: undefined,
@@ -1,6 +1,6 @@
import { Runner } from 'protractor';
import { serenity, Serenity } from '../..';
import { jsonReporter } from '../../serenity/stage/json_reporter';
import { serenityBDDReporter } from '../../serenity/reporting';
import { ProtractorReport, ProtractorReporter } from '../reporting';
import { SerenityFrameworkConfig } from './serenity_framework_config';
import { TestFramework } from './test_framework';
@@ -48,7 +48,7 @@ export class SerenityProtractorFramework {
private defaultConfig = (): SerenityFrameworkConfig => ({
serenity: {
crew: [ jsonReporter() ],
crew: [ serenityBDDReporter() ],
},
})
}
@@ -1,2 +1,4 @@
export * from './rehearsal_report';
export * from './report_exporter';
export * from './serenity_bdd_report';
export * from './serenity_bdd_reporter';
@@ -4,6 +4,8 @@ import {
ActivityStarts,
DomainEvent,
Outcome,
Photo,
PhotoAttempted,
Scene,
SceneFinished,
SceneStarts,
@@ -12,7 +14,8 @@ import { ReportExporter } from './report_exporter';
export class RehearsalReport {
static from(events: Array<DomainEvent<any>>): RehearsalPeriod {
let currentNode = undefined;
let previousNode: ReportPeriod<Scene | Activity> = undefined;
let currentNode: ReportPeriod<Scene | Activity> = undefined;
return events.reduce((fullReport, event) => {
switch (event.constructor) { // tslint:disable-line:switch-default - ignore other events
@@ -26,7 +29,15 @@ export class RehearsalReport {
currentNode = currentNode.concludeWith(event);
break;
case ActivityFinished:
currentNode = currentNode.concludeWith(event);
// The Photographer triggers a PhotoAttempted event after the ActivityFinished,
// that's why we need to cache both the previous and the current node
previousNode = currentNode;
currentNode = currentNode.concludeWith(event);
break;
case PhotoAttempted:
[previousNode, currentNode]
.filter(node => !! node && node.matches(event.value.activity))
.forEach(node => node.attach(event.value.photo));
break;
}
@@ -36,30 +47,47 @@ export class RehearsalReport {
}
export abstract class ReportPeriod<T> {
protected startedAt: number;
protected finishedAt: number;
public parent: ReportPeriod<any>;
public value: T;
public children: Array<ReportPeriod<any>> = [];
public outcome: Outcome<T>;
public startedAt: number;
protected finishedAt: number;
private promisedPhotos: Array<PromiseLike<Photo>> = [];
constructor(start: DomainEvent<T>) {
this.value = start.value;
this.startedAt = start.timestamp;
}
abstract matches(finished: DomainEvent<Outcome<any>>): boolean;
abstract matches(finished: T): boolean;
concludeWith(finished: DomainEvent<Outcome<any>>) {
this.finishedAt = finished.timestamp;
this.outcome = finished.value;
return this.parent;
if (! this.parent) {
return this;
}
return this.matches(finished.value.subject)
? this.parent
: this.parent.concludeWith(finished);
}
attach(promisedPhoto: PromiseLike<Photo>) {
this.promisedPhotos.push(promisedPhoto);
}
photos(): PromiseLike<Photo[]> {
// todo: maybe use some constant rather than a blunt "undefined"?
return Promise.all(this.promisedPhotos).then(photos => photos.filter(p => p !== undefined));
}
duration() {
// todo: error handling on incomplete object;
return this.finishedAt - this.startedAt;
}
@@ -74,7 +102,7 @@ export abstract class ReportPeriod<T> {
}
export class RehearsalPeriod extends ReportPeriod<Rehearsal> {
matches(finished: DomainEvent<Outcome<any>>): boolean {
matches(finished: Rehearsal): boolean {
return false;
}
@@ -84,16 +112,16 @@ export class RehearsalPeriod extends ReportPeriod<Rehearsal> {
constructor() {
super(new DomainEvent(new Rehearsal())); // todo: DomainEvent? Meh. Can I do better?
this.parent = this;
this.parent = undefined;
}
}
export class Rehearsal {
}
export class ActivityPeriod extends ReportPeriod<Activity> {
matches(finished: DomainEvent<Outcome<any>>): boolean {
return this.value.equals(finished.value.subject);
matches(another: Activity): boolean {
return this.value.equals(another);
}
exportedUsing<FORMAT>(exporter: ReportExporter<FORMAT>): PromiseLike<FORMAT> {
@@ -102,8 +130,8 @@ export class ActivityPeriod extends ReportPeriod<Activity> {
}
export class ScenePeriod extends ReportPeriod<Scene> {
matches(finished: DomainEvent<Outcome<any>>): boolean {
return this.value.equals(finished.value.subject);
matches(another: Scene): boolean {
return this.value.equals(another);
}
exportedUsing<FORMAT>(exporter: ReportExporter<FORMAT>): PromiseLike<FORMAT> {
Oops, something went wrong.

0 comments on commit 0b93ff0

Please sign in to comment.