Skip to content

Commit

Permalink
feat(core): introduced MetaQuestionAdapter
Browse files Browse the repository at this point in the history
to allow for more advanced List and PageElements filtering patterns
  • Loading branch information
jan-molak committed Aug 4, 2023
1 parent 8fd333e commit b6676fd
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 85 deletions.
2 changes: 1 addition & 1 deletion integration/web-specs/spec/expectations/isActive.spec.ts
Expand Up @@ -93,7 +93,7 @@ describe('isActive', function () {
| Expectation: isPresent\\(\\)
|
| Expected boolean:\\s+true
| Received Proxy<QuestionStatement>
| Received Proxy<MetaQuestionStatement>
|
| Unanswered { }`, 'gm')));
});
Expand Down
Expand Up @@ -141,9 +141,11 @@ describe('PageElements', () => {
const parents = () =>
PageElements.located(By.css('.parent'))
.of(filterStrategySection())
.describedAs('parents')

const children = () =>
PageElements.located(By.css('.child'));
PageElements.located(By.css('.child'))
.describedAs('children');

it(`finds the element, if one exists`, () =>
actorCalled('Peggy').attemptsTo(
Expand All @@ -159,6 +161,18 @@ describe('PageElements', () => {
),
));

it(`finds the element based on one of the children`, () =>
actorCalled('Peggy').attemptsTo(
Ensure.that(
Attribute.called('data-test-id').of(
parents()
.where(Text.of(children().last()), equals('coffee'))
.first()
),
equals('parent-2')
),
));

it(`allows to check if the element of interest is present`, () =>
actorCalled('Peggy').attemptsTo(
Ensure.that(
Expand Down
94 changes: 65 additions & 29 deletions packages/core/spec/screenplay/QuestionAdapter.spec.ts
Expand Up @@ -14,7 +14,7 @@ describe('Question', () => {
let actor: Actor;

beforeEach(() => {
actor = actorCalled('Stella');
actor = actorCalled('Stella');
});
afterEach(() => actor.dismiss());

Expand All @@ -35,8 +35,7 @@ describe('Question', () => {
{ description: 'Promise<object>', actual: p({ name: 'Alice' }), expected: { name: 'Alice' } },
{ description: 'primitive', actual: 42, expected: 42 },
{ description: 'Promise<primitive>', actual: p(42), expected: 42 },
]).
it('returns a promise of the underlying answer', async ({ expected, actual }) => {
]).it('returns a promise of the underlying answer', async ({ expected, actual }) => {
const answer = await actor.answer(Question.about('some value', _actor => actual));

expect(answer).to.deep.equal(expected);
Expand All @@ -48,15 +47,14 @@ describe('Question', () => {
given([
undefined,
null, // eslint-disable-line unicorn/no-null
]).
it('resolves to false when the answer is not defined', async (value: any) => {
]).it('resolves to false when the answer is not defined', async (value: any) => {
const answer = await actor.answer(Question.about('some value', _actor => value).isPresent());

expect(answer).to.equal(false);
});

it('resolves to false when one of the links in the chain is not defined', async () => {
const example: { a?: { b?: { c?: string }}} = { a: {} };
const example: { a?: { b?: { c?: string } } } = { a: {} };

const actual = await actor.answer(
Question.about('some array', _actor => example)
Expand All @@ -76,7 +74,7 @@ describe('Question', () => {
});

it('provides a human-readable description', async () => {
const example: { a?: { b?: { c?: string }}} = { a: {} };
const example: { a?: { b?: { c?: string } } } = { a: {} };

const actual = Question.about('some array', _actor => example)
.a
Expand All @@ -89,7 +87,7 @@ describe('Question', () => {
});

it('can have its description overridden', async () => {
const example: { a?: { b?: { c?: string }}} = { a: {} };
const example: { a?: { b?: { c?: string } } } = { a: {} };

const actual = Question.about('some array', _actor => example)
.a
Expand Down Expand Up @@ -147,7 +145,7 @@ describe('Question', () => {

it('resolves to `undefined` if any links in the chain are `undefined`', async () => {

const example: { a?: { b?: { c?: string }}} = { a: {} };
const example: { a?: { b?: { c?: string } } } = { a: {} };

const QuestionAdapter = Question.about('some array', _actor => example)
.a
Expand All @@ -161,7 +159,7 @@ describe('Question', () => {

it('resolves to `undefined` when a mapping function is used on a chain with an undefined link', async () => {

const example: { a?: { b?: { c?: string }}} = { a: {} };
const example: { a?: { b?: { c?: string } } } = { a: {} };

const QuestionAdapter = Question.about('some array', _actor => example)
.a
Expand Down Expand Up @@ -235,9 +233,13 @@ describe('Question', () => {
interface Person {
firstName: string;
lastName: string;

fullName(): string;

address: Address;

lastKnownAddress(): Address;

siblings?: Person[]
}

Expand All @@ -259,7 +261,7 @@ describe('Question', () => {
return this.address
},
address,
siblings: [ ],
siblings: [],
}

const Michael: Person = {
Expand All @@ -272,13 +274,13 @@ describe('Question', () => {
return this.address
},
address,
siblings: [ ],
siblings: [],
}

Jane.siblings.push(Michael);
Michael.siblings.push(Jane);

return [Jane, Michael]
return [ Jane, Michael ]
}

// eslint-disable-next-line unicorn/consistent-function-scoping
Expand All @@ -300,7 +302,7 @@ describe('Question', () => {

const name: Question<Promise<string>> = Value(Jane).siblings[0].siblings[0].firstName;

const subject: string = name.toString();
const subject: string = name.toString();

expect(subject).to.equal('<<some value>>.siblings[0].siblings[0].firstName');
});
Expand All @@ -311,16 +313,16 @@ describe('Question', () => {

it('is an instance of Interaction', async () => {

const stack: Interaction & QuestionAdapter<Array<number>> = Question.about('some stack', _actor => [1,2,3]);
const stack: Interaction & QuestionAdapter<Array<number>> = Question.about('some stack', _actor => [ 1, 2, 3 ]);
const push: Interaction = stack.push(4);

expect(stack).to.be.instanceOf(Interaction);
expect(push).to.be.instanceOf(Interaction);
});

it('allows for method calls to be executed as part of the Actor flow', async () => {
const actual: number[] = [];
const expected = [ 1, 2 ];
const actual: number[] = [];
const expected = [ 1, 2 ];

const stack: QuestionAdapter<Array<number>> = Question.about('some stack', _actor => actual);

Expand Down Expand Up @@ -358,8 +360,8 @@ describe('Question', () => {
});

it('wraps methods of the underlying answer in Interactions', async () => {
const example = [ 3, 1, 2 ];
const sorted = [ 1, 2, 3 ];
const example = [ 3, 1, 2 ];
const sorted = [ 1, 2, 3 ];

await actor.attemptsTo(
Question.about('some array', _actor => example).sort(),
Expand All @@ -369,8 +371,8 @@ describe('Question', () => {
});

it('makes the wrapped methods of the underlying answer accept Answerables', async () => {
const example = [ 1, 2, 3 ];
const expected = [ 1, 2, 3, 4, 5, 6 ];
const example = [ 1, 2, 3 ];
const expected = [ 1, 2, 3, 4, 5, 6 ];

await actor.attemptsTo(
Question.about('some array', _actor => example).push(
Expand All @@ -384,8 +386,8 @@ describe('Question', () => {
});

it('allows chaining method calls', async () => {
const example = [ 'c', 'b', 'a' ];
const expected = [ 'a', 'b', 'c', 'd', 'e' ];
const example = [ 'c', 'b', 'a' ];
const expected = [ 'a', 'b', 'c', 'd', 'e' ];

const actual = await actor.answer(
Question.about('some array', _actor => example)
Expand All @@ -398,8 +400,8 @@ describe('Question', () => {
});

it('allows chaining field calls', async () => {
const example = { a: { b: { c: 'value' }}};
const expected = 'value';
const example = { a: { b: { c: 'value' } } };
const expected = 'value';

const actual = await actor.answer(
Question.about('some array', _actor => example)
Expand All @@ -412,8 +414,8 @@ describe('Question', () => {
});

it('resolves to undefined if any of the links in the chain resolves to `undefined`', async () => {
const example: { a?: { b?: { c?: string }}} = { a: {} };
const expected = undefined;
const example: { a?: { b?: { c?: string } } } = { a: {} };
const expected = undefined;

const actual = await actor.answer(
Question.about('some array', _actor => example)
Expand Down Expand Up @@ -441,8 +443,8 @@ describe('Question', () => {
// "caller" and "argument" are here to show how a proxy resolves conflicts
// between a built-in fields like function.caller and function.arguments
// and properties of the wrapped object of the same name
{ name: 'Alice', caller: 'Bob', arguments: false, location: 'London', description: 'first actor' },
{ name: 'Bob', caller: 'Alice', arguments: true, location: 'New York', description: 'second actor' },
{ name: 'Alice', caller: 'Bob', arguments: false, location: 'London', description: 'first actor' },
{ name: 'Bob', caller: 'Alice', arguments: true, location: 'New York', description: 'second actor' },
]
}));

Expand Down Expand Up @@ -548,6 +550,35 @@ describe('Question', () => {
});
});
});

describe('creates a MetaQuestionAdapter, which', () => {

describe('when used as a Question', () => {

const questionMetaAdapter = Question.about(
'introduction',
(actor: Actor) => `I'm ${ actor.name }`,
(city: City) =>
Question.about(
`introduction from ${ city.name }`,
(actor: Actor) => `I'm ${ actor.name } from ${ city.name }`
)
);

it(`can be answered in the context of another question`, async () => {
const result: string = await actor.answer(
questionMetaAdapter.of({ name: 'London' })
);

expect(result).to.equal(`I'm Stella from London`);
});

it('has a human-readable description', () => {
expect(questionMetaAdapter.of({ name: 'London', toString: () => 'London' }).toString())
.to.equal(`'introduction' of London`)
});
});
});
});
});

Expand All @@ -560,3 +591,8 @@ class Counter {
return this;
}
}

interface City {
name: string;
toString(): string;
}

0 comments on commit b6676fd

Please sign in to comment.