@serenity-js/core
Version:
The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure
231 lines • 10.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.Expectation = void 0;
const io_1 = require("../../io");
const __1 = require("../");
const index_1 = require("../index");
const Question_1 = require("../Question");
const questions_1 = require("../questions");
/**
* Defines an expectation to be used with [`Wait.until`](https://serenity-js.org/api/core/class/Wait/#until),
* [`Check.whether`](https://serenity-js.org/api/core/class/Check/#whether),
* [`Ensure.that`](https://serenity-js.org/api/assertions/class/Ensure/#that)
* and as part of the Page Element Query Language with [`PageElements.where`](https://serenity-js.org/api/web/class/PageElements/#where)
* and [`List.where`](https://serenity-js.org/api/core/class/List/#where).
*
* @group Expectations
*/
class Expectation extends questions_1.Describable {
functionName;
predicate;
/**
* A factory method to that makes defining custom [expectations](https://serenity-js.org/api/core/class/Expectation/) easier
*
* #### Defining a custom expectation
*
* ```ts
* import { Expectation } from '@serenity-js/core'
* import { PageElement } from '@serenity-js/web'
*
* const isEmpty = Expectation.define(
* 'isEmpty', // name of the expectation function to be used when producing an AssertionError
* 'become empty', // human-readable description of the relationship between expected and actual values
* async (actual: PageElement) => {
* const value = await actual.value();
* return value.length === 0;
* }
* )
* ```
*
* #### Using a custom expectation in an assertion
*
* ```ts
* import { Ensure } from '@serenity-js/assertions'
* import { actorCalled } from '@serenity-js/core'
* import { By, Clear, PageElement } from '@serenity-js/web'
*
* const nameField = () =>
* PageElement.located(By.css('[data-test-id="name"]')).describedAs('name field');
*
* await actorCalled('Izzy').attemptsTo(
* Clear.the(nameField()),
* Ensure.that(nameField(), isEmpty())
* )
* ```
*
* #### Using a custom expectation in a control flow statement
*
* ```ts
* import { not } from '@serenity-js/assertions'
* import { actorCalled, Check, Duration, Wait } from '@serenity-js/core'
* import { By, PageElement } from '@serenity-js/web'
*
* const nameField = () =>
* PageElement.located(By.css('[data-test-id="name"]')).describedAs('name field');
*
* await actorCalled('Izzy').attemptsTo(
* Check.whether(nameField(), isEmpty())
* .andIfSo(
* Enter.theValue(actorInTheSpotlight().name).into(nameField()),
* ),
* )
* ```
*
* #### Using a custom expectation in a synchronisation statement
*
* ```ts
* import { not } from '@serenity-js/assertions'
* import { actorCalled, Duration, Wait } from '@serenity-js/core'
* import { By, PageElement } from '@serenity-js/web'
*
* const nameField = () =>
* PageElement.located(By.css('[data-test-id="name"]')).describedAs('name field');
*
* await actorCalled('Izzy').attemptsTo(
* Enter.theValue(actorInTheSpotlight().name).into(nameField()),
*
* Wait.upTo(Duration.ofSeconds(2))
* .until(nameField(), not(isEmpty())),
* )
* ```
*
* #### Learn more
* - [`Ensure`](https://serenity-js.org/api/assertions/class/Ensure/)
* - [`Check`](https://serenity-js.org/api/core/class/Check/)
* - [`Wait`](https://serenity-js.org/api/core/class/Wait/)
*
* @param functionName
* Name of the expectation function to be used when producing an [`AssertionError`](https://serenity-js.org/api/core/class/AssertionError/)
*
* @param relationship
* Human-readable description of the relationship between the `expected` and the `actual` values.
* Used when reporting [activities](https://serenity-js.org/api/core/class/Activity/) performed by an [actor](https://serenity-js.org/api/core/class/Actor/)
*
* @param predicate
*/
static define(functionName, relationship, predicate) {
return Object.defineProperty(function (...answerableArguments) {
const description = typeof relationship === 'function' ? relationship(...answerableArguments)
: (answerableArguments.length === 1 ? (0, index_1.the) `${{ toString: () => relationship }} ${answerableArguments[0]}`
: relationship);
return new Expectation(functionName, description, async (actor, actualValue) => {
const predicateArguments = await (0, io_1.asyncMap)(answerableArguments, answerableArgument => actor.answer(answerableArgument));
const actual = await actor.answer(actualValue);
const result = await predicate(actual, ...predicateArguments);
const descriptionText = await actor.answer(description);
const expectationDetails = __1.ExpectationDetails.of(functionName, ...predicateArguments);
const expected = predicateArguments.length > 0
? predicateArguments[0]
: true; // the only parameter-less expectations are boolean ones like `isPresent`, `isActive`, etc.
return result
? new __1.ExpectationMet(descriptionText, expectationDetails, expected, actual)
: new __1.ExpectationNotMet(descriptionText, expectationDetails, expected, actual);
});
}, 'name', { value: functionName, writable: false });
}
/**
* Used to define a simple [`Expectation`](https://serenity-js.org/api/core/class/Expectation/)
*
* #### Simple parameterised expectation
*
* ```ts
* import { actorCalled, Expectation } from '@serenity-js/core'
* import { Ensure } from '@serenity-js/assertions'
*
* function isDivisibleBy(expected: Answerable<number>): Expectation<number> {
* return Expectation.thatActualShould<number, number>('have value divisible by', expected)
* .soThat((actualValue, expectedValue) => actualValue % expectedValue === 0);
* }
*
* await actorCalled('Erica').attemptsTo(
* Ensure.that(4, isDivisibleBy(2)),
* )
* ```
*
* @param relationshipName
* Name of the relationship between the `actual` and the `expected`. Use format `have value <adjective>`
* so that the description works in both positive and negative contexts, e.g. `Waited until 5 does have value greater than 2`,
* `Expected 5 to not have value greater than 2`.
*
* @param expectedValue
*/
static thatActualShould(relationshipName, expectedValue) {
return ({
soThat: (simplifiedPredicate) => {
const message = relationshipName + ' ' + (0, io_1.d) `${expectedValue}`;
return new Expectation('unknown', message, async (actor, actualValue) => {
const expected = await actor.answer(expectedValue);
const actual = await actor.answer(actualValue);
const result = await simplifiedPredicate(actual, expected);
const expectationDetails = __1.ExpectationDetails.of('unknown');
return result
? new __1.ExpectationMet(message, expectationDetails, expected, actual)
: new __1.ExpectationNotMet(message, expectationDetails, expected, actual);
});
},
});
}
/**
* Used to compose [expectations](https://serenity-js.org/api/core/class/Expectation/).
*
* #### Composing [expectations](https://serenity-js.org/api/core/class/Expectation/)
*
* ```ts
* import { actorCalled, Expectation } from '@serenity-js/core'
* import { Ensure, and, or, isGreaterThan, isLessThan, equals } from '@serenity-js/assertions'
*
* function isWithin(lowerBound: number, upperBound: number) {
* return Expectation
* .to(`have value within ${ lowerBound } and ${ upperBound }`)
* .soThatActual(
* and(
* or(isGreaterThan(lowerBound), equals(lowerBound)),
* or(isLessThan(upperBound), equals(upperBound)),
* )
* )
* }
*
* await actorCalled('Erica').attemptsTo(
* Ensure.that(5, isWithin(3, 6)),
* )
* ```
*
* @param relationshipName
* Name of the relationship between the `actual` and the `expected`. Use format `have value <adjective>`
* so that the description works in both positive and negative contexts, e.g. `Waited until 5 does have value greater than 2`,
* `Expected 5 to not have value greater than 2`.
*/
static to(relationshipName) {
return {
soThatActual: (expectation) => {
return new Expectation('unknown', relationshipName, async (actor, actualValue) => {
return await actor.answer(expectation.isMetFor(actualValue));
});
},
};
}
constructor(functionName, description, predicate) {
super(description);
this.functionName = functionName;
this.predicate = predicate;
}
/**
* Returns a [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) that resolves to [`ExpectationOutcome`](https://serenity-js.org/api/core/class/ExpectationOutcome/)
* indicating that the [expectation was met](https://serenity-js.org/api/core/class/ExpectationMet/)
* or that the [expectation was not met](https://serenity-js.org/api/core/class/ExpectationNotMet/)
*
* @param actual
*/
isMetFor(actual) {
return Question_1.Question.about(this.getDescription(), actor => this.predicate(actor, actual));
}
/**
* @inheritDoc
*/
describedAs(description) {
super.setDescription(description);
return this;
}
}
exports.Expectation = Expectation;
//# sourceMappingURL=Expectation.js.map
;