Skip to content
Permalink
Browse files
feat(protractor): better API for Select.options and Select.values
Both APIs allow for more natuaral invocation using varargs as well as mixing of Answerable<string>
and Answerable<string[]>

Closes #373
  • Loading branch information
jan-molak committed Aug 16, 2020
1 parent 0904a33 commit 3331f578cbfd4d29d0a1925633863113495f2a62
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 17 deletions.
@@ -0,0 +1,38 @@
import 'mocha';
import { given } from 'mocha-testdata';
import { commaSeparated } from '../../src/io';
import { Question } from '../../src/screenplay';
import { expect } from '../expect';

describe('commaSeparated', () => {

const
p = value => Promise.resolve(value),
q = value => Question.about(`the meaning of life`, actor => value),
ts = value => ({ toString: () => value });

it('returns an empty string for an empty list', () => {
expect(commaSeparated([])).to.equal('');
});

given([
{ list: [ 'value' ], expected: 'value' }
]).
it('returns a string representation of a singleton list', ({ list, expected }) => {
expect(commaSeparated(list)).to.equal(expected);
});

given([
{ list: [ 'first', 'second' ], expected: 'first and second' }
]).
it('joins the last two elements with an "and"', ({ list, expected }) => {
expect(commaSeparated(list)).to.equal(expected);
});

given([
{ list: [ 'first', 'second', 'third', 'fourth' ], expected: 'first, second, third and fourth' }
]).
it('joins other elements with a comma', ({ list, expected }) => {
expect(commaSeparated(list)).to.equal(expected);
});
});
@@ -0,0 +1,30 @@
/**
* @desc
* Produces a comma-separated list based on the list provided.
*
* @param {Array<string>} list
* @param {function(item: string): string} map
* @param {string} [acc=''] acc
*/
export function commaSeparated(
list: Array<string | { toString: () => string }>,
map = item => `${ item }`.trim(),
acc: string = '',
): string {
switch (list.length) {
case 0: return acc;
case 1: return commaSeparated(tail(list), map, `${ acc }${ map(head(list)) }`);
case 2: return commaSeparated(tail(list), map, `${ acc }${ map(head(list)) } and `);
default: return commaSeparated(tail(list), map, `${ acc }${ map(head(list)) }, `);
}
}

/** @package */
function head<T>(list: T[]): T {
return list[0];
}

/** @package */
function tail<T>(list: T[]): T[] {
return list.slice(1);
}
@@ -1,4 +1,5 @@
export * from './AssertionReportDiffer';
export * from './commaSeparated';
export * from './ErrorSerialiser';
export * from './ErrorStackParser';
export * from './FileSystem';
@@ -143,6 +143,8 @@ describe('Select', () => {
<li>Poland</li>
<li>United Kingdom</li>
</ul>
<p id='another-country-of-interest-code'>DE</p>
<p id='another-country-of-interest-name'>Germany</p>
</body>
</html>
`);
@@ -151,6 +153,8 @@ describe('Select', () => {
static selector = Target.the('country selector').located(by.id('multi-option-select'));
static countryCodes = Target.all('country codes').located(by.css('#country-of-interest-codes li'));
static countryNames = Target.all('country names').located(by.css('#country-of-interest-names li'));
static anotherCountryCode = Target.the('another country code').located(by.css('#another-country-of-interest-code'));
static anotherCountryName = Target.the('another country name').located(by.css('#another-country-of-interest-name'));
}

describe('Select.values', () => {
@@ -160,7 +164,7 @@ describe('Select', () => {
it('should select multiple options by their static value', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(pageWithMultiSelect),
Select.values(['PL', 'DE']).from(MultiSelectPage.selector),
Select.values('PL', 'DE').from(MultiSelectPage.selector),
Ensure.that(Selected.valuesOf(MultiSelectPage.selector), equals(['PL', 'DE']))
));

@@ -173,12 +177,34 @@ describe('Select', () => {
Ensure.that(Selected.valuesOf(MultiSelectPage.selector), equals(['UK', 'PL']))
));

/** @test {Select.values} */
/** @test {Selected.valuesOf} */
it('should concatenate option values from several Answerables', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(pageWithMultiSelect),
Select.values(
Text.ofAll(MultiSelectPage.countryCodes),
Text.of(MultiSelectPage.anotherCountryCode),
'FR'
).from(MultiSelectPage.selector),
Ensure.that(Selected.valuesOf(MultiSelectPage.selector), equals(['UK', 'PL', 'DE', 'FR']))
));

/** @test {Select.values} */
/** @test {Selected.valuesOf} */
it('should concatenate option values from several static values', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(pageWithMultiSelect),
Select.values('UK', 'PL').from(MultiSelectPage.selector),
Ensure.that(Selected.valuesOf(MultiSelectPage.selector), equals(['UK', 'PL']))
));

describe('toString()', () => {

/** @test {Select.values} */
it('provides a sensible description of the interaction being performed', () => {
expect(Select.values(['PL', 'DE']).from(MultiSelectPage.selector).toString())
.to.equal(`#actor selects values [ 'PL', 'DE' ] from the country selector`);
expect(Select.values(['PL', 'DE'], 'FR').from(MultiSelectPage.selector).toString())
.to.equal(`#actor selects values 'PL', 'DE' and 'FR' from the country selector`);
});

