Skip to content
Permalink
Browse files
feat(assertions): Check.whether enables conditional flow
  • Loading branch information
jan-molak committed Feb 14, 2019
1 parent 25222a9 commit abbac184e44848bc5fa9b8e2fd6ea1f9b4973035
@@ -1,8 +1,10 @@
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinonChai from 'sinon-chai';
import { assertions } from './tiny-types';

chai.use(chaiAsPromised);
chai.use(sinonChai);
chai.use(assertions);

export const expect = chai.expect;
@@ -0,0 +1,87 @@
import 'mocha';

import { expect } from '@integration/testing-tools';
import { Actor, Interaction } from '@serenity-js/core';
import * as sinon from 'sinon';
import { Check, startsWith } from '../src';

/** @test {Check} */
describe('Check', () => {

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

const Call = (fn: () => void) => Interaction.where(`#actor calls a function`, actor => fn());

let spy: sinon.SinonSpy;
beforeEach(() => spy = sinon.spy());

describe('(if branch)', () => {

/** @test {Check.whether} */
/** @test {Check#andIfSo} */
it('makes the actor execute the activities when the expectation is met', () =>
expect(Enrique.attemptsTo(
Check.whether('Hello World', startsWith('Hello'))
.andIfSo(
Call(() => spy(true)),
Call(() => spy(true)),
),
)).to.be.fulfilled.
then(() => {
expect(spy).to.have.been.calledWith(true).callCount(2);
}),
);

/** @test {Check.whether} */
/** @test {Check#andIfSo} */
it('makes the actor ignore the activities when the expectation is not met', () =>
expect(Enrique.attemptsTo(
Check.whether('Hello World', startsWith('¡Hola'))
.andIfSo(
Call(() => spy(true)),
),
)).to.be.fulfilled.
then(() => {
expect(spy).to.not.have.been.called; // tslint:disable-line:no-unused-expression
}),
);
});

describe('(if/else branches)', () => {
/** @test {Check.whether} */
/** @test {Check#andIfSo} */
/** @test {Check#otherwise} */
it('makes the actor execute the activities when the expectation is met', () =>
expect(Enrique.attemptsTo(
Check.whether('Hello World', startsWith('Hello'))
.andIfSo(
Call(() => spy(true)),
)
.otherwise(
Call(() => spy(false)),
),
)).to.be.fulfilled.
then(() => {
expect(spy).to.have.been.calledWith(true).callCount(1);
}),
);

/** @test {Check.whether} */
/** @test {Check#andIfSo} */
/** @test {Check#otherwise} */
it('makes the actor execute the alternative activities when the expectation is not met', () =>
expect(Enrique.attemptsTo(
Check.whether('Hello World', startsWith('¡Hola'))
.andIfSo(
Call(() => spy(true)),
)
.otherwise(
Call(() => spy(false)),
),
)).to.be.fulfilled.
then(() => {
expect(spy).to.have.been.calledWith(false).callCount(1);
}),
);
});
});
@@ -0,0 +1,43 @@
import { Activity, AnswersQuestions, KnowableUnknown, PerformsTasks, Task } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { match } from 'tiny-types';
import { Expectation } from './Expectation';
import { ExpectationMet, ExpectationNotMet, Outcome } from './outcomes';

export class Check<Actual> implements Task {
static whether<A>(actual: KnowableUnknown<A>, expectation: Expectation<any, A>) {
return {
andIfSo: (...activities: Activity[]) => new Check(actual, expectation, activities),
};
}

constructor(
private readonly actual: KnowableUnknown<Actual>,
private readonly expectation: Expectation<any, Actual>,
private readonly activities: Activity[],
private readonly alternativeActivities: Activity[] = [],
) {
}

otherwise(...alternativeActivities: Activity[]) {
return new Check<Actual>(this.actual, this.expectation, this.activities, alternativeActivities);
}

performAs(actor: AnswersQuestions & PerformsTasks): PromiseLike<void> {
return Promise.all([
actor.answer(this.actual),
actor.answer(this.expectation),
]).then(([actual, expectation]) =>
expectation(actual).then(outcome =>
match<Outcome<any, Actual>, void>(outcome)
.when(ExpectationMet, o => actor.attemptsTo(...this.activities))
.when(ExpectationNotMet, o => actor.attemptsTo(...this.alternativeActivities))
.else(_ => void 0),
),
);
}

toString(): string {
return formatted `#actor ensures that ${ this.actual } does ${ this.expectation }`;
}
}
@@ -6,23 +6,23 @@ import { Expectation } from './Expectation';
import { ExpectationNotMet, Outcome } from './outcomes';

export class Ensure<Actual> implements Interaction {
static that<A>(actual: KnowableUnknown<A>, assertion: Expectation<any, A>) {
return new Ensure(actual, assertion);
static that<A>(actual: KnowableUnknown<A>, expectation: Expectation<any, A>) {
return new Ensure(actual, expectation);
}

constructor(
private readonly actual: KnowableUnknown<Actual>,
private readonly assertion: Expectation<Actual>,
private readonly expectation: Expectation<Actual>,
) {
}

performAs(actor: AnswersQuestions): PromiseLike<void> {
return Promise.all([
actor.answer(this.actual),
actor.answer(this.assertion),
actor.answer(this.expectation),
]).
then(([ actual, assertion ]) =>
assertion(actual).then(outcome =>
then(([ actual, expectation ]) =>
expectation(actual).then(outcome =>
match<Outcome<any, Actual>, void>(outcome)
.when(ExpectationNotMet, o => {
throw new AssertionError(
@@ -37,6 +37,6 @@ export class Ensure<Actual> implements Interaction {
}

toString(): string {
return formatted `#actor ensures that ${ this.actual } does ${ this.assertion }`;
return formatted `#actor ensures that ${ this.actual } does ${ this.expectation }`;
}
}
@@ -1,3 +1,4 @@
export * from './Check';
export * from './Expectation';
export * from './expectations';
export * from './Ensure';
@@ -3,12 +3,7 @@ import { PerformsTasks } from './actor';

export abstract class Task implements Activity {
static where(description: string, ...activities: Activity[]): Task {
// todo: if there are no activities, make it a PendingActivity
return new AnonymousTask(description, activities);

// return activities.length > 0
// ? new AnonymousTask(description, activities)
// : new PendingActivity();
}

abstract performAs(actor: PerformsTasks): PromiseLike<void>;
@@ -5,10 +5,12 @@ import { RuntimeError } from '../../../../errors';
export class ErrorRenderer {
render(error: Error) {
// todo: add diff for AssertionError
return {
...this.renderError(error),
...((error instanceof RuntimeError && error.cause) ? { rootCause: this.renderError(error.cause) } : {}),
};
// tslint:disable-next-line:prefer-object-spread Esdoc doesn't understand spread
return Object.assign(
{},
this.renderError(error),
((error instanceof RuntimeError && error.cause) ? { rootCause: this.renderError(error.cause) } : {}),
);
}

private renderError(error: Error) {

0 comments on commit abbac18

Please sign in to comment.