spec/stage/crew/artifact-archiver/ArtifactArchiver.spec.ts
import 'mocha';
import * as sinon from 'sinon';
import { Actor, StageCrewMember } from '../../../../src';
import { ArtifactArchived, ArtifactGenerated, DomainEvent } from '../../../../src/events';
import { FileSystem, Path } from '../../../../src/io';
import { CorrelationId, Duration, JSONData, Name, TestReport } from '../../../../src/model';
import { ArtifactArchiver, Cast, Clock, Stage, StageManager } from '../../../../src/stage';
import { expect } from '../../../expect';
import { photo } from '../samples';
describe('ArtifactArchiver', () => {
const
json = { key: 'value' },
jsonValueShortHash = '64cdd772d3',
photoShortHash = '6808b2e9fe',
sceneId = new CorrelationId('a-scene');
let stage: Stage,
fs: sinon.SinonStubbedInstance<FileSystem>,
archiver: StageCrewMember;
class Extras implements Cast {
prepare(actor: Actor): Actor {
return actor;
}
}
beforeEach(() => {
fs = sinon.createStubInstance(FileSystem);
fs.store.callsFake((path: Path, contents: any) => Promise.resolve(path));
});
describe('stores the artifacts generated by other stage crew members', () => {
beforeEach(() => {
stage = new Stage(new Extras(), new StageManager(Duration.ofMilliseconds(250), new Clock()));
archiver = new ArtifactArchiver(fs as any, stage);
stage.assign(archiver);
});
const
jsonArtifactName = new Name('Scenario Name'),
expectedJsonFileName = 'scenario-name',
pngArtifactName = new Name('PNG Artifact name'),
expectedPngFileName = 'png-artifact-name';
it('notifies the StageManager when an artifact is saved so that the promise of a stage cue can be fulfilled', () => {
stage.announce(new ArtifactGenerated(
sceneId,
jsonArtifactName,
JSONData.fromJSON(json),
));
return expect(stage.waitForNextCue()).to.be.fulfilled;
});
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')));
stage.announce(new ArtifactGenerated(
sceneId,
pngArtifactName,
photo,
));
return expect(stage.waitForNextCue()).to.be.rejected;
});
it('correctly saves the test report to a unique file', () => {
stage.announce(new ArtifactGenerated(
sceneId,
jsonArtifactName,
TestReport.fromJSON(json),
));
return stage.waitForNextCue().then(() => {
expect(fs.store).to.have.been.calledWith(
new Path(`scenario-${ expectedJsonFileName }-${ jsonValueShortHash }.json`),
JSON.stringify(json),
);
});
});
it('correctly saves PNG content to a file', () => {
stage.announce(new ArtifactGenerated(
sceneId,
pngArtifactName,
photo,
));
return stage.waitForNextCue().then(() => {
expect(fs.store).to.have.been.calledWith(
new Path(`photo-${ expectedPngFileName }-${ photoShortHash }.png`),
photo.base64EncodedValue,
'base64',
);
});
});
it(`ensures that the generate file name doesn't contain special characters`, () => {
const emittedEventName = 'linux-chrome-87-0.4280.88-jasmine-navigates-to-https://www.bounteous.com/#sr=g&m=o&cp=or&ct=-tmc&st=(opu%20qspwjefe)&ts=1402322447';
const expectedFileName = `photo-linux-chrome-87-0.4280.88-jasmine-navigates-to-https-www.bount-${ photoShortHash }.png`;
stage.announce(new ArtifactGenerated(
sceneId,
new Name(emittedEventName),
photo,
));
return stage.waitForNextCue().then(() => {
expect(fs.store).to.have.been.calledWith(
new Path(expectedFileName),
photo.base64EncodedValue,
'base64',
);
});
});
});
describe(`when it encounters events it's not interested in`, () => {
class SomeEvent extends DomainEvent {
constructor() {
super();
}
}
const someEvent = new SomeEvent();
it('ignores them', () => {
const stageManager = sinon.createStubInstance(StageManager);
fs = sinon.createStubInstance(FileSystem);
stage = new Stage(new Extras(), stageManager as unknown as StageManager);
archiver = new ArtifactArchiver(fs as any, stage);
stage.assign(archiver);
archiver.notifyOf(
someEvent,
);
expect(stageManager.notifyOf).to.not.have.been.called;
expect(fs.store).to.not.have.been.called;
});
});
it('notifies the StageManager when the artifact is correctly archived', () => {
const stageManager = new StageManager(Duration.ofMilliseconds(250), new Clock());
stage = new Stage(new Extras(), stageManager);
archiver = new ArtifactArchiver(fs as any, stage);
stage.assign(archiver);
const notifyOf = sinon.spy(stageManager, 'notifyOf');
stageManager.notifyOf(new ArtifactGenerated(
sceneId,
new Name('Some Report Name'),
TestReport.fromJSON(json),
));
return expect(stageManager.waitForNextCue()).to.be.fulfilled.then(() => {
const archived: ArtifactArchived = notifyOf.getCall(2).lastArg;
expect(archived).to.be.instanceOf(ArtifactArchived);
expect(archived.sceneId).to.equal(sceneId);
expect(archived.name).to.equal(new Name('Some Report Name'));
expect(archived.type).to.equal(TestReport);
expect(archived.path).to.equal(new Path(`scenario-some-report-name-${ jsonValueShortHash }.json`));
});
});
describe('when instantiated using a factory method', () => {
it('joins the path segments provided so that the developer doesn\'t need to worry about cross-OS compatibility of the path', () => {
archiver = ArtifactArchiver.storingArtifactsAt(process.cwd(), 'target', 'site/serenity');
expect((archiver as any).fileSystem.root).to.equal(new Path(process.cwd()).join(new Path('target/site/serenity')));
});
it('complains if the destination is not provided', () => {
expect(() => ArtifactArchiver.storingArtifactsAt()).to.throw(Error, `Path to destination directory should have a property "length" that is greater than 0`);
});
});
});