Skip to content
Permalink
Browse files
feat(core): an interaction to Loop.over an Answerable<Array<T>>
  • Loading branch information
jan-molak committed Aug 25, 2020
1 parent fbe995c commit ded7dc252609e1c9caf67c92784c82cfc3dbeaa5
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 0 deletions.
@@ -0,0 +1,101 @@
import 'mocha';
import { given } from 'mocha-testdata';

import { Serenity } from '../../../src';
import { Cast } from '../../../src/stage';
import { Actor, Question } from '../../../src/screenplay';
import { TaskFinished, TaskStarts } from '../../../src/events';
import { Loop } from '../../../src/screenplay/tasks';
import { expect } from '../../expect';
import { Recorder } from '../../Recorder';
import { Spy } from './Spy';

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

let serenity: Serenity,
recorder: Recorder;

class Actors implements Cast {
prepare(actor: Actor): Actor {
return actor;
}
}

beforeEach(() => {
serenity = new Serenity();
recorder = new Recorder();

serenity.configure({
crew: [ recorder ],
actors: new Actors(),
});
});

afterEach(() => Spy.reset());

const
personA = { name: 'Alice' },
personB = { name: 'Bob' },
personC = { name: 'Celine' },
listOfPeople = [ personA, personB, personC ],
emptyList = [],
p = <T>(value: T): Promise<T> => Promise.resolve(value),
q = <T>(value: T): Question<T> => Question.about('a list of people', actor => value);

given([
{ description: 'static list', list: listOfPeople },
{ description: 'Promise<list>', list: p(listOfPeople) },
{ description: 'Question<list>', list: q(listOfPeople) },
{ description: 'Question<Promise<list>>', list: q(p(listOfPeople)) },
]).
it(`iterates over a list`, ({ list }) =>
serenity.theActorCalled('Looper Joe').attemptsTo(
Loop.over(list).to(Spy.on(Loop.item(), Loop.index()))
).then(() => {
expect(Spy.call(0)).to.deep.equal([ personA, 0 ]);
expect(Spy.call(1)).to.deep.equal([ personB, 1 ]);
expect(Spy.call(2)).to.deep.equal([ personC, 2 ]);
}));

given([
{ description: 'static list', list: emptyList },
{ description: 'Promise<list>', list: p(emptyList) },
{ description: 'Question<list>', list: q(emptyList) },
{ description: 'Question<Promise<list>>', list: q(p(emptyList)) },
]).
it(`doesn't iterate over an empty list`, ({ list }) =>
serenity.theActorCalled('Looper Joe').attemptsTo(
Loop.over(list).to(Spy.on(Loop.item()))
).then(() => {
expect(Spy.calls()).to.equal(0);
}));

it(`reports the loop even when there are no activities to perform`, () =>
serenity.theActorCalled('Looper Joe').attemptsTo(
Loop.over(q(listOfPeople)).to(/* do nothing */)
).then(() => {
expect(recorder.events).to.have.lengthOf(2);

const taskStarts = recorder.events[0] as TaskStarts;
expect(taskStarts).to.be.instanceOf(TaskStarts);
expect(taskStarts.value.name.value).to.equal('Looper Joe loops over a list of people');

const taskFinished = recorder.events[1] as TaskFinished;
expect(taskFinished).to.be.instanceOf(TaskFinished);
expect(taskFinished.value.name.value).to.equal('Looper Joe loops over a list of people');
}));

given([
{ description: 'empty list', list: emptyList, expected: '#actor loops over a list of 0 items' },
{ description: 'singleton list', list: ['apple'], expected: '#actor loops over a list of 1 item' },
{ description: 'static list', list: listOfPeople, expected: '#actor loops over a list of 3 items' },
{ description: 'Promise<list>', list: p(listOfPeople), expected: '#actor loops over a Promise' },
{ description: 'Question<list>', list: q(listOfPeople), expected: '#actor loops over a list of people' },
{ description: 'Question<Promise<list>>', list: q(p(listOfPeople)), expected: '#actor loops over a list of people' },
]).
it(`provides a sensible description of the task being performed`, ({ list, expected }) => {
expect(Loop.over(list).to().toString())
.to.equal(expected);
});
});
@@ -0,0 +1,38 @@
import { Answerable, AnswersQuestions, Interaction } from '../../../src/screenplay';

export class Spy extends Interaction {
private static callArgs: any[][] = [];

static on(...answerables: Array<Answerable<any>>) {
return new Spy(answerables);
}

static call(n: number): any[] {
return Spy.callArgs[n];
}

static calls(): number {
return Spy.callArgs.length;
}

static reset(): void {
Spy.callArgs = [];
}

constructor(private readonly answerables: Array<Answerable<any>>) {
super();
}

performAs(actor: AnswersQuestions): PromiseLike<void> {
return Promise.all(
this.answerables.map(answerable => actor.answer(answerable))
).then(answers => {
Spy.callArgs.push(answers);
});
}

toString(): string {
return `#actor spies...`;
}
}

@@ -1,6 +1,10 @@
import { Question } from './Question';

/**
* @desc
* A union type that provides a convenient way to represent any value
* that can be resolved by {@link Actor#answer}.
*
* @public
*
* @typedef {Question<Promise<T>> | Question<T> | Promise<T> | T} Answerable<T>
@@ -0,0 +1,16 @@
/**
* @desc
* Describes a collection providing
* a [reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)-like interface.
*
* @interface
*
* @see {@link ElementArrayFinder}
* @see {@link Loop}
*/
export interface Reducible {
/**
* @type {function<T,A>(callback: (accumulator: A, currentValue: T, index: number) => A, initialValue: A): PromiseLike<A> | A}
*/
reduce: <T, A>(fn: (accumulator: A, currentValue: T, index: number) => A, initialValue: A) => PromiseLike<A> | A;
}
@@ -9,4 +9,6 @@ export * from './Interaction';
export * from './interactions';
export * from './Question';
export * from './questions';
export * from './Reducible';
export * from './Task';
export * from './tasks';

0 comments on commit ded7dc2

Please sign in to comment.