Skip to content

Commit

Permalink
feat(core): arbitrary data can be attached to interactions reported i…
Browse files Browse the repository at this point in the history
…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 cd67a74
Show file tree
Hide file tree
Showing 32 changed files with 531 additions and 238 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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);
});
85 changes: 75 additions & 10 deletions packages/core/spec/io/Artifact.spec.ts
Original file line number Diff line number Diff line change
@@ -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'],
}));
});
});
});
70 changes: 70 additions & 0 deletions packages/core/spec/screenplay/Interaction.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => {
Expand All @@ -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;
});
Expand All @@ -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;
});
Expand All @@ -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),
);
});
});

Expand All @@ -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',
);
});
});
});
Expand Down Expand Up @@ -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'));
});
});
});
Loading

0 comments on commit cd67a74

Please sign in to comment.