/** @test {Selected.valuesOf} */
@@ -208,15 +234,42 @@ describe('Select', () => {
Ensure.that(Selected.optionsOf(MultiSelectPage.selector), equals(['United Kingdom', 'Poland']))
));

/** @test {Select.values} */
/** @test {Selected.valuesOf} */
it('should concatenate option values from several Answerables', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(pageWithMultiSelect),
Select.options(
Text.ofAll(MultiSelectPage.countryNames),
Text.of(MultiSelectPage.anotherCountryName),
'France'
).from(MultiSelectPage.selector),
Ensure.that(Selected.optionsOf(MultiSelectPage.selector), equals(['United Kingdom', 'Poland', 'Germany', 'France']))
));

/** @test {Select.values} */
/** @test {Selected.valuesOf} */
it('should concatenate option values from several static values', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(pageWithMultiSelect),
Select.options(['Poland', 'Germany'], 'France').from(MultiSelectPage.selector),
Ensure.that(Selected.optionsOf(MultiSelectPage.selector), equals(['Poland', 'Germany', 'France']))
));

describe('toString()', () => {

/** @test {Select.options} */
it('provides a sensible description of the interaction being performed', () => {
expect(Select.options(['Poland', 'France']).from(MultiSelectPage.selector).toString())
.to.equal(`#actor selects [ 'Poland', 'France' ] from the country selector`);
expect(
Select.options(
['Poland', 'Germany' ],
'France',
Text.of(MultiSelectPage.anotherCountryName)
).from(MultiSelectPage.selector).toString()
).to.equal(`#actor selects 'Poland', 'Germany', 'France' and the text of the another country name from the country selector`);
});

/** @test {Selected.optionOf} */$
/** @test {Selected.optionOf} */
it('provides a sensible description of the question being answered', () => {
expect(Selected.optionsOf(MultiSelectPage.selector).toString())
.to.equal(`options selected in the country selector`);
@@ -1,5 +1,6 @@
import { Answerable, AnswersQuestions, Question } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { commaSeparated, formatted } from '@serenity-js/core/lib/io';
import { inspected } from '@serenity-js/core/lib/io/inspected';
import { Interaction, UsesAbilities } from '@serenity-js/core/lib/screenplay';
import { by, ElementFinder, protractor } from 'protractor';
import { promiseOf } from '../../promiseOf';
@@ -14,7 +15,7 @@ export class Select {
};
}

static values(values: string[] | Question<Promise<string[]>>) {
static values(...values: Array<Answerable<string[] | string>>) {
return {
from: (target: Question<ElementFinder> | ElementFinder): Interaction =>
new SelectValues(values, target)
@@ -28,7 +29,7 @@ export class Select {
};
}

static options(values: string[] | Question<Promise<string[]>>) {
static options(...values: Array<Answerable<string[] | string>>) {
return {
from: (target: Question<ElementFinder> | ElementFinder): Interaction =>
new SelectOptions(values, target)
@@ -67,16 +68,17 @@ class SelectValue implements Interaction {
class SelectValues implements Interaction {

constructor(
private readonly values: string[] | Question<Promise<string[]>>,
private readonly values: Array<Answerable<string[] | string>>,
private readonly target: Question<ElementFinder> | ElementFinder
) {
}

performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {

return actor.answer(this.values)
return Promise.all(this.values.map(value => actor.answer(value)))
.then(flatten)
.then(values => {

// todo: extract
const hasRequiredValue = (option: ElementFinder) => option.getAttribute('value').then(value => !!~values.indexOf(value)),
isAlreadySelected = (option: ElementFinder) => option.isSelected(),
ensureOnlyOneApplies = (list: boolean[]) => list.filter(_ => _ === true).length === 1,
@@ -95,7 +97,7 @@ class SelectValues implements Interaction {
}

toString () {
return formatted `#actor selects values ${ this.values } from ${ this.target }`;
return `#actor selects values ${ commaSeparated(flatten(this.values), inspected) } from ${ this.target }`;
}
}

@@ -130,14 +132,14 @@ class SelectOption implements Interaction {
class SelectOptions implements Interaction {

constructor(
// todo: vararg
private readonly values: string[] | Question<Promise<string[]>>,
private readonly values: Array<Answerable<string[] | string>>,
private readonly target: Question<ElementFinder> | ElementFinder
) {
}

performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return actor.answer(this.values)
return Promise.all(this.values.map(value => actor.answer(value)))
.then(flatten)
.then(values => {

const hasRequiredText = (option: ElementFinder) => option.getText().then(value => !!~values.indexOf(value)),
@@ -159,6 +161,13 @@ class SelectOptions implements Interaction {
}

toString () {
return formatted `#actor selects ${ this.values } from ${ this.target }`;
return `#actor selects ${ commaSeparated(flatten(this.values), inspected) } from ${ this.target }`;
}
}

/** @package */
function flatten<T>(listOfLists: Array<T[] | T>): T[] {
return listOfLists
.map(item => [].concat(item))
.reduce((acc: T[], list: T[]) => acc.concat(list), []);
}

0 comments on commit 3331f57

Please sign in to comment.