Skip to content
Permalink
Browse files
feat(core): sceneTagged event allows for the scene to be tagged with …
…an arbitrary tag

This feature will enable addressing #61

affects: @serenity-js/core
  • Loading branch information
jan-molak committed Jul 15, 2017
1 parent 3467b35 commit 75208e1b9414d3d44a216c321a36b3e4d9728469
Showing 5 changed files with 127 additions and 52 deletions.
@@ -1,6 +1,5 @@
import * as fs from 'fs';
import * as mockfs from 'mock-fs';
import { Md5 } from 'ts-md5/dist/md5';
import {
ActivityFinished,
ActivityStarts,
@@ -14,6 +13,7 @@ import {
Result,
SceneFinished,
SceneStarts,
SceneTagged,
Tag,
} from '../../src/domain';
import { FileSystem } from '../../src/io/file_system';
@@ -31,16 +31,14 @@ describe('When reporting on what happened during the rehearsal', () => {
startTime = 1467201010000,
duration = 42,
scene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' }),
sceneId = Md5.hashStr(scene.id),
filename = `${ sceneId }.json`,
rootDir = '/some/path/to/reports';

let stageManager: StageManager,
stage: Stage,
fileSystem: FileSystem,
reporter: SerenityBDDReporter;

beforeEach( () => {
beforeEach(() => {
fileSystem = new FileSystem(rootDir);
stageManager = new StageManager(new Journal());
stage = new Stage(stageManager);
@@ -515,7 +513,7 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport()).to.deep.equal(expectedReportWith({
expect(producedReport('324f8a667d6ae1b2c214f90d15368831.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
tags: [{
@@ -539,7 +537,7 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport()).to.deep.equal(expectedReportWith({
expect(producedReport('71ee0997d0a6ffc820a9e12ef991f7ba.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
tags: [{
@@ -552,6 +550,53 @@ describe('When reporting on what happened during the rehearsal', () => {
})));
});

it('adds in tags generated asynchronously', () => {
const aScene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' });

givenFollowingEvents(
sceneStarted(aScene, startTime),
sceneTagged(new Tag('browser', ['chrome']), startTime + 3),
sceneFinished(aScene, Result.SUCCESS, startTime + 2),
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('4ec27293d39642f72c52b21e57675a49.json')).to.deep.equal(expectedReportWith({
duration: 2,
result: 'SUCCESS',
tags: [{
name: 'chrome',
type: 'browser',
}, {
name: 'Checkout',
type: 'feature',
}],
})));
});

it('specifies what "context icon" to use when the context tag is present', () => {
const aScene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' });

givenFollowingEvents(
sceneStarted(aScene, startTime),
sceneTagged(new Tag('context', ['chrome']), startTime + 3),
sceneFinished(aScene, Result.SUCCESS, startTime + 2),
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('898a20ecc17b8d1dc3bf94b26147db3a.json')).to.deep.equal(expectedReportWith({
duration: 2,
result: 'SUCCESS',
tags: [{
name: 'chrome',
type: 'context',
}, {
name: 'Checkout',
type: 'feature',
}],
context: 'chrome',
})));
});

it('extracts the value of any @issues tags encountered and breaks them down to one tag per issue', () => {
const taggedScene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' }, [
new Tag('issues', [ 'MY-PROJECT-123', 'MY-PROJECT-456' ]),
@@ -564,7 +609,7 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport()).to.deep.equal(expectedReportWith({
expect(producedReport('d16a4fd5a0b46ee409c67f40784d0ae9.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
tags: [{
@@ -600,7 +645,7 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport()).to.deep.equal(expectedReportWith({
expect(producedReport('2cc43a438de2e6543553ccfe836e60b6.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
tags: [{
@@ -629,6 +674,10 @@ describe('When reporting on what happened during the rehearsal', () => {
return new SceneStarts(s, timestamp);
}

function sceneTagged(tag: Tag, timestamp: number) {
return new SceneTagged(Promise.resolve(tag), timestamp);
}

function activityStarted(name: string, timestamp: number) {
return new ActivityStarts(new RecordedActivity(name), timestamp);
}
@@ -651,6 +700,7 @@ describe('When reporting on what happened during the rehearsal', () => {

function expectedReportWith(overrides: any) {
const report = {
id: 'checkout;paying-with-a-default-card',
name: 'Paying with a default card',
testSteps: [],
issues: [],
@@ -667,7 +717,6 @@ describe('When reporting on what happened during the rehearsal', () => {
name: 'Checkout',
type: 'feature',
}],
driver: 'unknown',
manual: false,
startTime,
duration: undefined,
@@ -679,7 +728,7 @@ describe('When reporting on what happened during the rehearsal', () => {
return Object.assign(report, overrides);
}

function producedReport() {
function producedReport(filename: string = '24e4a1bb29546fcd3240136392110b20.json') {
return JSON.parse(fs.readFileSync(`${rootDir}/${filename}`).toString('ascii'));
}
});
@@ -1,5 +1,5 @@
import * as moment from 'moment';
import { Outcome, PhotoReceipt, RecordedActivity, RecordedScene } from './model';
import { Outcome, PhotoReceipt, RecordedActivity, RecordedScene, Tag } from './model';

export class DomainEvent<T> {
private type: string;
@@ -17,5 +17,6 @@ export class DomainEvent<T> {
export class SceneStarts extends DomainEvent<RecordedScene> {}
export class ActivityStarts extends DomainEvent<RecordedActivity> {}
export class ActivityFinished extends DomainEvent<Outcome<RecordedActivity>> {}
export class SceneTagged extends DomainEvent<PromiseLike<Tag>> {}
export class SceneFinished extends DomainEvent<Outcome<RecordedScene>> {}
export class PhotoAttempted extends DomainEvent<PhotoReceipt> {}
@@ -9,18 +9,22 @@ import {
RecordedScene,
SceneFinished,
SceneStarts,
SceneTagged,
Tag,
} from '../domain';
import { ReportExporter } from './report_exporter';

export class RehearsalReport {
static from(events: Array<DomainEvent<any>>): RehearsalPeriod {
let previousNode: ReportPeriod<RecordedScene | RecordedActivity>;
let currentNode: ReportPeriod<RecordedScene | RecordedActivity>;
let currentScene: ScenePeriod;

return events.reduce((fullReport, event) => {
switch (event.constructor) { // tslint:disable-line:switch-default - ignore other events
case SceneStarts:
currentNode = fullReport.append(new ScenePeriod(event));
currentScene = fullReport.append(new ScenePeriod(event)) as ScenePeriod;
currentNode = currentScene;
break;
case ActivityStarts:
currentNode = currentNode.append(new ActivityPeriod(event));
@@ -34,6 +38,9 @@ export class RehearsalReport {
previousNode = currentNode;
currentNode = currentNode.concludeWith(event);
break;
case SceneTagged:
currentScene.tagWithPromised(event.value);
break;
case PhotoAttempted:
[previousNode, currentNode]
.filter(node => !! node && node.matches(event.value.activity))
@@ -130,11 +137,21 @@ export class ActivityPeriod extends ReportPeriod<RecordedActivity> {
}

export class ScenePeriod extends ReportPeriod<RecordedScene> {
private tags: Array<PromiseLike<Tag>> = [];

matches(another: RecordedScene): boolean {
return this.value.equals(another);
}

exportedUsing<FORMAT>(exporter: ReportExporter<FORMAT>): PromiseLike<FORMAT> {
return exporter.exportScene(this);
}

tagWithPromised(tag: PromiseLike<Tag>) {
this.tags.push(tag);
}

promisedTags() {
return Promise.all(this.tags);
}
}
@@ -11,15 +11,16 @@ export interface FullReport extends JSONObject {
}

export interface SceneReport extends JSONObject {
id: string;
title: string;
name: string;
description: string;
context: string;
testSource?: string;
// testCaseName: string;
startTime: number;
duration: number;
sessionId?: string;
driver: string;
manual: boolean;
result: string;
userStory: UserStoryReport;
@@ -38,26 +38,31 @@ export class SerenityBDDReporter implements StageCrewMember {
}

notifyOf(event: DomainEvent<any>): void {
switch (event.constructor.name) { // tslint:disable-line:switch-default - ignore other events
case SceneFinished.name: return this.persistReportFor(event);
switch (event.constructor) { // tslint:disable-line:switch-default - ignore other events
case SceneFinished: return this.persistReport();
}
}

private persistReportFor({ value }: SceneFinished) {
const filename = `${ Md5.hashStr(value.subject.id) }.json`;

private persistReport() {
this.stage.manager.informOfWorkInProgress(
RehearsalReport.from(this.stage.manager.readNewJournalEntriesAs('SerenityBDDReporter'))
.exportedUsing(new SerenityBDDReportExporter())
.then((fullReport: FullReport) => Promise.all(
fullReport.scenes.map(
(scene: SceneReport) => this.fs.store(filename, JSON.stringify(scene)),
(scene: SceneReport) => this.fs.store(reportFileNameFor(scene), JSON.stringify(scene)),
),
)),
);
}
}

function reportFileNameFor(scene: SceneReport): string {
const id = scene.id,
tags = scene.tags.map(t => `${t.type}:${t.name}`).join('-');

return Md5.hashStr(`${id}-${tags}`) + '.json';
}

/**
* Transforms the tree structure of the RehearsalPeriod to a format acceptable by Protractor
*/
@@ -75,29 +80,32 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {

exportScene(node: ScenePeriod): PromiseLike<SceneReport> {
return Promise.all(node.children.map(child => child.exportedUsing(this)))
.then((children: ActivityReport[]) => this.errorExporter.tryToExport(node.outcome.error).then(error => ({
title: node.value.name,
name: node.value.name,
description: '',
startTime: node.startedAt,
duration: node.duration(),
driver: 'unknown', // todo: provide the correct driver information for web tests
testSource: 'cucumber', // todo: provide the correct test source
manual: false,
result: Result[node.outcome.result],
userStory: {
id: this.dashified(node.value.category),
path: path.relative(process.cwd(), node.value.location.path),
storyName: node.value.category,
type: 'feature',
},
tags: this.tagsFor(node.value),
issues: this.issuesCoveredBy(node.value),
testSteps: children,

annotatedResult: Result[node.outcome.result],
testFailureCause: error,
})));
.then((children: ActivityReport[]) => this.errorExporter.tryToExport(node.outcome.error).then(error => {
return node.promisedTags().then(tags => ({
id: `${this.dashified(node.value.category)};${this.dashified(node.value.name)}`,
title: node.value.name,
name: node.value.name,
context: tags.filter(tag => tag.type === 'context').map(tag => tag.value).pop(),
description: '',
startTime: node.startedAt,
duration: node.duration(),
testSource: 'cucumber', // todo: provide the correct test source
manual: false,
result: Result[ node.outcome.result ],
userStory: {
id: this.dashified(node.value.category),
path: path.relative(process.cwd(), node.value.location.path),
storyName: node.value.category,
type: 'feature',
},
tags: this.serialisedTags(tags.concat(node.value.tags).concat(this.featureTags(node.value))),
issues: this.issuesCoveredBy(node.value),
testSteps: children,

annotatedResult: Result[ node.outcome.result ],
testFailureCause: error,
}));
}));
}

exportActivity(node: ActivityPeriod): PromiseLike<ActivityReport> {
@@ -126,7 +134,14 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {
return _.chain(scene.tags).filter(onlyIssueTags).map(toIssueIds).flatten().uniq().value() as string[];
}

private tagsFor(scene: RecordedScene): TagReport[] {
// todo: add the capability tag?
private featureTags(scene: RecordedScene) {
return [
new Tag('feature', [scene.category]),
];
}

private serialisedTags(tags: Tag[]): TagReport[] {

const isAnIssue = this.isAnIssue;

@@ -145,19 +160,11 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {
: tag;
}

function featureTag(featureName: string) {
return {
name: featureName,
type: 'feature',
};
}

return _.chain(scene.tags)
return _.chain(tags)
.map(breakDownIssues)
.flatten()
.map(serialise)
.uniqBy('name')
.concat(featureTag(scene.category))
.value();
}

0 comments on commit 75208e1

Please sign in to comment.