Skip to content
Permalink
Browse files
feat(protractor): support for handling modal dialog windows
You can now `Accept` or `Dismiss` a `ModalDialog.window()` created using `Window.alert()`,
`Window.confirm()`, or `Window.prompt()`. You can also `Enter` text into `Window.prompt()`,
`Wait.until(ModalDialog.hasPoppedUp(), isTrue())` and `Ensure.that(ModalDialog.message(),
equals('expected value'))`

Closes #374
  • Loading branch information
jan-molak committed Jul 25, 2020
1 parent dc0090f commit 2dfb44c5e3761be47e11528c3485fedf2600924f
Show file tree
Hide file tree
Showing 13 changed files with 660 additions and 21 deletions.
Empty file.
@@ -2,7 +2,7 @@ import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure } from '@serenity-js/assertions';
import { actorCalled, AssertionError } from '@serenity-js/core';
import { actorCalled, AssertionError, Duration } from '@serenity-js/core';
import { by } from 'protractor';

import { isPresent, Navigate, Target, Wait } from '../../src';
@@ -31,6 +31,11 @@ describe('isPresent', function () {
Ensure.that(Page.Present_Header, isPresent()),
)).to.be.fulfilled);

/** @test {isPresent} */
it('breaks the actor flow when element does not become present in the DOM', () => expect(actorCalled('Bernie').attemptsTo(
Wait.upTo(Duration.ofMilliseconds(250)).until(Page.Non_Existent_Header, isPresent()),
)).to.be.rejectedWith(AssertionError, `Waited 250ms for the non-existent header to become present`));

/** @test {isPresent} */
it('breaks the actor flow when element is not present in the DOM', () => {
return expect(actorCalled('Bernie').attemptsTo(
@@ -6,37 +6,50 @@ import { by } from 'protractor';
import { Enter, Navigate, Target, Value } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

/** @test {Enter} */
describe('Enter', () => {

const Form = {
Field: Target.the('name field').located(by.id('name')),
Result: Target.the('result').located(by.id('your-name')),
Field: Target.the('name field').located(by.id('field')),
Result: Target.the('result').located(by.id('result')),
};

/** @test {Enter} */
/** @test {Enter.theValue} */
it('allows the actor to enter the value into a field', () => actorCalled('Bernie').attemptsTo(
Navigate.to(pageFromTemplate(`
const page = pageFromTemplate(`
<html>
<body>
<form>
<input type="text" id="name" onkeyup="update()" />
<div id="your-name" />
<input type="text" id="field" onkeyup="update()" />
<div id="result" />
</form>
<script>
function update() {
document.getElementById("your-name").textContent = document.getElementById("name").value;
document.getElementById("result").textContent = document.getElementById("field).value;
}
</script>
</body>
</html>
`)),
`)

/** @test {Enter} */
/** @test {Enter.theValue} */
it('allows the actor to enter the value into an input field', () => actorCalled('Bernie').attemptsTo(
Navigate.to(page),

Enter.theValue(actorCalled('Bernie').name).into(Form.Field),

Ensure.that(Value.of(Form.Field), equals(actorCalled('Bernie').name)),
));

/** @test {Enter} */
/** @test {Enter.theValue} */
it('allows the actor to enter the value into a number field', () => actorCalled('Bernie').attemptsTo(
Navigate.to(page),

Enter.theValue(123).into(Form.Field),

Ensure.that(Value.of(Form.Field), equals('123')),
));

/** @test {Enter#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(Enter.theValue(actorCalled('Bernie').name).into(Form.Field).toString())
@@ -0,0 +1,246 @@
import 'mocha';

import { EventRecorder, expect, PickEvent } from '@integration/testing-tools';
import { Ensure, equals, isFalse, isTrue } from '@serenity-js/assertions';
import { actorCalled, configure } from '@serenity-js/core';
import { AsyncOperationCompleted, AsyncOperationFailed, InteractionFinished } from '@serenity-js/core/lib/events';
import { Name } from '@serenity-js/core/lib/model';
import { by } from 'protractor';
import { Accept, Click, Dismiss, Enter, ModalDialog, Navigate, Photographer, TakePhotosOfInteractions, Target, Text, Wait } from '../../../src';
import { pageFromTemplate } from '../../fixtures';
import { UIActors } from '../../UIActors';

/** @test {ModalDialog} */
describe('ModalDialog', function () {

const Example = {
trigger: Target.the('alert trigger').located(by.id('trigger')),
result: Target.the('result').located(by.id('result')),
}

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

beforeEach(() =>
actorCalled('Nick').attemptsTo(
Navigate.to(sandboxWith(`
function() {
alert('Hello!');
// alert is blocking
return 'accepted';
}
`)),
Click.on(Example.trigger),
));

/** @test {ModalDialog.window} */
/** @test {Accept} */
/** @test {Accept.the} */
it('allows the actor to accept an alert', () =>
actorCalled('Nick').attemptsTo(
Accept.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('accepted')),
),
);

/** @test {ModalDialog.window} */
/** @test {Dismiss} */
/** @test {Dismiss.the} */
it('allows the actor to dismiss an alert', () =>
actorCalled('Nick').attemptsTo(
Dismiss.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('accepted')),
),
);

/** @test {ModalDialog.message} */
/** @test {Dismiss} */
/** @test {Dismiss.the} */
it('allows the actor to read the message on an alert', () =>
actorCalled('Nick').attemptsTo(
Ensure.that(ModalDialog.message(), equals('Hello!')),
Dismiss.the(ModalDialog.window()),
),
);
});

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

beforeEach(() =>
actorCalled('Nick').attemptsTo(
Navigate.to(sandboxWith(`
function() {
return confirm('Continue?')
? 'accepted'
: 'dismissed';
}
`)),
Click.on(Example.trigger),
));

/** @test {ModalDialog.window} */
/** @test {Accept} */
/** @test {Accept.the} */
it('allows the actor to accept a confirmation dialog', () =>
actorCalled('Nick').attemptsTo(
Accept.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('accepted')),
),
);

/** @test {ModalDialog.window} */
/** @test {Dismiss} */
/** @test {Dismiss.the} */
it('allows the actor to dismiss a confirmation dialog', () =>
actorCalled('Nick').attemptsTo(
Dismiss.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('dismissed')),
),
);

/** @test {ModalDialog.message} */
it('allows the actor to read the message on a confirmation dialog', () =>
actorCalled('Nick').attemptsTo(
Ensure.that(ModalDialog.message(), equals('Continue?')),
Dismiss.the(ModalDialog.window()),
),
);
});

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

beforeEach(() =>
actorCalled('Nick').attemptsTo(
Navigate.to(sandboxWith(`
function() {
return prompt('Feeling lucky?', 'sure');
}
`)),
Click.on(Example.trigger),
));

/** @test {ModalDialog.window} */
/** @test {Accept} */
/** @test {Accept.the} */
it('allows the actor to accept a prompt', () =>
actorCalled('Nick').attemptsTo(
Accept.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('sure')),
),
);

/** @test {ModalDialog.window} */
/** @test {Dismiss} */
/** @test {Dismiss.the} */
it('allows the actor to dismiss a prompt', () =>
actorCalled('Nick').attemptsTo(
Dismiss.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('')),
),
);

/** @test {ModalDialog.message} */
it('allows the actor to read the message on a prompt', () =>
actorCalled('Nick').attemptsTo(
Ensure.that(ModalDialog.message(), equals('Feeling lucky?')),
Dismiss.the(ModalDialog.window()),
),
);

/** @test {ModalDialog.message} */
/** @test {Enter.theValue} */
it('allows the actor to enter value into a prompt', () =>
actorCalled('Nick').attemptsTo(
Enter.theValue('certainly').into(ModalDialog.window()),
Accept.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('certainly')),
),
);
});

