Permalink
Browse files

feat(interactions): Screenplay-style explicit and fluent Wait

`Wait.for(d: Duration)` allows to make the test wait for an arbitrary period of time (explicit

wait); `Wait.until(t: Target, c: Condition)` and `Wait.upTo(d: Duration).until(t: Target, c:

Condition)` allow to make the test wait until a condition is met. For example:

`Wait.upTo(Duration.ofSeconds(0.5)).until(Notification.Message, Is.visible())`

Partially addresses #7
  • Loading branch information...
jan-molak committed Dec 27, 2016
1 parent c34f60f commit efb1f0cbec837b1d761de0309f37b919e23e5ea3
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<script src="/js/angular.min.js"></script>
<title>demo app</title>
</head>
<body ng-app="demo">
<section ng-controller="timeouts" id="timeouts" ng-cloak>
<div id="angular">
<button ng-click="ngTimeout()">Trigger Angular "$timeout"</button>
<pre ng-show="ngTimeoutWorks" class="ng-hide">Angular timeout works!</pre>
</div>
<div id="setTimeout">
<button ng-click="jsTimeout()">Trigger JavaScript "setTimeout"</button>
<pre ng-show="jsTimeoutWorks" class="ng-hide">JavaScript timeout works!</pre>
</div>
</section>
<script>
angular.module('demo', []).controller('timeouts', ['$scope', '$timeout', function ($scope, $timeout) {
$scope.ngTimeoutWorks = false;
$scope.ngTimeout = () => {
$timeout(() => {
$scope.ngTimeoutWorks = true;
}, 500);
};
$scope.jsTimeoutWorks = false;
$scope.jsTimeout = () => {
setTimeout(function () {
$scope.jsTimeoutWorks = true;
$scope.$apply();
}, 500);
};
}]);
</script>
</body>
</html>
@@ -0,0 +1,124 @@
import synced = require('selenium-webdriver/testing');
import expect = require('../expect');
import { by, protractor } from 'protractor';
import { Actor, BrowseTheWeb, Click, Duration, Is, Open, Target, Wait, WebElement } from '../../src/screenplay-protractor';
import { AppServer } from '../support/server';
export class AngularTimeout {
static Button = Target.the('NG $timeout trigger').located(by.css('#timeouts #angular button'));
static Result = Target.the('NG $timeout result').located(by.css('#timeouts #angular pre'));
}
export class JavaScriptTimeout {
static Button = Target.the('JS timeout trigger').located(by.css('#timeouts #setTimeout button'));
static Result = Target.the('JS timeout result').located(by.css('#timeouts #setTimeout pre'));
}
synced.describe ('When waiting for things to happen, a test scenario', function () {
this.timeout(10000);
const Not_Long_Enough = Duration.ofMillis(200),
Long_Enough = Duration.ofMillis(1500);
let app = new AppServer();
let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser));
synced.before(app.start());
synced.after(app.stop());
synced.beforeEach(() =>
james.attemptsTo(
Open.browserOn(app.demonstrating('waiting')),
).then(() => Promise.all([
expect(james.toSee(WebElement.of(AngularTimeout.Result))).not.displayed,
expect(james.toSee(WebElement.of(JavaScriptTimeout.Result))).not.displayed,
])));
synced.describe('using Passive Wait', () => {
synced.it ('will fail if the timeout is too short', () =>
james.attemptsTo(
Click.on(AngularTimeout.Button),
Wait.for(Not_Long_Enough),
).then(() => expect(james.toSee(WebElement.of(AngularTimeout.Result))).not.displayed));
synced.it ('will pass if the timeout is long enough', () =>
james.attemptsTo(
Click.on(AngularTimeout.Button),
Wait.for(Long_Enough),
).then(() => expect(james.toSee(WebElement.of(AngularTimeout.Result))).displayed));
});
synced.describe('using Active Wait', () => {
synced.describe('with Angular apps', () => {
synced.describe('to determine if an element is visible', () => {
synced.it('will fail if the condition is not met within the timeout (timeout not triggered)', () =>
expect(james.attemptsTo(
Wait.until(AngularTimeout.Result, Is.visible()),
)).to.be.rejectedWith('"the NG $timeout result" did not become visible'));
synced.it('will fail if the condition is not met within the timeout (timeout triggered)', () =>
expect(james.attemptsTo(
Click.on(AngularTimeout.Button),
Wait.upTo(Not_Long_Enough).until(AngularTimeout.Result, Is.visible()),
)).to.be.rejectedWith('"the NG $timeout result" did not become visible'));
synced.it('will pass if the condition is met within the timeout', () =>
expect(james.attemptsTo(
Click.on(AngularTimeout.Button),
Wait.upTo(Long_Enough).until(AngularTimeout.Result, Is.visible()),
)).to.be.fulfilled.then(() => Promise.all([
expect(james.toSee(WebElement.of(AngularTimeout.Result))).displayed,
expect(james.toSee(WebElement.of(AngularTimeout.Result))).present,
])));
});
synced.describe('to determine if an element is invisible', () => {
synced.it('will pass if the element is already invisible', () =>
james.attemptsTo(
Wait.until(AngularTimeout.Result, Is.invisible()),
).then(() => expect(james.toSee(WebElement.of(AngularTimeout.Result))).not.displayed));
});
});
synced.describe('with non-Angular apps', () => {
synced.describe('to determine if an element is visible', () => {
synced.it('will fail if the condition is not met within the timeout (timeout not triggered)', () =>
expect(james.attemptsTo(
Wait.until(JavaScriptTimeout.Result, Is.visible()),
)).to.be.rejectedWith('"the JS timeout result" did not become visible'));
synced.it('will fail if the condition is not met within the timeout (timeout triggered)', () =>
expect(james.attemptsTo(
Click.on(JavaScriptTimeout.Result),
Wait.upTo(Not_Long_Enough).until(JavaScriptTimeout.Result, Is.visible()),
)).to.be.rejectedWith('"the JS timeout result" did not become visible'));
synced.it('will pass if the condition is met within the timeout', () =>
expect(james.attemptsTo(
Click.on(JavaScriptTimeout.Button),
Wait.upTo(Long_Enough).until(JavaScriptTimeout.Result, Is.visible()),
)).to.be.fulfilled.then(() => Promise.all([
expect(james.toSee(WebElement.of(JavaScriptTimeout.Result))).displayed,
expect(james.toSee(WebElement.of(JavaScriptTimeout.Result))).present,
])));
});
synced.describe('to determine if an element is invisible', () => {
synced.it('will pass if the element is already invisible', () =>
james.attemptsTo(
Wait.until(JavaScriptTimeout.Result, Is.invisible()),
).then(() => expect(james.toSee(WebElement.of(JavaScriptTimeout.Result))).not.displayed));
});
});
});
});
@@ -57,6 +57,21 @@ export class BrowseTheWeb implements Ability {
return this.browser.driver.manage();
}
sleep(millis: number): PromiseLike<void> {
return defer(() => this.browser.sleep(millis));
}
wait(condition: webdriver.promise.Promise<any> | webdriver.until.Condition<any> | Function,
timeout?: number,
message?: string): PromiseLike<void>
{
return this.browser.wait(
condition,
timeout,
message,
);
}
constructor(private browser: ProtractorBrowser) {
}
}
@@ -5,3 +5,4 @@ export * from './hit';
export * from './open';
export * from './resize_browser_window';
export * from './select';
export * from './wait';
@@ -0,0 +1,103 @@
import { Interaction, Performable, UsesAbilities } from '../../../serenity/screenplay';
import { BrowseTheWeb } from '../abilities/browse_the_web';
import { Target } from '../ui/target';
import { ElementFinder, protractor } from 'protractor';
export class Duration {
static ofMillis = (milliseconds: number) => new Duration(milliseconds);
static ofSeconds = (seconds: number) => Duration.ofMillis(seconds * 1000);
toMillis = () => this.milliseconds;
toString = () => `${ this.milliseconds }ms`;
constructor(private milliseconds: number) {
}
}
export type TimeoutCondition<T> = (s: T, timeout: Duration) => Performable;
export type TargetTimeoutCondition = TimeoutCondition<Target>;
export class Wait {
static for = (duration: Duration): Interaction => new PassiveWait(duration);
static upTo = (timeout: Duration) => new ActiveWait(timeout);
static until<T> (somethingToWaitFor: T, condition: TimeoutCondition<T>) {
return new ActiveWait().until(somethingToWaitFor, condition);
}
}
export class ActiveWait {
until<T> (somethingToWaitFor: T, condition: TimeoutCondition<T>): Performable {
return condition(somethingToWaitFor, this.timeout);
}
constructor(private timeout: Duration = Duration.ofSeconds(1)) {
}
}
export class Is {
static visible(): TargetTimeoutCondition {
return (target: Target, timeout: Duration) => new WaitUntil(target, new Visibility(), timeout);
}
static invisible(): TargetTimeoutCondition {
return (target: Target, timeout: Duration) => new WaitUntil(target, new Invisibility(), timeout);
}
}
// package-protected
class PassiveWait implements Interaction {
performAs = (actor: UsesAbilities) => BrowseTheWeb.as(actor).sleep(this.duration.toMillis());
constructor(private duration: Duration) {
}
}
interface Condition<T> {
check(thing: ElementFinder): Function;
name(): string;
}
class Visibility implements Condition<ElementFinder> {
check = (thing: ElementFinder): Function => protractor.ExpectedConditions.visibilityOf(thing);
name = () => 'visible';
}
class Invisibility implements Condition<ElementFinder> {
check = (thing: ElementFinder): Function => protractor.ExpectedConditions.invisibilityOf(thing);
name = () => 'invisible';
}
class WaitUntil implements Interaction {
performAs(actor: UsesAbilities): PromiseLike<void> {
return BrowseTheWeb.as(actor).wait(
this.condition.check(BrowseTheWeb.as(actor).locate(this.target)),
this.timeout.toMillis(),
`"${ this.target }" did not become ${ this.condition.name() } within ${this.timeout}`,
);
}
constructor(private target: Target, private condition: Condition<ElementFinder>, private timeout: Duration) {
}
}
/*
[x] visibilityOf(elementFinder: ElementFinder): Function {
[x] invisibilityOf(elementFinder: ElementFinder): Function {
[ ] presenceOf(elementFinder: ElementFinder): Function {
[ ] stalenessOf(elementFinder: ElementFinder): Function {
[ ] elementToBeSelected(elementFinder: ElementFinder): Function {
[ ] alertIsPresent(): Function {
[ ] elementToBeClickable(elementFinder: ElementFinder): Function {
[ ] textToBePresentInElement(elementFinder: ElementFinder, text: string): Function {
[ ] textToBePresentInElementValue(elementFinder: ElementFinder, text: string): Function {
[ ] titleContains(title: string): Function {
[ ] titleIs(title: string): Function {
[ ] urlContains(url: string): Function {
[ ] urlIs(url: string): Function {
*/

0 comments on commit efb1f0c

Please sign in to comment.