Skip to content

Commit

Permalink
feat(core): interactions to Wait.for and Wait.until are now browser-i…
Browse files Browse the repository at this point in the history
…ndependent

`Wait` has also been moved to `@serenity-js/core`, see Serenity/JS 3 migration guide for details and
examples.

closes #1035, re #1236
  • Loading branch information
jan-molak committed Jun 23, 2022
1 parent 1c039d2 commit d115142
Show file tree
Hide file tree
Showing 31 changed files with 600 additions and 401 deletions.
61 changes: 51 additions & 10 deletions documentation/website/src/handbook/release-notes/serenity-js-3.md
Expand Up @@ -360,6 +360,45 @@ actorCalled('Alice')

The untyped flavour gives you access to `QuestionAdapter`s just like the typed version, however your text editor might not be able to provide you with as much support as it would if your notepad had been typed.

## Waiting

In Serenity/JS 2, interactions to `Wait.for` and `Wait.until` relied on browser-specific wait APIs, such as Protractor [`wait`](https://www.protractortest.org/#/api?view=webdriver.WebDriver.prototype.wait) or WebdriverIO [`waitUntil`](https://webdriver.io/docs/api/browser/waitUntil/).
Since the interactions were specific to browser integration tools, they'd also come as part of `@serenity-js/protractor` or `@serenity-js/webdriverio` modules.

In Serenity/JS 3, interactions to `Wait` **don't rely on any browser integration tool** and are, in fact, completely browser-independent.
What this means in practice is that you can use `Wait` for both browser and API tests.

Since `Wait` is no longer tied to the browser, it's also been moved to `@serenity-js/core`:

```diff
import { actorCalled, Duration } from '@serenity-js/core';
- import { Wait } from '@serenity-js/protractor';
- import { Wait } from '@serenity-js/webdriverio';
+ import { Wait } from '@serenity-js/core';

actorCalled('Alice').attemptsTo(
Wait.for(Duration.ofSeconds(1)),

Wait.until(someQuestion, someExpectation)
.pollingEvery(Duration.ofMilliseconds(10)),

Wait.upTo(Duration.ofSeconds(5)
.until(someQuestion, someExpectation)
.pollingEvery(Duration.ofMilliseconds(10)),
)
```

Additionally, `Wait.until` has also received a new API allowing you to configure its polling interval (500ms by default):

```typescript
import { actorCalled, Duration, Wait } from '@serenity-js/core';

actorCalled('Alice').attemptsTo(
Wait.until(someQuestion, someExpectation)
.pollingEvery(Duration.ofMilliseconds(10)),
)
```

## `@serenity-js/assertions`

### `property` removed
Expand Down Expand Up @@ -400,9 +439,9 @@ Ensure.that(User().toUpperCase().slice(1, 4), equals('JAN'))

## `@serenity-js/rest`

### `DynamicRecord<AxiosRequestConfig>` in HTTP requests
### `Answerable<WithAnswerableProperties<AxiosRequestConfig>>` in HTTP requests

All HTTP requests now accept dynamic `DynamicRecord<AxiosRequestConfig>`, which means you can now specify additional
All HTTP requests now accept `Answerable<WithAnswerableProperties<AxiosRequestConfig>>`, which means you can now specify additional
HTTP request configuration using a configuration object with nested `Question`s, `QuestionAdapter`s and `Promise`s.

For example:
Expand Down Expand Up @@ -445,9 +484,10 @@ actorCalled('René').attemptsTo(

## `@serenity-js/core`

### Screenplay-style `Dictionary<T>`
### Screenplay-style dictionaries with `Question.fromObject`

A new Screenplay-style data structure, `Dictionary<T>` will help you convert and merge plain JavaScript objects with nested [`Answerable`s](/modules/core/typedef/index.html#static-typedef-Answerable%3CT%3E) into a `QuestionAdapter<T>`.
A new Screenplay-style data structure, `Answerable<WithAnswerableProperties<Source_Type>>` will help you convert
and merge plain JavaScript objects with nested [`Answerable`s](/modules/core/typedef/index.html#static-typedef-Answerable%3CT%3E) into a `QuestionAdapter<T>`.

For example:

Expand All @@ -465,7 +505,7 @@ actorCalled('René').attemptsTo(
Send.a(
PostRequest.to('/products')
.with(
Dictionary.of<AddProductRequestData>({
Question.fromObject<AddProductRequestData>({
name: Text.of(someElement),
quantity: Text.of(someOtherElement).as(Number)
})
Expand All @@ -474,10 +514,10 @@ actorCalled('René').attemptsTo(
);
```

To merge several objects, pass them to `Dictionary.of` as per the example below:
To merge several objects, pass them to `Question.fromObject` as per the example below:

```typescript
Dictionary.of<AddProductRequestData>(
Question.fromObject<AddProductRequestData>(
// initial values
{ name: 'unknown', quantity: 0 },
// overrides
Expand All @@ -488,8 +528,10 @@ Dictionary.of<AddProductRequestData>(
```

Note that in the above code sample, the first object contains values for all the fields
required by AddProductRequestData interface. If not all the fields are required, make sure
to mark them as [optional](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties).
required by `AddProductRequestData` interface.

If not all the fields are required, make sure to mark them
as [optional](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties).

For example:

Expand All @@ -502,7 +544,6 @@ interface AddProductRequestData {

## More coming soon!


<div class="pro-tip">
<div class="icon"><i class="fab fa-github"></i></div>
<div class="text">
Expand Down
18 changes: 7 additions & 11 deletions integration/playwright-web/.mocharc.yml
Expand Up @@ -25,23 +25,19 @@ spec:
- './node_modules/@integration/web-specs/spec/screenplay/interactions/TakeScreenshot.spec.ts'
- './node_modules/@integration/web-specs/spec/screenplay/models/PageElement.located.*.spec.ts'
- './node_modules/@integration/web-specs/spec/screenplay/questions/*.spec.ts'
# WIP
- './node_modules/@integration/web-specs/spec/screenplay/interactions/Wait.spec.ts'
- './node_modules/@integration/web-specs/spec/expectations/isEnabled.spec.ts'
- './node_modules/@integration/web-specs/spec/expectations/isPresent.spec.ts'

# TODO
# - './node_modules/@integration/web-specs/spec/expectations/isActive.spec.ts'
# - './node_modules/@integration/web-specs/spec/expectations/isClickable.spec.ts'
# - './node_modules/@integration/web-specs/spec/expectations/isEnabled.spec.ts' # todo Wait.until
# - './node_modules/@integration/web-specs/spec/expectations/isPresent.spec.ts'
# - './node_modules/@integration/web-specs/spec/expectations/isSelected.spec.ts'
# - './node_modules/@integration/web-specs/spec/expectations/isVisible.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/Page.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/PageElements.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/interactions/Select.spec.ts' # todo PageElement.selectOptionByValue() PageElement.selectOptionByLabel()
# - './node_modules/@integration/web-specs/spec/screenplay/interactions/Wait.spec.ts'

# - './node_modules/@integration/web-specs/spec/stage/*.spec.ts'

# todo
# - './node_modules/@integration/web-specs/spec/screenplay/interactions/Select.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/Cookie.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/ModalDialog.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/Page.spec.ts'
# - './node_modules/@integration/web-specs/spec/screenplay/models/PageElement.spec.ts'
# - './node_modules/@integration/web-specs/spec/**/Page.spec.ts'
# - './node_modules/@integration/web-specs/spec/stage/**/*.spec.ts'
6 changes: 3 additions & 3 deletions integration/web-specs/spec/expectations/isActive.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, not } from '@serenity-js/assertions';
import { actorCalled, AssertionError } from '@serenity-js/core';
import { By, Click, isActive, Navigate, PageElement, Wait } from '@serenity-js/web';
import { actorCalled, AssertionError, Wait } from '@serenity-js/core';
import { By, Click, isActive, Navigate, PageElement } from '@serenity-js/web';

describe('isActive', function () {

Expand Down Expand Up @@ -42,6 +42,6 @@ describe('isActive', function () {
/** @test {isActive} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Page.activeInput, isActive()).toString())
.to.equal(`#actor waits up to 5s until the active input does become active`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until the active input does become active`);
});
});
6 changes: 3 additions & 3 deletions integration/web-specs/spec/expectations/isClickable.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure } from '@serenity-js/assertions';
import { actorCalled, AssertionError } from '@serenity-js/core';
import { By, isClickable, Navigate, PageElement, Wait } from '@serenity-js/web';
import { actorCalled, AssertionError, Wait } from '@serenity-js/core';
import { By, isClickable, Navigate, PageElement } from '@serenity-js/web';

describe('isClickable', function () {

Expand Down Expand Up @@ -52,6 +52,6 @@ describe('isClickable', function () {
/** @test {isClickable} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Elements.enabledButton, isClickable()).toString())
.to.equal(`#actor waits up to 5s until the enabled button does become clickable`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until the enabled button does become clickable`);
});
});
6 changes: 3 additions & 3 deletions integration/web-specs/spec/expectations/isEnabled.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure } from '@serenity-js/assertions';
import { actorCalled, AssertionError } from '@serenity-js/core';
import { By, isEnabled, Navigate, PageElement, Wait } from '@serenity-js/web';
import { actorCalled, AssertionError, Wait } from '@serenity-js/core';
import { By, isEnabled, Navigate, PageElement } from '@serenity-js/web';

describe('isEnabled', function () {

Expand Down Expand Up @@ -40,6 +40,6 @@ describe('isEnabled', function () {
/** @test {isEnabled} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Page.enabledButton, isEnabled()).toString())
.to.equal(`#actor waits up to 5s until the enabled button does become enabled`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until the enabled button does become enabled`);
});
});
8 changes: 4 additions & 4 deletions integration/web-specs/spec/expectations/isPresent.spec.ts
Expand Up @@ -2,9 +2,9 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, isPresent } from '@serenity-js/assertions';
import { actorCalled, AssertionError, Duration } from '@serenity-js/core';
import { actorCalled, AssertionError, Duration, Wait } from '@serenity-js/core';
import { ErrorSerialiser } from '@serenity-js/core/lib/io';
import { By, Navigate, PageElement, Wait } from '@serenity-js/web';
import { By, Navigate, PageElement } from '@serenity-js/web';

describe('isPresent', function () {

Expand All @@ -29,7 +29,7 @@ describe('isPresent', function () {
it('breaks the actor flow when element does not become present in the DOM', () =>
expect(actorCalled('Wendy').attemptsTo(
Wait.upTo(Duration.ofMilliseconds(250)).until(Page.nonExistentHeader, isPresent()),
)).to.be.rejectedWith(AssertionError, `Waited 250ms for the non-existent header to become present`));
)).to.be.rejectedWith(AssertionError, `Waited 250ms, polling every 500ms, for the non-existent header to become present`));

/** @test {isPresent} */
it('breaks the actor flow when element is not present in the DOM', () =>
Expand All @@ -55,6 +55,6 @@ describe('isPresent', function () {
/** @test {isPresent} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Page.presentHeader, isPresent()).toString())
.to.equal(`#actor waits up to 5s until the header does become present`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until the header does become present`);
});
});
6 changes: 3 additions & 3 deletions integration/web-specs/spec/expectations/isSelected.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure } from '@serenity-js/assertions';
import { actorCalled, AssertionError } from '@serenity-js/core';
import { By, isSelected, Navigate, PageElement, Wait } from '@serenity-js/web';
import { actorCalled, AssertionError, Wait } from '@serenity-js/core';
import { By, isSelected, Navigate, PageElement } from '@serenity-js/web';

describe('isSelected', function () {

Expand Down Expand Up @@ -46,6 +46,6 @@ describe('isSelected', function () {
/** @test {isSelected} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Languages.typeScript, isSelected()).toString())
.to.equal(`#actor waits up to 5s until the TypeScript option does become selected`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until the TypeScript option does become selected`);
});
});
6 changes: 3 additions & 3 deletions integration/web-specs/spec/expectations/isVisible.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, equals, not } from '@serenity-js/assertions';
import { actorCalled } from '@serenity-js/core';
import { Attribute, By, Hover, isVisible, Navigate, PageElement, Text, Wait } from '@serenity-js/web';
import { actorCalled, Wait } from '@serenity-js/core';
import { Attribute, By, Hover, isVisible, Navigate, PageElement, Text } from '@serenity-js/web';

describe('isVisible', function () {

Expand Down Expand Up @@ -90,7 +90,7 @@ describe('isVisible', function () {
/** @test {isVisible} */
it('contributes to a human-readable description of a wait', () => {
expect(Wait.until(Elements.displayed, isVisible()).toString())
.to.equal(`#actor waits up to 5s until visible element does become visible`);
.to.equal(`#actor waits up to 5s, polling every 500ms, until visible element does become visible`);
});
});

Expand Down
13 changes: 5 additions & 8 deletions integration/web-specs/spec/screenplay/interactions/Wait.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, equals } from '@serenity-js/assertions';
import { actorCalled, AssertionError, Duration } from '@serenity-js/core';
import { By, Click, Navigate, PageElement, Text, Wait } from '@serenity-js/web';
import { actorCalled, AssertionError, Duration, Wait } from '@serenity-js/core';
import { By, Click, Navigate, PageElement, Text } from '@serenity-js/web';

/** @test {Wait} */
describe('Wait', () => {
Expand Down Expand Up @@ -57,22 +57,19 @@ describe('Wait', () => {
Ensure.that(Text.of(status), equals('Not ready')),
Click.on(loadButton),

Wait.upTo(Duration.ofMilliseconds(10)).until(Text.of(status), equals('Ready!')),
Wait.upTo(Duration.ofMilliseconds(100)).until(Text.of(status), equals('Ready!')).pollingEvery(Duration.ofMilliseconds(50)),

)).to.be.rejected.then((error: AssertionError) => {
expect(error).to.be.instanceOf(AssertionError);
expect(error.message).to.be.equal(`Waited 10ms for the text of the header to equal 'Ready!'`);
expect(error.message).to.be.equal(`Waited 100ms, polling every 50ms, for the text of the header to equal 'Ready!'`);
expect(error.actual).to.be.equal('Loading...');
expect(error.expected).to.be.equal('Ready!');

expect(error.cause).to.be.instanceOf(Error);
expect(error.cause.message).to.match(/^Wait timed out after.*/);
}));

/** @test {Wait#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(Wait.upTo(Duration.ofMilliseconds(10)).until(Text.of(status), equals('Ready!')).toString())
.to.equal(`#actor waits up to 10ms until the text of the header does equal 'Ready!'`);
.to.equal(`#actor waits up to 10ms, polling every 500ms, until the text of the header does equal 'Ready!'`);
});
});
});
Expand Up @@ -2,10 +2,10 @@ import 'mocha';

import { EventRecorder, expect, PickEvent } from '@integration/testing-tools';
import { Ensure, equals, isPresent, not } from '@serenity-js/assertions';
import { actorCalled, Clock, Duration, Serenity,serenity } from '@serenity-js/core';
import { actorCalled, Clock, Duration, Serenity, serenity, Wait } from '@serenity-js/core';
import { AsyncOperationCompleted, InteractionFinished } from '@serenity-js/core/lib/events';
import { Name } from '@serenity-js/core/lib/model';
import { By, Click, ModalDialog, Navigate, PageElement, Photographer, TakePhotosOfInteractions, Text, Wait } from '@serenity-js/web';
import { By, Click, ModalDialog, Navigate, PageElement, Photographer, TakePhotosOfInteractions, Text } from '@serenity-js/web';

describe('ModalDialog', () => {

Expand Down
4 changes: 2 additions & 2 deletions integration/web-specs/spec/screenplay/models/Page.spec.ts
Expand Up @@ -2,8 +2,8 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { endsWith, Ensure, equals, includes, isPresent, not, startsWith } from '@serenity-js/assertions';
import { actorCalled, LogicError, notes } from '@serenity-js/core';
import { By, Click, Navigate, Page, PageElement, Switch, Text, Wait } from '@serenity-js/web';
import { actorCalled, LogicError, notes, Wait } from '@serenity-js/core';
import { By, Click, Navigate, Page, PageElement, Switch, Text } from '@serenity-js/web';
import { URL } from 'url';

/** @test {Page} */
Expand Down
Expand Up @@ -2,11 +2,11 @@ import 'mocha';

import { EventRecorder, expect, PickEvent } from '@integration/testing-tools';
import { isPresent } from '@serenity-js/assertions';
import { Duration } from '@serenity-js/core';
import { Duration, Wait } from '@serenity-js/core';
import { ActivityRelatedArtifactGenerated, ActivityStarts } from '@serenity-js/core/lib/events';
import { CorrelationId, Photo } from '@serenity-js/core/lib/model';
import { Stage } from '@serenity-js/core/lib/stage';
import { BrowseTheWeb, By, PageElement, Photographer, TakePhotosOfFailures, Wait } from '@serenity-js/web';
import { BrowseTheWeb, By, PageElement, Photographer, TakePhotosOfFailures } from '@serenity-js/web';

import { create } from '../create';
import { Perform } from '../fixtures';
Expand Down
2 changes: 1 addition & 1 deletion packages/assertions/src/expectations/isPresent.ts
Expand Up @@ -10,7 +10,7 @@ import { Answerable, AnswersQuestions, Expectation, ExpectationMet, ExpectationN
*
* @see {@link @serenity-js/assertions~Ensure}
* @see {@link @serenity-js/core/lib/screenplay/questions~Check}
* @see {@link Wait}
* @see {@link @serenity-js/core/lib/screenplay/interactions~Wait}
*/
export function isPresent<Actual>(): Expectation<Actual> {
return new IsPresent<Actual>();
Expand Down
23 changes: 22 additions & 1 deletion packages/core/spec/screenplay/Ensure.ts
@@ -1,6 +1,7 @@
import { equal } from 'tiny-types/lib/objects/equal';

import { AssertionError } from '../../src/errors';
import { d } from '../../src/io';
import { Answerable, Interaction } from '../../src/screenplay';

export const Ensure = {
Expand All @@ -22,5 +23,25 @@ export const Ensure = {
if (! equal(actualValue, expectedValue)) {
throw new AssertionError(`Expected ${ actualValue } to equal ${ expectedValue }`, actualValue, expectedValue);
}
})
}),

greaterThanOrEqual: <T extends number>(actual: Answerable<T>, expected: Answerable<T>): Interaction =>
Interaction.where(d`#actor ensures that ${actual} >= ${ expected }`, async actor => {
const actualValue = await actor.answer(actual);
const expectedValue = await actor.answer(expected);

if (! (actualValue >= expectedValue)) {
throw new AssertionError(`Expected ${ actualValue } to be greater than or equal to ${ expectedValue }`, actualValue, expectedValue);
}
}),

lessThan: <T extends number>(actual: Answerable<T>, expected: Answerable<T>): Interaction =>
Interaction.where(d`#actor ensures that ${actual} < ${ expected }`, async actor => {
const actualValue = await actor.answer(actual);
const expectedValue = await actor.answer(expected);

if (! (actualValue < expectedValue)) {
throw new AssertionError(`Expected ${ actualValue } to be less than ${ expectedValue }`, actualValue, expectedValue);
}
}),
}

0 comments on commit d115142

Please sign in to comment.