Skip to content
Permalink
Browse files
feat(protractor): LastScriptExecution.result() gives access to the va…
…lue returned by the script pass
  • Loading branch information
jan-molak committed Mar 4, 2019
1 parent 11668f0 commit 75acc79dea5f89c9ce3baf8338855cf9b82890fc
@@ -25,7 +25,8 @@
export abstract class RuntimeError extends Error {

protected constructor(
type: { new(...args: any[]): RuntimeError } , message: string,
type: { new(...args: any[]): RuntimeError },
message: string,
public readonly cause?: Error,
) {
super(message);
@@ -71,15 +71,15 @@ export class Actor implements PerformsTasks, UsesAbilities, AnswersQuestions, Co
return !! (v as any).answeredBy;
}

if (knowableUnknown === undefined || knowableUnknown === null) {
return Promise.reject(new LogicError(`Can't answer a question that's not been defined.`));
function isDefined<V>(v: KnowableUnknown<V>) {
return ! (knowableUnknown === undefined || knowableUnknown === null);
}

if (isAPromise(knowableUnknown)) {
if (isDefined(knowableUnknown) && isAPromise(knowableUnknown)) {
return knowableUnknown;
}

if (isAQuestion(knowableUnknown)) {
if (isDefined(knowableUnknown) && isAQuestion(knowableUnknown)) {
return this.answer(knowableUnknown.answeredBy(this));
}

@@ -12,7 +12,6 @@ import { pageFromTemplate } from '../../../fixtures';

/** @test {ExecuteAsynchronousScript} */
describe('ExecuteAsynchronousScript', function() {
this.timeout(30000);

const Joe = Actor.named('Joe').whoCan(
BrowseTheWeb.using(protractor.browser),
@@ -123,6 +122,18 @@ describe('ExecuteAsynchronousScript', function() {
).to.equal(`#actor executes an asynchronous script with arguments: [ a promised value, 'arg2', arg number 3 ]`);
});

/** @test {ExecuteScript.async} */
/** @test {ExecuteAsynchronousScript} */
it('complains if the script has failed', () => expect(Joe.attemptsTo(
Navigate.to(page),

ExecuteScript.async(`
var callback = arguments[arguments.length - 1];
throw new Error("something's not quite right here");
`),
)).to.be.rejectedWith(Error, `something's not quite right here`));

/** @test {ExecuteScript.async} */
/** @test {ExecuteAsynchronousScript} */
it('emits the events so that the details of the script being executed can be reported', () => {
@@ -7,7 +7,6 @@ import { BrowseTheWeb, ExecuteScript, Navigate, Target, Text } from '../../../..

/** @test {ExecuteScriptFromUrl} */
describe('ExecuteScriptFromUrl', function() {
this.timeout(30000);

const Joe = Actor.named('Joe').whoCan(
BrowseTheWeb.using(protractor.browser),
@@ -5,14 +5,13 @@ import { ActivityFinished, ActivityStarts, ArtifactGenerated } from '@serenity-j
import { Name, TextData } from '@serenity-js/core/lib/model';
import { Clock, StageManager } from '@serenity-js/core/lib/stage';

import { by, protractor } from 'protractor';
import { by, error, protractor } from 'protractor';
import * as sinon from 'sinon';
import { BrowseTheWeb, ExecuteScript, Navigate, Target, Text, Value } from '../../../../src';
import { pageFromTemplate } from '../../../fixtures';

/** @test {ExecuteSynchronousScript} */
describe('ExecuteSynchronousScript', function() {
this.timeout(30000);

const Joe = Actor.named('Joe').whoCan(
BrowseTheWeb.using(protractor.browser),
@@ -107,6 +106,17 @@ describe('ExecuteSynchronousScript', function() {
).to.equal(`#actor executes a synchronous script with arguments: [ a promised value, 'arg2', arg number 3 ]`);
});

/** @test {ExecuteScript.sync} */
/** @test {ExecuteSynchronousScript} */
/** @test {LastScriptExecution} */
it('complains if the script has failed', () => expect(Joe.attemptsTo(
Navigate.to(page),

ExecuteScript.sync(`
throw new Error("something's not quite right here");
`),
)).to.be.rejectedWith(error.JavascriptError, `javascript error: something's not quite right here`));

/** @test {ExecuteScript.sync} */
/** @test {ExecuteSynchronousScript} */
it('emits the events so that the details of the script being executed can be reported', () => {
@@ -0,0 +1,102 @@
import { expect } from '@integration/testing-tools';
import { Ensure, equals } from '@serenity-js/assertions';
import { Actor, LogicError } from '@serenity-js/core';
import { by, error, protractor } from 'protractor';
import { BrowseTheWeb, Enter, ExecuteScript, LastScriptExecution, Navigate, Target } from '../../../../src';
import { pageFromTemplate } from '../../../fixtures';

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

const Joe = () => Actor.named('Joe').whoCan(
BrowseTheWeb.using(protractor.browser),
);

const page = pageFromTemplate(`
<html>
<body>
<form>
<input type="text" id="name" />
</form>
</body>
</html>
`);

class Sandbox {
static Input = Target.the('input field').located(by.id('name'));
}

describe('when used with ExecuteScript.sync', () => {

/** @test {ExecuteScript.sync} */
/** @test {ExecuteSynchronousScript} */
/** @test {LastScriptExecution} */
it('allows the actor to retrieve the result of the script execution', () => Joe().attemptsTo(
Navigate.to(page),

Enter.theValue(Joe().name).into(Sandbox.Input),

ExecuteScript.sync(`
var field = arguments[0];
return field.value;
`).withArguments(Sandbox.Input),

Ensure.that(LastScriptExecution.result<string>(), equals(Joe().name)),
));

/** @test {ExecuteScript.sync} */
/** @test {ExecuteSynchronousScript} */
/** @test {LastScriptExecution} */
it('returns null if the script did not return any result', () => Joe().attemptsTo(
Navigate.to(page),

ExecuteScript.sync(`
/* do nothing */
`),

Ensure.that(LastScriptExecution.result<null>(), equals(null)),
));
});

describe('when used with ExecuteScript.async', () => {

/** @test {ExecuteScript.async} */
/** @test {ExecuteAsynchronousScript} */
/** @test {LastScriptExecution} */
it('allows the actor to retrieve the result of the script execution', () => Joe().attemptsTo(
Navigate.to(page),

Enter.theValue(Joe().name).into(Sandbox.Input),

ExecuteScript.async(`
var field = arguments[0];
var callback = arguments[arguments.length - 1];
callback(field.value);
`).withArguments(Sandbox.Input),

Ensure.that(LastScriptExecution.result<string>(), equals(Joe().name)),
));

/** @test {ExecuteScript.async} */
/** @test {ExecuteAsynchronousScript} */
/** @test {LastScriptExecution} */
it('returns null if the script did not return any result', () => Joe().attemptsTo(
Navigate.to(page),

ExecuteScript.async(`
var callback = arguments[arguments.length - 1];
callback();
`),

Ensure.that(LastScriptExecution.result<null>(), equals(null)),
));
});

/** @test {ExecuteAsynchronousScript} */
/** @test {LastScriptExecution} */
it(`complains if the script hasn't been executed yet`, () => expect(Joe().attemptsTo(
Navigate.to(page),

Ensure.that(LastScriptExecution.result<string>(), equals(Joe().name)),
)).to.be.rejectedWith(LogicError, 'Make sure to execute a script before checking on the result'));
});
@@ -1,4 +1,4 @@
import { Ability, UsesAbilities } from '@serenity-js/core';
import { Ability, LogicError, UsesAbilities } from '@serenity-js/core';
import { ActionSequence, ElementArrayFinder, ElementFinder, Locator, protractor, ProtractorBrowser } from 'protractor';
import { Navigation } from 'selenium-webdriver';
import { promiseOf } from '../promiseOf';
@@ -33,6 +33,11 @@ import { promiseOf } from '../promiseOf';
*/
export class BrowseTheWeb implements Ability {

/**
* @private
*/
private lastScriptExecutionSummary: LastScriptExecutionSummary;

/**
* @desc
* Ability to interact with web front-ends using a given protractor browser instance.
@@ -188,7 +193,13 @@ export class BrowseTheWeb implements Ability {
* @param {any[]} args
*/
executeScript(description: string, script: string | Function, ...args: any[]) { // tslint:disable-line:ban-types
return promiseOf(this.browser.executeScriptWithDescription(script, description, ...args));
return promiseOf(this.browser.executeScriptWithDescription(script, description, ...args))
.then(result => {
this.lastScriptExecutionSummary = new LastScriptExecutionSummary(
result,
);
return result;
});
}

/**
@@ -240,7 +251,14 @@ export class BrowseTheWeb implements Ability {
* @param {any[]} args
*/
executeAsyncScript(script: string | Function, ...args: any[]): Promise<any> { // tslint:disable-line:ban-types
return promiseOf(this.browser.executeAsyncScript(script, ...args));
return promiseOf(this.browser.executeAsyncScript(script, ...args))
.then(result => {
this.lastScriptExecutionSummary = new LastScriptExecutionSummary(
result,
);
return result;
});
// todo: should I wrap this an provide additional diagnostic information? execution time? error handling?
}

/**
@@ -284,4 +302,16 @@ export class BrowseTheWeb implements Ability {
wait(condition: () => Promise<boolean>, timeout: number): Promise<boolean> {
return promiseOf(this.browser.wait(condition, timeout));
}

getLastScriptExecutionResult(): any {
if (! this.lastScriptExecutionSummary) {
throw new LogicError(`Make sure to execute a script before checking on the result`);
}

return this.lastScriptExecutionSummary.result;
}
}

class LastScriptExecutionSummary {
constructor(public readonly result: any) {}
}
@@ -13,6 +13,62 @@ export class ExecuteScript {
return new ExecuteScriptFromUrl(sourceUrl);
}

/**
* @desc
* Schedules a command to execute asynchronous JavaScript in the context of the currently selected frame or window.
* The script fragment will be executed as the body of an anonymous function.
* If the script is provided as a function object, that function will be converted to a string for injection
* into the target window.
*
* Any arguments provided in addition to the script will be included as script arguments and may be referenced
* using the `arguments` object. Arguments may be a `boolean`, `number`, `string`
* or `Target` (`Question<ElementFinder>`).
* Arrays and objects may also be used as script arguments as long as each item adheres
* to the types previously mentioned.
*
* Unlike executing synchronous JavaScript with {@link ExecuteScript#sync},
* scripts executed with this function must explicitly signal they are finished by invoking the provided callback.
*
* This callback will always be injected into the executed function as the last argument,
* and thus may be referenced with `arguments[arguments.length - 1]`.
*
* If the script invokes the `callback` with a return value, this will be made available
* via the {@Link LastScriptExecution.result}
*
* **Please note** that in order to signal an error in the `script` you need to throw an {@link Error}
* instead of passing it to the callback function.
*
* @example <caption>Executing an async script</caption>
* actor.attemptsTo(ExecuteScript.async(
* var callback = arguments[arguments.length - 1];
*
* // do stuff
*
* callback(result)
* ));
*
* @example <caption>Passing arguments to an async script</caption>
* actor.attemptsTo(ExecuteScript.async(
* var name = arguments[0];
* var age = arguments[1];
* var callback = arguments[arguments.length - 1];
*
* // do stuff
*
* callback(result)
* ).withArguments('Bob', 24));
*
* @example <caption>Passing Target arguments to an async script</caption>
* actor.attemptsTo(ExecuteScript.async(
* var header = arguments[0]; // Target gets converted to a WebElement
* var callback = arguments[arguments.length - 1];
*
* callback(header.innerText)
* ).withArguments(Target.the('header').located(by.css('h1'))));
*
* @param {string | Function} script
* @returns {ExecuteScriptWithArguments}
*/
static async(script: string | Function): ExecuteScriptWithArguments { // tslint:disable-line:ban-types
return new ExecuteAsynchronousScript(script);
}
@@ -21,5 +77,3 @@ export class ExecuteScript {
return new ExecuteSynchronousScript(script);
}
}

// todo: add questions around scripts that return values
@@ -0,0 +1,16 @@
import { Question } from '@serenity-js/core';
import { BrowseTheWeb } from '../../abilities';

export class LastScriptExecution {

/**
* @desc
* Enables asserting on the result of a function executed via {@link ExecuteScript}.
*
* @returns {Question<Promise<R>>}
*/
static result<R>(): Question<Promise<R>> {
return Question.about(`last script execution result`, actor =>
BrowseTheWeb.as(actor).getLastScriptExecutionResult());
}
}
@@ -1,3 +1,4 @@
export * from './ExecuteScript';
export * from './ExecuteScriptFromUrl';
export * from './ExecuteScriptWithArguments';
export * from './LastScriptExecution';
@@ -2,7 +2,7 @@ export * from './Clear';
export * from './Click';
export * from './DoubleClick';
export * from './Enter';
export { ExecuteScript } from './execute-script';
export { ExecuteScript, LastScriptExecution } from './execute-script';
export * from './Navigate';
export * from './Press';
export * from './Wait';

0 comments on commit 75acc79

Please sign in to comment.