describe('waiting', () => {

beforeEach(() =>
actorCalled('Nick').attemptsTo(
Navigate.to(sandboxWith(`
function() {
setTimeout(function() {
alert('Almost there!');
document.getElementById("result").innerHTML = 'And the wait is over :-)';
}, 250);
return 'The wait has began';
}
`)),
));

/** @test {ModalDialog.hasPoppedUp} */
/** @test {Wait.until} */
it('allows the actor to wait until a modal dialog is present', () =>
actorCalled('Nick').attemptsTo(
Ensure.that(ModalDialog.hasPoppedUp(), isFalse()),
Click.on(Example.trigger),
Wait.until(ModalDialog.hasPoppedUp(), isTrue()),
Accept.the(ModalDialog.window()),
Ensure.that(Text.of(Example.result), equals('And the wait is over :-)')),
),
);
});

describe('Photograper', () => {

let recorder: EventRecorder;

beforeEach(() => {
recorder = new EventRecorder();

configure({
actors: new UIActors(),
crew: [
Photographer.whoWill(TakePhotosOfInteractions),
recorder,
]
})
});

/** @test {Photographer} */
it('is not negatively impacted by the presence of an alert', () =>
actorCalled('Nick').attemptsTo(
Navigate.to(sandboxWith(`
function() {
return alert('All good?');
}
`)),
Click.on(Example.trigger),
Accept.the(ModalDialog.window()),
).
then(() => {
PickEvent.from(recorder.events)
.next(AsyncOperationCompleted, ({ taskDescription }: AsyncOperationCompleted) => {
expect(taskDescription.value).to.include(`Took screenshot of 'Nick navigates`);
})
.next(AsyncOperationFailed, ({ error }: AsyncOperationFailed) => {
expect(error.name).to.equal('UnexpectedAlertOpenError');
})
.next(InteractionFinished, ({ value }: InteractionFinished) => {
expect(value.name).to.equal(new Name('Nick accepts the modal dialog window'));
})
}),
);

});

function sandboxWith(script: string) {
return pageFromTemplate(`
<html>
<body>
<button id="trigger" onclick="trigger()">Trigger Alert</button>
<p id="result"></p>
<script>
function trigger() {
document.getElementById("result").innerHTML = (${script})();
}
</script>
</body>
</html>
`)
}
});
@@ -0,0 +1,38 @@
import { promise } from 'selenium-webdriver';

/**
* @desc
* Represents a [WebElement](https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html),
* [ElementFinder](https://www.protractortest.org/#/api?view=ElementFinder)
* or a {@link ModalDialog}.
*
* @see {@link Enter}
* @see {@link Text}
*
* @public
*/
export interface HasText {

/**
* @desc
* Get the visible (i.e. not hidden by CSS) innerText of this element,
* including sub-elements, without any leading or trailing whitespace.
*
* @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html#getText
* @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_AlertPromise.html#getText
*
* @type {function(): Promise<String>}
*/
getText: () => promise.Promise<string>;

/**
* @desc
* Types a key sequence on the DOM element represented by this instance.
*
* @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html#sendKeys
* @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_AlertPromise.html#sendKeys
*
* @type {function(text: string): Promise<String>}
*/
sendKeys: (text: string) => promise.Promise<void>;
}
@@ -0,0 +1 @@
export * from './HasText';

0 comments on commit 2dfb44c

Please sign in to comment.