Skip to content
Permalink
Browse files
feat(core): knownUnkowns - an Actor answers Questions and more!
affects: @serenity-js/assertions, @serenity-js/core
  • Loading branch information
jan-molak committed Oct 23, 2018
1 parent 9d480ee commit 892ba7a30ce7cb5ad8fe69ee68f98dc7aa6facc9
@@ -4,7 +4,7 @@ import { expect } from '@integration/testing-tools';
import { Actor, AssertionError, Question } from '@serenity-js/core';
import { given } from 'mocha-testdata';

import { Ensure, equals, ValueOf } from '../src';
import { Ensure, equals } from '../src';

describe('Ensure', () => {

@@ -1,26 +1,26 @@
import { Activity, AnswersQuestions, AssertionError, Interaction, UsesAbilities } from '@serenity-js/core';
import { Activity, AnswersQuestions, AssertionError, Interaction, KnownUnknown, UsesAbilities } from '@serenity-js/core';
import { Assertion } from './assertions/Assertion';
import { descriptionOf, extracted, ValueOf } from './values';
import { descriptionOf } from './values';

export class Ensure<T> implements Interaction {

static that<V>(
actual: ValueOf<V>,
assertion: Assertion<ValueOf<V>>,
actual: KnownUnknown<V>,
assertion: Assertion<KnownUnknown<V>>,
): Activity {
return new Ensure<V>(actual, assertion);
}

constructor(
private readonly actual: ValueOf<T>,
private readonly assertion: Assertion<ValueOf<T>>,
private readonly actual: KnownUnknown<T>,
private readonly assertion: Assertion<KnownUnknown<T>>,
) {
}

performAs(actor: AnswersQuestions & UsesAbilities): Promise<void> {
return Promise.all([
extracted(this.actual, actor),
extracted(this.assertion.expected, actor),
actor.answer(this.actual),
actor.answer(this.assertion.expected),
])
.then(([ actual, expected ]) => {
if (! this.assertion.test(expected, actual)) {
@@ -1,7 +1,7 @@
import { ValueOf } from '../values';
import { KnownUnknown } from '@serenity-js/core';

export abstract class Assertion<V> {
constructor(public readonly expected: ValueOf<V>) {
constructor(public readonly expected: KnownUnknown<V>) {
}

abstract test(expected: V, actual: V): boolean;
@@ -1,8 +1,8 @@
import { KnownUnknown } from '@serenity-js/core';
import { equal } from 'tiny-types/lib/objects/equal'; // tslint:disable-line:no-submodule-imports
import { ValueOf } from '../values';
import { Assertion } from './Assertion';

export function equals<T>(expected: ValueOf<T>): Assertion<T> {
export function equals<T>(expected: KnownUnknown<T>): Assertion<T> {
return new Equals(expected);
}

@@ -1,40 +1,14 @@
import { AnswersQuestions, Question, UsesAbilities } from '@serenity-js/core';
import { KnownUnknown, Question } from '@serenity-js/core';
import { inspect } from 'util';

/**
* @public
*/
export type ValueOf<T> = Question<PromiseLike<T>> | Question<T> | PromiseLike<T> | T;

/**
* @desc
* Extracts the value wrapped in a Promise, Question or Question<Promise>.
* If the value is not wrapped, it returns the value itself.
*
* @package
* @param {ValueOf<T>} value
* @param {AnswersQuestions} actor
*/
export function extracted<T>(value: ValueOf<T>, actor: AnswersQuestions & UsesAbilities): Promise<T> {
if (isAPromise(value)) {
return value;
}

if (isAQuestion(value)) {
return this.extracted(value.answeredBy(actor), actor);
}

return Promise.resolve(value as T);
}

/**
* @desc
* Provides a human-readable and sync description of the {@link ValueOf<T>}
* Provides a human-readable and sync description of the {@link KnownUnknown<T>}
*
* @package
* @param value
*/
export function descriptionOf(value: ValueOf<any>): string {
export function descriptionOf(value: KnownUnknown<any>): string {
if (! isDefined(value)) {
return inspect(value);
}
@@ -67,9 +41,9 @@ export function descriptionOf(value: ValueOf<any>): string {
* Checks if the value is defined
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function isDefined(v: ValueOf<any>) {
function isDefined(v: KnownUnknown<any>) {
return !! v;
}

@@ -78,9 +52,9 @@ function isDefined(v: ValueOf<any>) {
* Checks if the value defines its own `toString` method
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function hasItsOwnToString(v: ValueOf<any>): v is { toString: () => string } {
function hasItsOwnToString(v: KnownUnknown<any>): v is { toString: () => string } {
return typeof v === 'object'
&& !! (v as any).toString
&& typeof (v as any).toString === 'function'
@@ -92,9 +66,9 @@ function hasItsOwnToString(v: ValueOf<any>): v is { toString: () => string } {
* Checks if the value defines its own `inspect` method
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function isInspectable(v: ValueOf<any>): v is { inspect: () => string } {
function isInspectable(v: KnownUnknown<any>): v is { inspect: () => string } {
return !! (v as any).inspect && typeof (v as any).inspect === 'function';
}

@@ -103,9 +77,9 @@ function isInspectable(v: ValueOf<any>): v is { inspect: () => string } {
* Checks if the value is a {@link Question}
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function isAQuestion<T>(v: ValueOf<T>): v is Question<T> {
function isAQuestion<T>(v: KnownUnknown<T>): v is Question<T> {
return !! (v as any).answeredBy;
}

@@ -114,9 +88,9 @@ function isAQuestion<T>(v: ValueOf<T>): v is Question<T> {
* Checks if the value is a {@link Date}
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function isADate(v: ValueOf<any>): v is Date {
function isADate(v: KnownUnknown<any>): v is Date {
return v instanceof Date;
}

@@ -125,9 +99,9 @@ function isADate(v: ValueOf<any>): v is Date {
* Checks if the value is a {@link Promise}
*
* @private
* @param {ValueOf<any>} v
* @param {KnownUnknown<any>} v
*/
function isAPromise<T>(v: ValueOf<T>): v is Promise<T> {
function isAPromise<T>(v: KnownUnknown<T>): v is Promise<T> {
return !! (v as any).then;
}

@@ -10,7 +10,7 @@ import { expect } from '../../expect';
import { Recorder } from '../../Recorder';
import { AcousticGuitar, Chords, Guitar, MusicSheets, NumberOfGuitarStringsLeft, PlayAChord, PlayAGuitar, PlayASong } from '../example-implementation';

const equals = (expected: number) => (actual: PromiseLike<number>) => expect(actual).to.eventually.equal(expected);
const equals = (expected: number) => (actual: PromiseLike<number>) => expect(actual).to.equal(expected);

describe('Actor', () => {

@@ -14,7 +14,7 @@ describe('Interactions', () => {
it('allows the actor to verify a condition', () => {
const actor = Actor.named('James');

const promise = See.if(SomeAsyncResult(), r => expect(r).to.eventually.equal('some value')).performAs(actor);
const promise = See.if(SomeAsyncResult(), r => expect(r).to.equal('some value')).performAs(actor);

return expect(promise).to.be.eventually.fulfilled;
});
@@ -23,7 +23,7 @@ describe('Interactions', () => {
it('rejects the promise if the condition is not met', () => {
const actor = Actor.named('James');

const promise = See.if(SomeAsyncResult(), r => expect(r).to.eventually.equal('other value')).performAs(actor);
const promise = See.if(SomeAsyncResult(), r => expect(r).to.equal('other value')).performAs(actor);

return expect(promise).to.be.eventually.rejectedWith(AssertionError, `expected 'some value' to equal 'other value'`);
});
@@ -0,0 +1,6 @@
import { Question } from './Question';

/**
* @public
*/
export type KnownUnknown<T> = Question<Promise<T>> | Question<T> | Promise<T> | T;
@@ -1,5 +1,5 @@
import { TestCompromisedError } from '../../errors';
import { serenity } from '../../index';
import { KnownUnknown, serenity } from '../../index';
import { Clock, StageManager } from '../../stage';
import { Ability, AbilityType } from '../Ability';
import { TrackedActivity } from '../activities';
@@ -49,8 +49,20 @@ export class Actor implements PerformsTasks, UsesAbilities, AnswersQuestions {
}, Promise.resolve(null));
}

toSee<T>(question: Question<T>): T {
return question.answeredBy(this);
answer<T>(knownUnknown: KnownUnknown<T>): Promise<T> {
const
isAPromise = <V>(v: KnownUnknown<V>): v is Promise<V> => !! (v as any).then,
isAQuestion = <V>(v: KnownUnknown<V>): v is Question<V> => !! !! (v as any).answeredBy;

if (isAPromise(knownUnknown)) {
return knownUnknown;
}

if (isAQuestion(knownUnknown)) {
return this.answer(knownUnknown.answeredBy(this));
}

return Promise.resolve(knownUnknown as T);
}

whoCan(...abilities: Ability[]): Actor {
@@ -1,5 +1,5 @@
import { Question } from '../Question';
import { KnownUnknown } from '../KnownUnknown';

export interface AnswersQuestions {
toSee<T>(question: Question<T>): T;
answer<T>(knownUnknown: KnownUnknown<T>): Promise<T>;
}
@@ -2,6 +2,7 @@ export * from './actor';
export * from './Ability';
export * from './Activity';
export * from './Interaction';
export * from './KnownUnknown';
export * from './interactions';
export * from './Question';
export * from './Task';
@@ -1,39 +1,21 @@
import { AnswersQuestions, Interaction, Question } from '..';

export type PromisedAssertion<A> = (actual: A) => PromiseLike<any>;
export type Assertion<A> = (actual: A) => void;

export class See<S> implements Interaction {
static if<T>(question: Question<T>, assertion: Assertion<T> | PromisedAssertion<T>) {
static if<T>(question: Question<T>, assertion: Assertion<T>) {
return new See<T>(question, assertion);
}

constructor(
private question: Question<S>,
private assert: Assertion<S> | PromisedAssertion<S>,
private assert: Assertion<S>,
) {
}

performAs(actor: AnswersQuestions): PromiseLike<void> {
return new Promise((resolve, reject) => {
try {
const result = this.assert(actor.toSee(this.question));

if (this.isAPromiseOf(result)) {
return result.then(resolve, reject);
}

resolve(void 0);
}
catch (error) {
reject(error); // todo: an opportunity for a custom assertion error with diffs
}
});
return actor.answer(this.question).then(this.assert);
}

toString = () => `#actor checks ${this.question}`;

private isAPromiseOf(value: any): value is PromiseLike<any> {
return !! value && !! value.then;
}
}

0 comments on commit 892ba7a

Please sign in to comment.