Skip to content
Permalink
Browse files
feat(core): arbitrary data can be attached to interactions reported i…
…n the test reports

affects: @serenity-js/core, @serenity-js-examples/cucumber-domain-level-testing
  • Loading branch information
jan-molak committed Oct 9, 2018
1 parent d1326b9 commit cd67a746f5686104723a03729482ccee45fbae36
Showing 32 changed files with 531 additions and 238 deletions.
@@ -20,7 +20,7 @@ When(/(?:he|she|they) uses? the (.) operator/, function(this: WithStage, operato

Then(/(?:he|she|they) should get a result of (\d+)/, function(this: WithStage, expectedResult: string) {
return this.stage.theActorInTheSpotlight().attemptsTo(
Ensure.that(ResultOfCalculation(), not(equals(parseFloat(expectedResult)))),
Ensure.that(ResultOfCalculation(), equals(parseFloat(expectedResult))),
);
});

@@ -1,14 +1,21 @@
import { EnterOperandCommand, Operand } from '@serenity-js-examples/calculator-app';
import { Actor, Interaction } from '@serenity-js/core';
import { Actor, EmitArtifact, Interaction } from '@serenity-js/core';
import { JSONData } from '@serenity-js/core/lib/model';
import { InteractDirectly } from '../abilities';

export const EnterOperand = (operand: Operand) => Interaction.where(`#actor enters an operand of ${operand.value}`, (actor: Actor) => {
const ability = InteractDirectly.as(actor);
export const EnterOperand = (operand: Operand) =>
Interaction.where(`#actor enters an operand of ${operand.value}`,
(actor: Actor, emitArtifact: EmitArtifact) => {
const ability = InteractDirectly.as(actor);

return ability.execute(
new EnterOperandCommand(
operand,
ability.currentCalculationId(),
),
);
});
const command = new EnterOperandCommand(
operand,
ability.currentCalculationId(),
);

ability.execute(
command,
);

emitArtifact(JSONData.fromJSON(command.toJSON()), command.constructor.name);
});
@@ -1,14 +1,20 @@
import { Operator, UseOperatorCommand } from '@serenity-js-examples/calculator-app';
import { Actor, Interaction } from '@serenity-js/core';
import { EnterOperandCommand, Operand, Operator, UseOperatorCommand } from '@serenity-js-examples/calculator-app';
import { Actor, EmitArtifact, Interaction } from '@serenity-js/core';
import { JSONData } from '@serenity-js/core/lib/model';
import { InteractDirectly } from '../abilities';

export const UseOperator = (operator: Operator) => Interaction.where(`#actor uses the ${ operator.constructor.name }`, (actor: Actor) => {
const ability = InteractDirectly.as(actor);
export const UseOperator = (operator: Operator) => Interaction.where(`#actor uses the ${ operator.constructor.name }`,
(actor: Actor, emitArtifact: EmitArtifact) => {
const ability = InteractDirectly.as(actor);

return ability.execute(
new UseOperatorCommand(
const command = new UseOperatorCommand(
operator,
ability.currentCalculationId(),
),
);
});
);

ability.execute(
command,
);

emitArtifact(JSONData.fromJSON(command.toJSON()), command.constructor.name);
});
@@ -1,20 +1,85 @@
import { Serialised } from 'tiny-types';

import { Artifact, FileType } from '../../src/io';
import { Name } from '../../src/model';
import { Artifact, JSONData, Name, Photo } from '../../src/model';
import { expect } from '../expect';

describe ('Artifact', () => {
describe('Photo', () => {

/** @test {Artifact} */
it('can be serialised and deserialised', () => {
const photo = Photo.fromBase64('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEW01FWbeM52AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==');

const artifact = new Artifact(
new Name('report'),
FileType.JSON,
{ some: 'report '},
);
/** @test {Photo#toJSON} */
it('can be serialised', () => {
const serialised = photo.toJSON();

expect(Artifact.fromJSON(artifact.toJSON() as Serialised<Artifact<any>>)).to.equal(artifact);
expect(serialised.type).to.equal('Photo');
expect(serialised.base64EncodedValue).to.equal(photo.base64EncodedValue);
});

/**
* @test {Photo#toJSON}
* @test {Artifact.fromJSON}
*/
it('can be de-serialised', () => {
const
serialised = photo.toJSON(),
deserialised = Artifact.fromJSON(serialised);

expect(deserialised).to.equal(photo);
});

/**
* @test {Photo#map}
* @test {Photo#base64EncodedValue}
*/
it('allows for its value to be extracted as a Buffer', () => {
photo.map(value => expect(value).to.be.instanceOf(Buffer));
photo.map(value => expect(value.toString('base64')).to.equal(photo.base64EncodedValue));
});

/**
* @test {Photo.fromBuffer}
*/
it('can be instantiated from a Buffer', () => {
expect(Photo.fromBuffer(Buffer.from(photo.base64EncodedValue, 'base64'))).to.equal(photo);
});
});

describe('JSONData', () => {

const json = JSONData.fromJSON({
key: ['v', 'a', 'l', 'u', 'e'],
});

/** @test {JSONData#toJSON} */
it('can be serialised', () => {
const serialised = json.toJSON();

expect(serialised.type).to.equal('JSONData');
expect(serialised.base64EncodedValue).to.equal(json.base64EncodedValue);
});

/**
* @test {JSONData#toJSON}
* @test {Artifact.fromJSON}
*/
it('can be de-serialised', () => {
const
serialised = json.toJSON(),
deserialised = Artifact.fromJSON(serialised);

expect(deserialised).to.equal(json);
});

/**
* @test {JSONData#map}
* @test {JSONData#base64EncodedValue}
*/
it('allows for its value to be extracted as a JSON value', () => {
json.map(value => expect(value).to.be.instanceOf(Object));
json.map(value => expect(value).to.deep.equal({
key: ['v', 'a', 'l', 'u', 'e'],
}));
});
});
});
@@ -0,0 +1,70 @@
import 'mocha';
import * as sinon from 'sinon';

import { Actor, EmitArtifact, Interaction } from '../../src/screenplay';

import { JSONData } from '../../src/model/artifacts';
import { expect } from '../expect';

describe('Interaction', () => {

const Ivonne = Actor.named('Ivonne');

describe('when defining an interaction', () => {

/** @test {Interaction} */
it(`provides a convenient factory method for synchronous interactions`, () => {
const spy = sinon.spy();

const InteractWithTheSystem = () => Interaction.where(`#actor interacts with the system`, (actor: Actor) => {
spy(actor);
});

return expect(Ivonne.attemptsTo(
InteractWithTheSystem(),
))
.to.be.fulfilled
.then(() => {
expect(spy).to.have.been.calledWith(Ivonne);
});
});

/** @test {Interaction} */
it(`provides a convenient factory method for asynchronous interactions`, () => {
const spy = sinon.spy();

const InteractWithTheSystem = () => Interaction.where(`#actor interacts with the system`, (actor: Actor) => {
spy(actor);

return Promise.resolve();
});

return expect(Ivonne.attemptsTo(
InteractWithTheSystem(),
))
.to.be.fulfilled
.then(() => {
expect(spy).to.have.been.calledWith(Ivonne);
});
});
});

/** @test {Interaction} */
it('can optionally emit an artifact to be attached to the report or stored', () => {
const spy = sinon.spy();

const InteractWithTheSystem = () => Interaction.where(`#actor interacts with the system`, (actor: Actor, emitArtifact: EmitArtifact) => {
spy(actor);

emitArtifact(JSONData.fromJSON({ token: '123' }), 'Session Token');
});

return expect(Ivonne.attemptsTo(
InteractWithTheSystem(),
))
.to.be.fulfilled
.then(() => {
expect(spy).to.have.been.calledWith(Ivonne);
});
});
});
@@ -1,9 +1,9 @@
import 'mocha';
import * as sinon from 'sinon';

import { ArtifactGenerated, DomainEvent } from '../../../../src/events';
import { Artifact, FileSystem, FileType, Path } from '../../../../src/io';
import { Duration, Name } from '../../../../src/model';
import { ArtifactArchived, ArtifactGenerated, DomainEvent } from '../../../../src/events';
import { FileSystem, Path } from '../../../../src/io';
import { Duration, JSONData, Name, Photo } from '../../../../src/model';
import { ArtifactArchiver, StageManager } from '../../../../src/stage';

import { expect } from '../../../expect';
@@ -17,8 +17,8 @@ describe('ArtifactArchiver', () => {
archiver: ArtifactArchiver;

beforeEach(() => {
fs = sinon.createStubInstance(FileSystem);
fs.store.returns(Promise.resolve(new Path('/some/absolute/path/to/the/artifact')));
fs = sinon.createStubInstance(FileSystem);
fs.store.callsFake((path: Path, contents: any) => Promise.resolve(path));
});

describe('stores the artifacts generated by the other stage crew members', () => {
@@ -32,19 +32,19 @@ describe('ArtifactArchiver', () => {
});

const
artifactName = new Name('expected-file-name'),
json = { key: 'value' },
expectedJsonFilename = [ artifactName.value, FileType.JSON.extesion.value ].join('.'),
expectedPngFilename = [ artifactName.value, FileType.PNG.extesion.value ].join('.');
jsonArtifactName = new Name('expected-json-artifact-name'),
pngArtifactName = new Name('expected-png-artifact-name');

/**
* @test {ArtifactArchiver}
* @test {ArtifactGenerated}
*/
it('notifies the StageManager when an artifact is saved so that the promise of a stage cue can be fulfilled', () => {
stageManager.notifyOf(
new ArtifactGenerated(new Artifact(artifactName, FileType.JSON, json)),
);
stageManager.notifyOf(new ArtifactGenerated(
jsonArtifactName,
JSONData.fromJSON(json),
));

return expect(stageManager.waitForNextCue()).to.be.fulfilled;
});
@@ -56,9 +56,10 @@ describe('ArtifactArchiver', () => {
it('notifies the StageManager when an artifact cannot be saved so that the promise of a stage cue can be rejected', () => {
fs.store.returns(Promise.reject(new Error('Something happened')));

stageManager.notifyOf(
new ArtifactGenerated(new Artifact(artifactName, FileType.PNG, photo.value)),
);
stageManager.notifyOf(new ArtifactGenerated(
pngArtifactName,
photo,
));

return expect(stageManager.waitForNextCue()).to.be.rejected;
});
@@ -67,13 +68,17 @@ describe('ArtifactArchiver', () => {
* @test {ArtifactArchiver}
* @test {ArtifactGenerated}
*/
it('correctly saves JSON content to a file', () => {
stageManager.notifyOf(
new ArtifactGenerated(new Artifact(artifactName, FileType.JSON, json)),
);
it('correctly saves JSON content to a unique file', () => {
stageManager.notifyOf(new ArtifactGenerated(
jsonArtifactName,
JSONData.fromJSON(json),
));

return stageManager.waitForNextCue().then(() => {
expect(fs.store).to.have.been.calledWith(new Path(expectedJsonFilename), JSON.stringify(json));
expect(fs.store).to.have.been.calledWith(
new Path(`${jsonArtifactName.value}-b283bd69b0fcd75d754f678ac6685786.json`),
JSON.stringify(json),
);
});
});

@@ -82,12 +87,17 @@ describe('ArtifactArchiver', () => {
* @test {ArtifactGenerated}
*/
it('correctly saves PNG content to a file', () => {
stageManager.notifyOf(
new ArtifactGenerated(new Artifact(artifactName, FileType.PNG, photo.value)),
);
stageManager.notifyOf(new ArtifactGenerated(
pngArtifactName,
photo,
));

return stageManager.waitForNextCue().then(() => {
expect(fs.store).to.have.been.calledWith(new Path(expectedPngFilename), photo.value, 'base64');
expect(fs.store).to.have.been.calledWith(
new Path(`${pngArtifactName.value}-4fdc8acbf8f6c958b2726fc8ae435bf5.png`),
photo.base64EncodedValue,
'base64',
);
});
});
});
@@ -120,4 +130,34 @@ describe('ArtifactArchiver', () => {
expect(fs.store).to.not.have.been.called; // tslint:disable-line:no-unused-expression
});
});

/**
* @test {ArtifactArchiver}
* @test {ArtifactGenerated}
* @test {ArtifactArchived}
*/
it('notifies the StageManager when the artifact is correctly archived', () => {
stageManager = new StageManager(Duration.ofMillis(250));

archiver = new ArtifactArchiver(fs as any);
archiver.assignTo(stageManager);
stageManager.register(archiver);

const notifyOf = sinon.spy(stageManager, 'notifyOf');

stageManager.notifyOf(new ArtifactGenerated(
new Name('report'),
JSONData.fromJSON({ key: 'value' }),
));

return expect(stageManager.waitForNextCue()).to.be.fulfilled.then(() => {

const archived: ArtifactArchived = notifyOf.getCall(2).lastArg;

expect(archived).to.be.instanceOf(ArtifactArchived);
expect(archived.name).to.equal(new Name('report'));
expect(archived.type).to.equal(JSONData);
expect(archived.path).to.equal(new Path('report-b283bd69b0fcd75d754f678ac6685786.json'));
});
});
});

0 comments on commit cd67a74

Please sign in to comment.