Skip to content
Permalink
Browse files
fix(reporting): @Manual tags are correctly represented in the report
affects: @serenity-js/core

But please don't rely on manual tests too much. After all, Serenity/JS is a test automation
framework... ;-)

ISSUES CLOSED: #67
  • Loading branch information
jan-molak committed Sep 30, 2017
1 parent 5a81502 commit babc5878ebc6e9be4686df1419c52c49298b32f7
Showing 2 changed files with 121 additions and 46 deletions.
@@ -27,6 +27,10 @@ describe('When reporting on what happened during the rehearsal', () => {

describe ('SerenityBDDReporter', () => {

// todo: extract hash generation so that it doesn't have to be hard-coded
// todo: correct the manual tag handling
// todo: correct the feature and capability tags handling

const
startTime = 1467201010000,
duration = 42,
@@ -513,8 +517,10 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('324f8a667d6ae1b2c214f90d15368831.json')).to.deep.equal(expectedReportWith({
expect(producedReport('d1a32bdd50a70230530d092e3236b22b.json')).to.deep.equal(expectedReportWith({
duration: 1,
id: 'checkout;paying-with-a-default-card;regression',
name: 'checkout;paying-with-a-default-card;regression',
result: 'SUCCESS',
tags: [{
name: 'regression',
@@ -526,6 +532,36 @@ describe('When reporting on what happened during the rehearsal', () => {
})));
});

it('describes the scenario as "manual" when it is tagged as such', () => {
const taggedScene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' }, [
new Tag('manual'),
]);

givenFollowingEvents(
sceneStarted(taggedScene, startTime),
sceneFinished(taggedScene, Result.SUCCESS, startTime + 1),
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('e78f6c3a32e9fe31c5b9b68ec4002996.json')).to.deep.equal(expectedReportWith({
duration: 1,
id: 'checkout;paying-with-a-default-card;manual',
name: 'checkout;paying-with-a-default-card;manual',
result: 'SUCCESS',
manual: true,
tags: [{
name: 'manual',
type: 'tag',
}, {
name: 'Manual',
type: 'External Tests',
}, {
name: 'Checkout',
type: 'feature',
}],
})));
});

it('describes the complex tags encountered', () => {
const taggedScene = new RecordedScene('Paying with a default card', 'Checkout', { path: 'features/checkout.feature' }, [
new Tag('priority', [ 'must-have' ]),
@@ -537,9 +573,11 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('71ee0997d0a6ffc820a9e12ef991f7ba.json')).to.deep.equal(expectedReportWith({
expect(producedReport('5797820850d2f54c02f4af4329e72da3.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
id: 'checkout;paying-with-a-default-card;priority:must-have',
name: 'checkout;paying-with-a-default-card;priority:must-have',
tags: [{
name: 'must-have',
type: 'priority',
@@ -613,9 +651,11 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('d16a4fd5a0b46ee409c67f40784d0ae9.json')).to.deep.equal(expectedReportWith({
expect(producedReport('347aeb7615908c793d4e658896b3bdcc.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
id: 'checkout;paying-with-a-default-card;issues:MY-PROJECT-123,MY-PROJECT-456;issues:MY-PROJECT-789',
name: 'checkout;paying-with-a-default-card;issues:MY-PROJECT-123,MY-PROJECT-456;issues:MY-PROJECT-789',
tags: [{
name: 'MY-PROJECT-123',
type: 'issue',
@@ -649,9 +689,11 @@ describe('When reporting on what happened during the rehearsal', () => {
);

return stageManager.waitForNextCue().then(_ =>
expect(producedReport('2cc43a438de2e6543553ccfe836e60b6.json')).to.deep.equal(expectedReportWith({
expect(producedReport('7895a8ceecdf653a2a42302cfb40e4e4.json')).to.deep.equal(expectedReportWith({
duration: 1,
result: 'SUCCESS',
id: 'checkout;paying-with-a-default-card;issues:MY-PROJECT-123,MY-PROJECT-456;issue:MY-PROJECT-123',
name: 'checkout;paying-with-a-default-card;issues:MY-PROJECT-123,MY-PROJECT-456;issue:MY-PROJECT-123',
tags: [{
name: 'MY-PROJECT-123',
type: 'issue',
@@ -63,6 +63,42 @@ function reportFileNameFor(scene: SceneReport): string {
return Md5.hashStr(`${id}-${tags}`) + '.json';
}

class Tags {
private readonly tags: Tag[];

constructor(...tags: Tag[]) {
this.tags = tags;
}

ofType(type: string) {
return this.tags.filter(tag => tag.type === type);
}

all = () => this.tags;

process = (...fns: Array<(tags: Tag[]) => Tag[]>) => fns.reduce((tags, fn) => fn(tags), this.tags);
}

const splitIssueTags = (tags: Tag[]) => {
const isAnIssue = (tag: Tag): boolean => !! ~['issue', 'issues'].indexOf(tag.type);
const breakDownIssues = (tag: Tag) => isAnIssue(tag)
? tag.values.map(issueId => new Tag('issue', [ issueId ]))
: tag;

return _.chain(tags)
.map(breakDownIssues)
.flatten()
.value() as Tag[];
};

const addCapabilityAndFeatureTags = (scene: RecordedScene) => (tags: Tag[]) => {
return tags.concat(
new Tag('feature', [scene.category]),
);
};

const tagAsManualIfNeeded = (tags: Tag[]) => tags.concat(...tags.filter(tag => tag.type === 'manual').map(t => new Tag('External Tests', ['Manual'])));

/**
* Transforms the tree structure of the RehearsalPeriod to a format acceptable by Protractor
*/
@@ -81,30 +117,43 @@ 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 => {
return node.promisedTags().then(tags => ({
id: this.idOf(node, tags),
title: node.value.name,
name: this.idOf(node, tags),
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,
}));
return node.promisedTags().then(promisedTags => {
const scene = node.value;
const tags = new Tags(...scene.tags, ...promisedTags);

return ({
id: this.idOf(node, tags.all()),
name: this.idOf(node, tags.all()),

context: tags.ofType('context').map(tag => tag.value).pop(),
manual: !! tags.ofType('manual').pop(),
tags: this.serialisedTags(tags.process(
splitIssueTags,
tagAsManualIfNeeded,
addCapabilityAndFeatureTags(scene),
)),

title: scene.name,
description: '',
startTime: node.startedAt,
duration: node.duration(),
testSource: 'cucumber', // todo: provide the correct test source

userStory: {
id: this.dashified(scene.category),
path: path.relative(process.cwd(), scene.location.path),
storyName: scene.category,
type: 'feature',
},

issues: this.issuesCoveredBy(scene),
testSteps: children,

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

@@ -122,7 +171,8 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {
}

private idOf(node: ScenePeriod, tags: Tag[]) {
const combined = (ts: Tag[]) => (tags || []).map(tag => `${ tag.type }:${tag.value}`).join(';');
const asString = (t: Tag) => !! t.value ? `${ t.type }:${t.value}` : t.type ;
const combined = (ts: Tag[]) => (tags || []).map(asString).join(';').replace(' ', '');

return [
this.dashified(node.value.category),
@@ -144,17 +194,8 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {
return _.chain(scene.tags).filter(onlyIssueTags).map(toIssueIds).flatten().uniq().value() as string[];
}

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

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

const isAnIssue = this.isAnIssue;

function serialise(tag: Tag) {
const noValue = (t: Tag) => ({ name: t.type, type: 'tag' }),
withValue = (t: Tag) => ({ name: t.values.join(','), type: t.type });
@@ -164,15 +205,7 @@ export class SerenityBDDReportExporter implements ReportExporter<JSONObject> {
: withValue(tag);
}

function breakDownIssues(tag: Tag) {
return isAnIssue(tag)
? tag.values.map(issueId => new Tag('issue', [ issueId ]))
: tag;
}

return _.chain(tags)
.map(breakDownIssues)
.flatten()
.map(serialise)
.uniqBy('name')
.value();

0 comments on commit babc587

Please sign in to comment.