Skip to content

Commit

Permalink
feat(mocha): support for custom tags
Browse files Browse the repository at this point in the history
custom tags, such as @issues:JIRA-1,JIRA-2 or @regression-pack, included in test names are now
recognised in Serenity BDD reports
  • Loading branch information
jan-molak committed Mar 3, 2024
1 parent 9f573ab commit b86f2bb
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, it } from 'mocha';

import { ClearLocalStorage, RecordedItems, RecordItem, RemoveItem, RenameItem, Start, ToggleItem } from '../src';

describe('Managing a Todo List', () => {
describe('Managing a Todo List @smoke', () => {

afterEach(() =>
actorInTheSpotlight().attemptsTo(
Expand All @@ -13,7 +13,7 @@ describe('Managing a Todo List', () => {

describe('TodoMVC', () => {

describe('actor', () => {
describe('actor @ux', () => {

it('records new items', () =>
actorCalled('Wendy').attemptsTo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
describe('My feature @feature', () => {

describe('A scenario @scenario @issues:JIRA-1', () => {

it('passes @positive @issues:JIRA-2,JIRA-3', () => {
// no-op, passing
});

it.skip('manual test @manual');
});
});
81 changes: 81 additions & 0 deletions integration/mocha/spec/custom_tag_reporting.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools';
import { Timestamp } from '@serenity-js/core';
import {
SceneFinished,
SceneFinishes,
SceneStarts,
SceneTagged,
TestRunFinished,
TestRunFinishes,
TestRunnerDetected,
TestRunStarts
} from '@serenity-js/core/lib/events';
import {
ArbitraryTag,
CapabilityTag,
CorrelationId,
ExecutionSuccessful,
FeatureTag,
ImplementationPending,
IssueTag,
ManualTag,
Name
} from '@serenity-js/core/lib/model';
import { describe, it } from 'mocha';

import { mocha } from '../src/mocha';

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

it('emits custom tags if present on describe or test title so that Serenity BDD can aggregate them correctly', () => mocha( 'examples/custom_tags/custom_tags_in_title.spec.js')
.then(ifExitCodeIsOtherThan(0, logOutput))
.then(result => {
expect(result.exitCode).to.equal(0);

let currentSceneId: CorrelationId;

PickEvent.from(result.events)
.next(TestRunStarts, event => expect(event.timestamp).to.be.instanceof(Timestamp))
.next(SceneStarts, event => {
expect(event.details.name).to.equal(new Name('A scenario passes'));
currentSceneId = event.sceneId;
})
.next(SceneTagged, event => expect(event.tag).to.equal(new CapabilityTag('Custom tags')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('My feature')))
.next(TestRunnerDetected, event => expect(event.name).to.equal(new Name('Mocha')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ArbitraryTag('feature')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ArbitraryTag('scenario')))
.next(SceneTagged, event => expect(event.tag).to.equal(new IssueTag('JIRA-1')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ArbitraryTag('positive')))
.next(SceneTagged, event => expect(event.tag).to.equal(new IssueTag('JIRA-2')))
.next(SceneTagged, event => expect(event.tag).to.equal(new IssueTag('JIRA-3')))

// triggered by requiring actorCalled
.next(SceneFinishes, event => {
expect(event.sceneId).to.equal(currentSceneId);
})
.next(SceneFinished, event => {
expect(event.sceneId).to.equal(currentSceneId);
expect(event.outcome).to.equal(new ExecutionSuccessful());
})
.next(SceneStarts, event => {
expect(event.details.name).to.equal(new Name('A scenario manual test'));
currentSceneId = event.sceneId;
})
.next(SceneTagged, event => expect(event.tag).to.equal(new CapabilityTag('Custom tags')))
.next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('My feature')))
.next(TestRunnerDetected, event => expect(event.name).to.equal(new Name('Mocha')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ArbitraryTag('feature')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ArbitraryTag('scenario')))
.next(SceneTagged, event => expect(event.tag).to.equal(new IssueTag('JIRA-1')))
.next(SceneTagged, event => expect(event.tag).to.equal(new ManualTag('Manual')))

.next(SceneFinished, event => {
expect(event.sceneId).to.equal(currentSceneId);
expect(event.outcome).to.be.instanceof(ImplementationPending);
})
.next(TestRunFinishes, event => expect(event.timestamp).to.be.instanceof(Timestamp))
.next(TestRunFinished, event => expect(event.timestamp).to.be.instanceof(Timestamp))
;
}));
});
8 changes: 4 additions & 4 deletions packages/mocha/spec/exampleTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Test } from 'mocha';
export const exampleTest = {
'state': 'passed',
'type': 'test',
'title': 'passes',
'title': 'passes @smoke-test',
'body': '() => {\n\n }',
'async': false,
'sync': true,
Expand All @@ -18,7 +18,7 @@ export const exampleTest = {
isPassed: () => ! exampleTest.pending && exampleTest.state === 'passed',
isFailed: () => ! exampleTest.pending && exampleTest.state === 'failed',
isPending: () => exampleTest.pending,
'file': '/Users/jan/Projects/serenity-js/integration/mocha/examples/passing.spec.js',
'file': '/fake/path/serenity-js/integration/mocha/examples/passing.spec.js',
'parent': {
'title': 'A scenario',
'ctx': null, // circular reference, ignore
Expand Down Expand Up @@ -87,8 +87,8 @@ export const exampleTest = {
'_onlySuites': [],
'delayed': false,
},
'file': '/Users/jan/Projects/serenity-js/integration/mocha/examples/passing.spec.js',
'file': '/fake/path/serenity-js/integration/mocha/examples/passing.spec.js',
},
'file': '/Users/jan/Projects/serenity-js/integration/mocha/examples/passing.spec.js',
'file': '/fake/path/serenity-js/integration/mocha/examples/passing.spec.js',
},
} as unknown as Test;
12 changes: 8 additions & 4 deletions packages/mocha/spec/mappers/MochaTestMapper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@integration/testing-tools';
import { FileSystemLocation, Path } from '@serenity-js/core/lib/io';
import { Category, Name, ScenarioDetails } from '@serenity-js/core/lib/model';
import { ArbitraryTag, Category, Name, ScenarioDetails } from '@serenity-js/core/lib/model';
import { describe, it } from 'mocha';

import { MochaTestMapper } from '../../src/mappers';
Expand All @@ -17,12 +17,16 @@ describe('MochaTestMapper', () => {
});

it('maps a Mocha test to Serenity/JS ScenarioDetails', () => {
const scenario = mapper.detailsOf(exampleTest)
const { scenarioDetails, scenarioTags } = mapper.detailsOf(exampleTest)

expect(scenario).to.equal(new ScenarioDetails(
expect(scenarioDetails).to.equal(new ScenarioDetails(
new Name('A scenario passes'),
new Category('Mocha reporting'),
new FileSystemLocation(new Path('/Users/jan/Projects/serenity-js/integration/mocha/examples/passing.spec.js'))
new FileSystemLocation(new Path('/fake/path/serenity-js/integration/mocha/examples/passing.spec.js'))
));

expect(scenarioTags).to.deep.equal([
new ArbitraryTag('smoke-test')
]);
});
});
34 changes: 23 additions & 11 deletions packages/mocha/src/SerenityReporterForMocha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ import {
TestSuiteFinished,
TestSuiteStarts,
} from '@serenity-js/core/lib/events';
import type { RequirementsHierarchy } from '@serenity-js/core/lib/io';
import { FileSystemLocation, Path } from '@serenity-js/core/lib/io';
import { ArbitraryTag, CorrelationId, ExecutionFailedWithError, ExecutionRetriedTag, ExecutionSuccessful, Name, TestSuiteDetails } from '@serenity-js/core/lib/model';
import { FileSystemLocation, Path, type RequirementsHierarchy } from '@serenity-js/core/lib/io';
import {
ArbitraryTag,
CorrelationId,
ExecutionFailedWithError,
ExecutionRetriedTag,
ExecutionSuccessful,
Name,
TestSuiteDetails
} from '@serenity-js/core/lib/model';
import type { MochaOptions, Runnable, Suite, Test } from 'mocha';
import { reporters, Runner } from 'mocha';

Expand All @@ -37,6 +44,7 @@ export class SerenityReporterForMocha extends reporters.Base {

/**
* @param {Serenity} serenity
* @param requirementsHierarchy
* @param {mocha~Runner} runner
* @param {mocha~MochaOptions} options
*/
Expand Down Expand Up @@ -198,19 +206,23 @@ export class SerenityReporterForMocha extends reporters.Base {
private announceSceneStartsFor(test: Test): void {
this.currentSceneId = this.serenity.assignNewSceneId()

const scenario = this.testMapper.detailsOf(test);
const { scenarioDetails, scenarioTags } = this.testMapper.detailsOf(test);

this.emit(
new SceneStarts(this.currentSceneId, scenario, this.serenity.currentTime()),
...this.requirementsHierarchy.requirementTagsFor(scenario.location.path, scenario.category.value)
new SceneStarts(this.currentSceneId, scenarioDetails, this.serenity.currentTime()),

... this.requirementsHierarchy.requirementTagsFor(scenarioDetails.location.path, scenarioDetails.category.value)
.map(tag => new SceneTagged(this.currentSceneId, tag, this.serenity.currentTime())),

new TestRunnerDetected(this.currentSceneId, new Name('Mocha'), this.serenity.currentTime()),

... scenarioTags.map(tag => new SceneTagged(this.currentSceneId, tag, this.serenity.currentTime())),
);
}

private announceSceneFinishedFor(test: Test, runnable: Runnable): Promise<void> {
const
scenario = this.testMapper.detailsOf(test),
{ scenarioDetails } = this.testMapper.detailsOf(test),
outcome = this.recorder.outcomeOf(test) || this.outcomeMapper.outcomeOf(test);

this.emit(
Expand All @@ -224,7 +236,7 @@ export class SerenityReporterForMocha extends reporters.Base {
.then(() => {
this.emit(new SceneFinished(
this.currentSceneId,
scenario,
scenarioDetails,
outcome,
this.serenity.currentTime(),
));
Expand All @@ -233,7 +245,7 @@ export class SerenityReporterForMocha extends reporters.Base {
}, error => {
this.emit(new SceneFinished(
this.currentSceneId,
scenario,
scenarioDetails,
new ExecutionFailedWithError(error),
this.serenity.currentTime(),
));
Expand All @@ -248,7 +260,7 @@ export class SerenityReporterForMocha extends reporters.Base {

private announceSceneSkippedFor(test: Test): void {
const
scenario = this.testMapper.detailsOf(test),
{ scenarioDetails } = this.testMapper.detailsOf(test),
outcome = this.outcomeMapper.outcomeOf(test);

this.announceSceneStartsFor(test);
Expand All @@ -260,7 +272,7 @@ export class SerenityReporterForMocha extends reporters.Base {
),
new SceneFinished(
this.currentSceneId,
scenario,
scenarioDetails,
outcome,
this.serenity.currentTime(),
)
Expand Down
19 changes: 12 additions & 7 deletions packages/mocha/src/mappers/MochaTestMapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FileSystemLocation, Path } from '@serenity-js/core/lib/io';
import { Category, Name, ScenarioDetails } from '@serenity-js/core/lib/model';
import { Category, Name, ScenarioDetails, type Tag, Tags } from '@serenity-js/core/lib/model';
import type { Suite, Test } from 'mocha';

/**
Expand All @@ -9,7 +9,7 @@ export class MochaTestMapper {
constructor(private readonly cwd: Path) {
}

public detailsOf(test: Test): ScenarioDetails {
public detailsOf(test: Test): { scenarioDetails: ScenarioDetails, scenarioTags: Tag[] } {

function fileOf(t) {
switch (true) {
Expand All @@ -28,15 +28,20 @@ export class MochaTestMapper {
const scenarioName = this.nameOf(test);
const title = this.fullNameOf(test);

const name = scenarioName || title;

const featureName = scenarioName
? this.featureNameFor(test)
: this.cwd.relative(path).value;

return new ScenarioDetails(
new Name(scenarioName || title),
new Category(featureName),
new FileSystemLocation(path),
);
return {
scenarioDetails: new ScenarioDetails(
new Name(Tags.stripFrom(name)),
new Category(Tags.stripFrom(featureName)),
new FileSystemLocation(path),
),
scenarioTags: Tags.from(`${ featureName } ${ name }`),
};
}

public featureNameFor(scenario: Test | Suite): string {
Expand Down

0 comments on commit b86f2bb

Please sign in to comment.