@serenity-js/web
Advanced tools
Comparing version 3.0.0-rc.2 to 3.0.0-rc.3
@@ -6,2 +6,26 @@ # Change Log | ||
# [3.0.0-rc.3](https://github.com/serenity-js/serenity-js/compare/v3.0.0-rc.2...v3.0.0-rc.3) (2021-12-29) | ||
### Bug Fixes | ||
* **core:** refactored Mappable so that it's easier to implement filters ([176e0cd](https://github.com/serenity-js/serenity-js/commit/176e0cd0303d63271477b2b7a8e7b0572dda99a0)), closes [#1074](https://github.com/serenity-js/serenity-js/issues/1074) | ||
* **deps:** updated tiny-types to 1.17.0 ([3187051](https://github.com/serenity-js/serenity-js/commit/3187051594158b4b450c82e851e417fd2ed21652)) | ||
* **web:** corrected synchronisation in Web questions and interactions ([c3a0ad1](https://github.com/serenity-js/serenity-js/commit/c3a0ad16de311e71d7e82e4f463baa0ca6b18863)) | ||
* **web:** Photographer skips taking a screenshot if the Window is closed (DevTools protocol) ([b682577](https://github.com/serenity-js/serenity-js/commit/b682577ad649046fc1a4cd61a7315e11d60dcf32)) | ||
* **web:** refactored Selector and NativeElementLocator classes to simplify the implementation ([f0c8f11](https://github.com/serenity-js/serenity-js/commit/f0c8f113433958877d36f13d0bc7f355ea68d280)) | ||
* **web:** simplified the selectors ([b167e42](https://github.com/serenity-js/serenity-js/commit/b167e422eb66556845c31d5847b9fd33b707c764)), closes [#1074](https://github.com/serenity-js/serenity-js/issues/1074) | ||
### Features | ||
* **core:** new implementation of List.where filters ([45b3c80](https://github.com/serenity-js/serenity-js/commit/45b3c8080ca467ac6362e5217e7899ca36a04cdc)), closes [#1074](https://github.com/serenity-js/serenity-js/issues/1074) | ||
* **web:** isVisible checks if the element is in viewport and not hidden behind other elements ([429040f](https://github.com/serenity-js/serenity-js/commit/429040fb32b04cd4bc7524100635203fd8128eb6)) | ||
* **web:** new PageElement retrieval model based on Selectors ([48bd94f](https://github.com/serenity-js/serenity-js/commit/48bd94f3c29707b66dcf81a7522f7529b6f9fcfb)) | ||
* **web:** re-introduced PageElements.where DSL and universal By selectors ([39fe0a1](https://github.com/serenity-js/serenity-js/commit/39fe0a10edf7f652e93911159e4a4689c36d6876)), closes [#1081](https://github.com/serenity-js/serenity-js/issues/1081) | ||
# [3.0.0-rc.2](https://github.com/serenity-js/serenity-js/compare/v3.0.0-rc.1...v3.0.0-rc.2) (2021-12-09) | ||
@@ -8,0 +32,0 @@ |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isVisible = void 0; | ||
const assertions_1 = require("@serenity-js/assertions"); | ||
const core_1 = require("@serenity-js/core"); | ||
const ElementExpectation_1 = require("./ElementExpectation"); | ||
const isPresent_1 = require("./isPresent"); | ||
/** | ||
@@ -20,8 +17,5 @@ * @desc | ||
function isVisible() { | ||
return core_1.Expectation.to('become visible').soThatActual((0, assertions_1.and)((0, isPresent_1.isPresent)(), isDisplayed())); | ||
return ElementExpectation_1.ElementExpectation.forElementTo('become visible', actual => actual.isVisible()); | ||
} | ||
exports.isVisible = isVisible; | ||
function isDisplayed() { | ||
return ElementExpectation_1.ElementExpectation.forElementTo('become displayed', actual => actual.isDisplayed()); | ||
} | ||
//# sourceMappingURL=isVisible.js.map |
import { Ability, Duration, UsesAbilities } from '@serenity-js/core'; | ||
import { Key } from '../../input'; | ||
import { Cookie, CookieData, ModalDialog, Page, PageElement, PageElements } from '../models'; | ||
import { Cookie, CookieData, ModalDialog, NativeElementLocator, Page, PageElement, Selector } from '../models'; | ||
import { BrowserCapabilities } from './BrowserCapabilities'; | ||
export declare abstract class BrowseTheWeb implements Ability { | ||
export declare abstract class BrowseTheWeb<Native_Element_Type = any> implements Ability { | ||
/** | ||
@@ -22,10 +22,5 @@ * @desc | ||
abstract waitUntil(condition: () => boolean | Promise<boolean>, timeout: Duration): Promise<void>; | ||
abstract findByCss(selector: string): PageElement; | ||
abstract findByCssContainingText(selector: string, text: string): PageElement; | ||
abstract findById(selector: string): PageElement; | ||
abstract findByTagName(selector: string): PageElement; | ||
abstract findByXPath(selector: string): PageElement; | ||
abstract findAllByCss(selector: string): PageElements; | ||
abstract findAllByTagName(selector: string): PageElements; | ||
abstract findAllByXPath(selector: string): PageElements; | ||
abstract locate<T>(selector: Selector<T>, locator?: NativeElementLocator<Native_Element_Type>): PageElement<Native_Element_Type>; | ||
abstract locateAll<T>(selector: Selector<T>, locator?: NativeElementLocator<Native_Element_Type>): Promise<Array<PageElement<Native_Element_Type>>>; | ||
abstract nativeElementLocator(): NativeElementLocator<Native_Element_Type>; | ||
abstract browserCapabilities(): Promise<BrowserCapabilities>; | ||
@@ -32,0 +27,0 @@ abstract sendKeys(keys: Array<Key | string>): Promise<void>; |
@@ -38,3 +38,3 @@ import { Answerable, AnswersQuestions, UsesAbilities } from '@serenity-js/core'; | ||
export declare class Enter extends PageElementInteraction { | ||
private readonly value; | ||
private readonly values; | ||
private readonly field; | ||
@@ -45,3 +45,3 @@ /** | ||
* | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -51,5 +51,5 @@ * | ||
*/ | ||
static theValue(...value: Array<Answerable<string | number | string[] | number[]>>): EnterBuilder; | ||
static theValue(...values: Array<Answerable<string | number | string[] | number[]>>): EnterBuilder; | ||
/** | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -60,3 +60,3 @@ * | ||
*/ | ||
constructor(value: Array<Answerable<string | number | string[] | number[]>>, field: Answerable<PageElement>); | ||
constructor(values: Array<Answerable<string | number | string[] | number[]>>, field: Answerable<PageElement>); | ||
/** | ||
@@ -63,0 +63,0 @@ * @desc |
@@ -40,3 +40,3 @@ "use strict"; | ||
/** | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -47,5 +47,5 @@ * | ||
*/ | ||
constructor(value, field /* todo | Question<AlertPromise> | AlertPromise */) { | ||
super((0, io_1.formatted) `#actor enters ${value.join(', ')} into ${field}`); | ||
this.value = value; | ||
constructor(values, field /* todo | Question<AlertPromise> | AlertPromise */) { | ||
super((0, io_1.formatted) `#actor enters ${values.join(', ')} into ${field}`); | ||
this.values = values; | ||
this.field = field; | ||
@@ -57,3 +57,3 @@ } | ||
* | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -63,5 +63,5 @@ * | ||
*/ | ||
static theValue(...value) { | ||
static theValue(...values) { | ||
return { | ||
into: (field /* todo Question<AlertPromise> | AlertPromise */) => new Enter(value, field), | ||
into: (field /* todo Question<AlertPromise> | AlertPromise */) => new Enter(values, field), | ||
}; | ||
@@ -84,5 +84,5 @@ } | ||
async performAs(actor) { | ||
const values = await Promise.all(this.value.map(part => actor.answer(part))); | ||
const field = await this.resolve(actor, this.field); | ||
return field.enterValue(values.flat()); | ||
const valuesToEnter = await (0, io_1.asyncMap)(this.values, value => actor.answer(value)); | ||
return field.enterValue(valuesToEnter.flat()); | ||
} | ||
@@ -89,0 +89,0 @@ } |
@@ -204,4 +204,4 @@ import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, UsesAbilities } from '@serenity-js/core'; | ||
*/ | ||
performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): PromiseLike<void>; | ||
performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void>; | ||
protected abstract executeAs(actor: UsesAbilities & AnswersQuestions, args: any[]): Promise<any>; | ||
} |
@@ -208,10 +208,9 @@ "use strict"; | ||
*/ | ||
performAs(actor) { | ||
// todo: interaction should store the result on the ability; the ability should not have a side-effect (?) | ||
return Promise.all(this.args.map(arg => actor.answer(arg))) | ||
.then(args => this.executeAs(actor, args)) | ||
.then(() => actor.collect(model_1.TextData.fromJSON({ | ||
async performAs(actor) { | ||
const args = await (0, io_1.asyncMap)(this.args, arg => actor.answer(arg)); | ||
await this.executeAs(actor, args); | ||
actor.collect(model_1.TextData.fromJSON({ | ||
contentType: 'text/javascript;charset=UTF-8', | ||
data: this.script.toString(), | ||
}), new model_1.Name('Script source'))); | ||
}), new model_1.Name('Script source')); | ||
} | ||
@@ -218,0 +217,0 @@ } |
@@ -136,6 +136,7 @@ "use strict"; | ||
} | ||
answeredBy(actor) { | ||
return Promise.all(this.keys.map(part => actor.answer(part))).then(keys => { | ||
return keys.flat().filter(key => !!key); | ||
}); | ||
async answeredBy(actor) { | ||
const keys = await (0, io_1.asyncMap)(this.keys, key => actor.answer(key)); | ||
return keys | ||
.flat() | ||
.filter(key => !!key); | ||
} | ||
@@ -142,0 +143,0 @@ /** |
@@ -69,3 +69,3 @@ "use strict"; | ||
from: (pageElement) => screenplay_1.Interaction.where((0, io_1.formatted) `#actor selects value ${value} from ${pageElement}`, async (actor) => { | ||
return models_1.PageElement.locatedByCss((0, core_1.q) `option[value=${value}]`) | ||
return models_1.PageElement.located(models_1.By.css((0, core_1.q) `option[value=${value}]`)) | ||
.of(pageElement) | ||
@@ -128,10 +128,13 @@ .click() | ||
from: (pageElement) => screenplay_1.Interaction.where(`#actor selects values ${(0, io_1.commaSeparated)(values.flat(), item => (0, inspected_1.inspected)(item, { inline: true }))} from ${(0, inspected_1.inspected)(pageElement, { inline: true })}`, async (actor) => { | ||
const desiredValues = (await Promise.all(values.map(value => actor.answer(value)))).flat(); // eslint-disable-line unicorn/no-await-expression-member | ||
const options = await models_1.PageElements.locatedByCss(`option`).of(pageElement).answeredBy(actor); | ||
const shouldSelect = await options.map(optionsToSelect(hasValueEqualOneOf(desiredValues))); | ||
return options.forEach((option, index) => { | ||
if (shouldSelect[index]) { | ||
return option.click(); | ||
const answers = await (0, io_1.asyncMap)(values, value => actor.answer(value)); | ||
const desiredValues = answers.flat(); | ||
const options = await models_1.PageElements.located(models_1.By.css(`option`)) | ||
.of(pageElement) | ||
.answeredBy(actor); | ||
for (const option of options) { | ||
const shouldSelect = await optionsToSelect(hasValueEqualOneOf(desiredValues))(option); | ||
if (shouldSelect) { | ||
await option.click(); | ||
} | ||
}); | ||
} | ||
}), | ||
@@ -193,3 +196,3 @@ }; | ||
from: (pageElement) => screenplay_1.Interaction.where((0, io_1.formatted) `#actor selects ${value} from ${pageElement}`, async (actor) => { | ||
return models_1.PageElement.locatedByCssContainingText('option', value) | ||
return models_1.PageElement.located(models_1.By.cssContainingText('option', value)) | ||
.of(pageElement) | ||
@@ -254,10 +257,11 @@ .click() | ||
from: (pageElement) => screenplay_1.Interaction.where(`#actor selects ${(0, io_1.commaSeparated)(values.flat(), item => (0, inspected_1.inspected)(item, { inline: true }))} from ${(0, inspected_1.inspected)(pageElement, { inline: true })}`, async (actor) => { | ||
const desiredOptions = (await Promise.all(values.map(value => actor.answer(value)))).flat(); // eslint-disable-line unicorn/no-await-expression-member | ||
const options = await models_1.PageElements.locatedByCss(`option`).of(pageElement).answeredBy(actor); | ||
const shouldSelect = await options.map(optionsToSelect(hasTextEqualOneOf(desiredOptions))); | ||
return options.forEach((option, index) => { | ||
if (shouldSelect[index]) { | ||
return option.click(); | ||
const answers = await (0, io_1.asyncMap)(values, value => actor.answer(value)); | ||
const desiredOptions = answers.flat(); | ||
const options = await models_1.PageElements.located(models_1.By.css(`option`)).of(pageElement).answeredBy(actor); | ||
for (const option of options) { | ||
const shouldSelect = await optionsToSelect(hasTextEqualOneOf(desiredOptions))(option); | ||
if (shouldSelect) { | ||
await option.click(); | ||
} | ||
}); | ||
} | ||
}), | ||
@@ -264,0 +268,0 @@ }; |
@@ -59,3 +59,3 @@ import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, UsesAbilities } from '@serenity-js/core'; | ||
*/ | ||
performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): PromiseLike<void>; | ||
performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void>; | ||
/** | ||
@@ -62,0 +62,0 @@ * @desc |
@@ -69,7 +69,6 @@ "use strict"; | ||
*/ | ||
performAs(actor) { | ||
return Promise.all([ | ||
abilities_1.BrowseTheWeb.as(actor).takeScreenshot(), | ||
actor.answer(this.name), | ||
]).then(([screenshot, name]) => actor.collect(model_1.Photo.fromBase64(screenshot), new model_1.Name(name))); | ||
async performAs(actor) { | ||
const screenshot = await abilities_1.BrowseTheWeb.as(actor).takeScreenshot(); | ||
const name = await actor.answer(this.name); | ||
actor.collect(model_1.Photo.fromBase64(screenshot), new model_1.Name(name)); | ||
} | ||
@@ -76,0 +75,0 @@ /** |
export * from './Cookie'; | ||
export * from './CookieData'; | ||
export * from './ModalDialog'; | ||
export * from './NativeElementLocator'; | ||
export * from './Page'; | ||
export * from './PageElement'; | ||
export * from './PageElements'; | ||
export * from './PassThroughNativeElementLocator'; | ||
export * from './selectors'; |
@@ -16,5 +16,8 @@ "use strict"; | ||
__exportStar(require("./ModalDialog"), exports); | ||
__exportStar(require("./NativeElementLocator"), exports); | ||
__exportStar(require("./Page"), exports); | ||
__exportStar(require("./PageElement"), exports); | ||
__exportStar(require("./PageElements"), exports); | ||
__exportStar(require("./PassThroughNativeElementLocator"), exports); | ||
__exportStar(require("./selectors"), exports); | ||
//# sourceMappingURL=index.js.map |
import { Adapter, Answerable, Question } from '@serenity-js/core'; | ||
export declare abstract class PageElement<NativeElementContext = any, NativeElement = any> { | ||
protected readonly context: () => Promise<NativeElementContext> | NativeElementContext; | ||
protected readonly locator: (root: NativeElementContext) => Promise<NativeElement> | NativeElement; | ||
static of(childElement: Answerable<PageElement>, parentElement: Answerable<PageElement>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
static locatedByCss(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
static locatedByCssContainingText(selector: Answerable<string>, text: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
static locatedById(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
static locatedByTagName(tagName: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement>; | ||
constructor(context: () => Promise<NativeElementContext> | NativeElementContext, locator: (root: NativeElementContext) => Promise<NativeElement> | NativeElement); | ||
abstract of(parent: PageElement): PageElement; | ||
nativeElement(): Promise<NativeElement>; | ||
import { NativeElementLocator } from './NativeElementLocator'; | ||
import { Selector } from './selectors'; | ||
export declare abstract class PageElement<Native_Element_Type = any> { | ||
protected readonly selector: Selector<unknown>; | ||
private readonly locator; | ||
static located<NET, ST>(selector: Answerable<Selector<ST>>): Question<Promise<PageElement<NET>>> & Adapter<PageElement<NET>>; | ||
static of<NET>(childElement: Answerable<PageElement<NET>>, parentElement: Answerable<PageElement<NET>>): Question<Promise<PageElement<NET>>> & Adapter<PageElement<NET>>; | ||
constructor(selector: Selector<unknown>, locator: NativeElementLocator<Native_Element_Type>); | ||
abstract of(parent: PageElement<Native_Element_Type>): PageElement<Native_Element_Type>; | ||
nativeElement(): Promise<Native_Element_Type>; | ||
nativeElementLocator(): NativeElementLocator<Native_Element_Type>; | ||
toString(): string; | ||
abstract enterValue(value: string | number | Array<string | number>): Promise<void>; | ||
@@ -26,6 +26,6 @@ abstract clearValue(): Promise<void>; | ||
abstract isClickable(): Promise<boolean>; | ||
abstract isDisplayed(): Promise<boolean>; | ||
abstract isEnabled(): Promise<boolean>; | ||
abstract isPresent(): Promise<boolean>; | ||
abstract isSelected(): Promise<boolean>; | ||
abstract isVisible(): Promise<boolean>; | ||
} |
@@ -7,8 +7,13 @@ "use strict"; | ||
const d = (0, core_1.format)({ markQuestions: false }); | ||
const f = (0, core_1.format)({ markQuestions: true }); | ||
class PageElement { | ||
constructor(context, locator) { | ||
this.context = context; | ||
constructor(selector, locator) { | ||
this.selector = selector; | ||
this.locator = locator; | ||
} | ||
static located(selector) { | ||
return core_1.Question.about(d `page element located ${selector}`, async (actor) => { | ||
const bySelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).locate(bySelector); | ||
}); | ||
} | ||
static of(childElement, parentElement) { | ||
@@ -21,37 +26,5 @@ return core_1.Question.about(d `${childElement} of ${parentElement})`, async (actor) => { | ||
} | ||
static locatedByCss(selector) { | ||
return core_1.Question.about(f `page element located by css (${selector})`, async (actor) => { | ||
const cssSelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findByCss(cssSelector); | ||
}); | ||
} | ||
static locatedByCssContainingText(selector, text) { | ||
return core_1.Question.about(f `page element located by css (${selector}) containing text ${text}`, async (actor) => { | ||
const cssSelector = await actor.answer(selector); | ||
const desiredText = await actor.answer(text); | ||
return abilities_1.BrowseTheWeb.as(actor).findByCssContainingText(cssSelector, desiredText); | ||
}); | ||
} | ||
static locatedById(selector) { | ||
return core_1.Question.about(f `page element located by id (${selector})`, async (actor) => { | ||
const idSelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findById(idSelector); | ||
}); | ||
} | ||
static locatedByTagName(tagName) { | ||
return core_1.Question.about(f `page element located by tag name (${tagName})`, async (actor) => { | ||
const tagNameSelector = await actor.answer(tagName); | ||
return abilities_1.BrowseTheWeb.as(actor).findByTagName(tagNameSelector); | ||
}); | ||
} | ||
static locatedByXPath(selector) { | ||
return core_1.Question.about(f `page element located by xpath (${selector})`, async (actor) => { | ||
const xpathSelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findByXPath(xpathSelector); | ||
}); | ||
} | ||
async nativeElement() { | ||
try { | ||
const context = await this.context(); | ||
return this.locator(context); | ||
return this.locator.locate(this.selector); | ||
} | ||
@@ -62,4 +35,10 @@ catch (error) { | ||
} | ||
nativeElementLocator() { | ||
return this.locator; | ||
} | ||
toString() { | ||
return `PageElement located ${this.selector}`; | ||
} | ||
} | ||
exports.PageElement = PageElement; | ||
//# sourceMappingURL=PageElement.js.map |
@@ -1,20 +0,15 @@ | ||
import { Adapter, Answerable, Question } from '@serenity-js/core'; | ||
import { Answerable, List } from '@serenity-js/core'; | ||
import { PageElement } from './PageElement'; | ||
export declare abstract class PageElements<NativeElementContext = any, NativeElementList = any, NativeElement = any> { | ||
protected readonly context: () => Promise<NativeElementContext> | NativeElementContext; | ||
protected readonly locator: (root: NativeElementContext) => Promise<NativeElementList> | NativeElementList; | ||
static of(childElements: Answerable<PageElements>, parentElement: Answerable<PageElement>): Question<Promise<PageElements>> & Adapter<PageElements>; | ||
static locatedByCss(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements>; | ||
static locatedByTagName(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements>; | ||
static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements>; | ||
constructor(context: () => Promise<NativeElementContext> | NativeElementContext, locator: (root: NativeElementContext) => Promise<NativeElementList> | NativeElementList); | ||
abstract of(parent: PageElement): PageElements; | ||
nativeElementList(): Promise<NativeElementList>; | ||
abstract count(): Promise<number>; | ||
abstract first(): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
abstract last(): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
abstract get(index: number): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
abstract map<O>(fn: (element: PageElement, index?: number, elements?: PageElements) => Promise<O> | O): Promise<O[]>; | ||
abstract filter(fn: (element: PageElement, index?: number) => Promise<boolean> | boolean): PageElements; | ||
abstract forEach(fn: (element: PageElement, index?: number) => Promise<void> | void): Promise<void>; | ||
import { Selector } from './selectors'; | ||
export declare class PageElements<Native_Element_Type = any> extends List<PageElement<Native_Element_Type>> { | ||
protected readonly selector: Answerable<Selector<unknown>>; | ||
private readonly parent?; | ||
static located<NET, ST>(selector: Answerable<Selector<ST>>): PageElements<NET>; | ||
/** | ||
* @param {Answerable<Selector<unknown>>} selector | ||
* @param {Answerable<PageElement>} [parent] | ||
* if not specified, browser root selector is used | ||
*/ | ||
constructor(selector: Answerable<Selector<unknown>>, parent?: Answerable<PageElement>); | ||
of(parent: Answerable<PageElement<Native_Element_Type>>): PageElements<Native_Element_Type>; | ||
} |
@@ -6,45 +6,46 @@ "use strict"; | ||
const abilities_1 = require("../abilities"); | ||
const d = (0, core_1.format)({ markQuestions: false }); | ||
const f = (0, core_1.format)({ markQuestions: true }); | ||
class PageElements { | ||
constructor(context, locator) { | ||
this.context = context; | ||
this.locator = locator; | ||
class PageElements extends core_1.List { | ||
/** | ||
* @param {Answerable<Selector<unknown>>} selector | ||
* @param {Answerable<PageElement>} [parent] | ||
* if not specified, browser root selector is used | ||
*/ | ||
constructor(selector, parent) { | ||
super(new PageElementCollection(selector, parent)); | ||
this.selector = selector; | ||
this.parent = parent; | ||
} | ||
static of(childElements, parentElement) { | ||
return core_1.Question.about(d `${childElements} of ${parentElement})`, async (actor) => { | ||
const children = await actor.answer(childElements); | ||
const parent = await actor.answer(parentElement); | ||
return children.of(parent); | ||
}); | ||
static located(selector) { | ||
return new PageElements(selector); | ||
} | ||
static locatedByCss(selector) { | ||
return core_1.Question.about(f `page elements located by css (${selector})`, async (actor) => { | ||
const value = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findAllByCss(value); | ||
}); | ||
of(parent) { | ||
return new PageElements(this.selector, parent) | ||
.describedAs(`<<${this.toString()}>>` + f `.of(${parent})`); | ||
} | ||
static locatedByTagName(selector) { | ||
return core_1.Question.about(f `page elements located by tag name (${selector})`, async (actor) => { | ||
const tagNameSelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findAllByTagName(tagNameSelector); | ||
}); | ||
} | ||
exports.PageElements = PageElements; | ||
/** | ||
* @package | ||
*/ | ||
class PageElementCollection extends core_1.Question { | ||
constructor(selector, parent) { | ||
super(); | ||
this.selector = selector; | ||
this.parent = parent; | ||
this.subject = `page elements located ${selector.toString()}`; | ||
} | ||
static locatedByXPath(selector) { | ||
return core_1.Question.about(f `page elements located by xpath (${selector})`, async (actor) => { | ||
const xpathSelector = await actor.answer(selector); | ||
return abilities_1.BrowseTheWeb.as(actor).findAllByXPath(xpathSelector); | ||
}); | ||
async answeredBy(actor) { | ||
const bySelector = await actor.answer(this.selector); | ||
const parent = await actor.answer(this.parent); | ||
return abilities_1.BrowseTheWeb.as(actor).locateAll(bySelector, parent ? parent.nativeElementLocator() : undefined); | ||
} | ||
async nativeElementList() { | ||
try { | ||
const context = await this.context(); | ||
return this.locator(context); | ||
} | ||
catch (error) { | ||
throw new core_1.LogicError(`Couldn't find elements`, error); | ||
} | ||
describedAs(subject) { | ||
this.subject = subject; | ||
return this; | ||
} | ||
toString() { | ||
return this.subject; | ||
} | ||
} | ||
exports.PageElements = PageElements; | ||
//# sourceMappingURL=PageElements.js.map |
import { Answerable, AnswersQuestions, Question } from '@serenity-js/core'; | ||
import { PageElement, PageElements } from '../models'; | ||
/** | ||
@@ -33,3 +32,3 @@ * @desc | ||
*/ | ||
protected resolve<T = PageElement | PageElements>(actor: AnswersQuestions, element: Answerable<T>): Promise<T>; | ||
protected resolve<T>(actor: AnswersQuestions, element: Answerable<T>): Promise<T>; | ||
} |
@@ -6,2 +6,4 @@ "use strict"; | ||
const models_1 = require("../models"); | ||
const Text_1 = require("./Text"); | ||
const Value_1 = require("./Value"); | ||
/** | ||
@@ -57,3 +59,3 @@ * @desc | ||
static valueOf(pageElement) { | ||
return models_1.PageElement.locatedByCss('option:checked') | ||
return models_1.PageElement.located(models_1.By.css('option:checked')) | ||
.of(pageElement) | ||
@@ -105,5 +107,5 @@ .value() | ||
static valuesOf(pageElement) { | ||
return models_1.PageElements.locatedByCss('option:checked') | ||
return models_1.PageElements.located(models_1.By.css('option:checked')) | ||
.of(pageElement) | ||
.map(item => item.value()) | ||
.eachMappedTo(Value_1.Value) | ||
.describedAs((0, io_1.formatted) `values selected in ${pageElement}`); | ||
@@ -156,3 +158,3 @@ } | ||
static optionIn(pageElement) { | ||
return models_1.PageElement.locatedByCss('option:checked') | ||
return models_1.PageElement.located(models_1.By.css('option:checked')) | ||
.of(pageElement) | ||
@@ -207,5 +209,5 @@ .text() | ||
static optionsIn(pageElement) { | ||
return models_1.PageElements.locatedByCss('option:checked') | ||
return models_1.PageElements.located(models_1.By.css('option:checked')) | ||
.of(pageElement) | ||
.map(item => item.text()) | ||
.eachMappedTo(Text_1.Text) | ||
.describedAs((0, io_1.formatted) `options selected in ${pageElement}`); | ||
@@ -212,0 +214,0 @@ } |
@@ -96,5 +96,5 @@ import { Adapter, Answerable, MetaQuestion, Question } from '@serenity-js/core'; | ||
*/ | ||
static ofAll(elements: Answerable<PageElements>): Question<Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent | ||
static ofAll(elements: PageElements): Question<Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent | ||
MetaQuestion<Answerable<PageElement>, Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent | ||
Adapter<string[]>; | ||
} |
@@ -5,2 +5,3 @@ "use strict"; | ||
const core_1 = require("@serenity-js/core"); | ||
const io_1 = require("@serenity-js/core/lib/io"); | ||
const models_1 = require("../models"); | ||
@@ -125,9 +126,10 @@ const ElementQuestion_1 = require("./ElementQuestion"); | ||
of(parent) { | ||
return new TextOfMultipleElements(models_1.PageElements.of(this.elements, parent)); | ||
// todo: return Question.about directly? | ||
return new TextOfMultipleElements(this.elements.of(parent)); | ||
} | ||
async answeredBy(actor) { | ||
const elements = await this.resolve(actor, this.elements); | ||
return elements.map(element => element.text()); | ||
return (0, io_1.asyncMap)(elements, element => element.text()); | ||
} | ||
} | ||
//# sourceMappingURL=Text.js.map |
@@ -52,6 +52,4 @@ "use strict"; | ||
try { | ||
const [screenshot, capabilities] = await Promise.all([ | ||
browseTheWeb.takeScreenshot(), | ||
browseTheWeb.browserCapabilities(), | ||
]); | ||
const capabilities = await browseTheWeb.browserCapabilities(); | ||
const screenshot = await browseTheWeb.takeScreenshot(); | ||
const context = [capabilities.platformName, capabilities.browserName, capabilities.browserVersion], photoName = this.combinedNameFrom(...context, nameSuffix); | ||
@@ -74,4 +72,4 @@ stage.announce(new events_1.ActivityRelatedArtifactGenerated(event.sceneId, event.activityId, photoName, model_1.Photo.fromBase64(screenshot))); | ||
shouldIgnore(error) { | ||
return error.name | ||
&& (error.name === 'NoSuchSessionError'); | ||
return error.name && (error.name === 'NoSuchSessionError' || | ||
(error.name === 'ProtocolError' && error.message.includes('Target closed'))); | ||
// todo: add SauceLabs | ||
@@ -78,0 +76,0 @@ // [0-0] 2021-12-02T01:32:36.402Z ERROR webdriver: Request failed with status 404 due to no such window: no such window: target window already closed |
{ | ||
"name": "@serenity-js/web", | ||
"version": "3.0.0-rc.2", | ||
"version": "3.0.0-rc.3", | ||
"description": "Serenity/JS Screenplay Pattern APIs for the Web", | ||
@@ -49,5 +49,5 @@ "author": { | ||
"dependencies": { | ||
"@serenity-js/assertions": "3.0.0-rc.2", | ||
"@serenity-js/core": "3.0.0-rc.1", | ||
"tiny-types": "^1.16.1" | ||
"@serenity-js/assertions": "3.0.0-rc.3", | ||
"@serenity-js/core": "3.0.0-rc.3", | ||
"tiny-types": "^1.17.0" | ||
}, | ||
@@ -85,3 +85,3 @@ "devDependencies": { | ||
}, | ||
"gitHead": "944ff400a3766e75980aea5d3b401d8acca7c6d8" | ||
"gitHead": "69da7444e581c457e29e87edcd910ec06a069338" | ||
} |
@@ -1,2 +0,1 @@ | ||
import { and } from '@serenity-js/assertions'; | ||
import { Expectation } from '@serenity-js/core'; | ||
@@ -6,3 +5,2 @@ | ||
import { ElementExpectation } from './ElementExpectation'; | ||
import { isPresent } from './isPresent'; | ||
@@ -21,10 +19,3 @@ /** | ||
export function isVisible(): Expectation<boolean, PageElement> { | ||
return Expectation.to<PageElement>('become visible').soThatActual(and( | ||
isPresent(), | ||
isDisplayed(), | ||
)); | ||
return ElementExpectation.forElementTo('become visible', actual => actual.isVisible()); | ||
} | ||
function isDisplayed(): Expectation<any, PageElement> { | ||
return ElementExpectation.forElementTo('become displayed', actual => actual.isDisplayed()); | ||
} |
import { Ability, Duration, UsesAbilities } from '@serenity-js/core'; | ||
import { Key } from '../../input'; | ||
import { Cookie, CookieData, ModalDialog, Page, PageElement, PageElements } from '../models'; | ||
import { Cookie, CookieData, ModalDialog, NativeElementLocator, Page, PageElement, Selector } from '../models'; | ||
import { BrowserCapabilities } from './BrowserCapabilities'; | ||
export abstract class BrowseTheWeb implements Ability { | ||
export abstract class BrowseTheWeb<Native_Element_Type = any> implements Ability { | ||
/** | ||
@@ -33,12 +33,6 @@ * @desc | ||
abstract findByCss(selector: string): PageElement; | ||
abstract findByCssContainingText(selector: string, text: string): PageElement; | ||
abstract findById(selector: string): PageElement; | ||
abstract findByTagName(selector: string): PageElement; | ||
abstract findByXPath(selector: string): PageElement; | ||
abstract locate<T>(selector: Selector<T>, locator?: NativeElementLocator<Native_Element_Type>): PageElement<Native_Element_Type>; | ||
abstract locateAll<T>(selector: Selector<T>, locator?: NativeElementLocator<Native_Element_Type>): Promise<Array<PageElement<Native_Element_Type>>>; | ||
abstract nativeElementLocator(): NativeElementLocator<Native_Element_Type>; | ||
abstract findAllByCss(selector: string): PageElements; | ||
abstract findAllByTagName(selector: string): PageElements; | ||
abstract findAllByXPath(selector: string): PageElements; | ||
abstract browserCapabilities(): Promise<BrowserCapabilities>; | ||
@@ -45,0 +39,0 @@ |
import { Answerable, AnswersQuestions, UsesAbilities } from '@serenity-js/core'; | ||
import { formatted } from '@serenity-js/core/lib/io'; | ||
import { asyncMap, formatted } from '@serenity-js/core/lib/io'; | ||
@@ -46,3 +46,3 @@ import { PageElement } from '../models'; | ||
* | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -52,6 +52,6 @@ * | ||
*/ | ||
static theValue(...value: Array<Answerable<string | number | string[] | number[]>>): EnterBuilder { | ||
static theValue(...values: Array<Answerable<string | number | string[] | number[]>>): EnterBuilder { | ||
return { | ||
into: (field: Answerable<PageElement> /* todo Question<AlertPromise> | AlertPromise */) => | ||
new Enter(value, field), | ||
new Enter(values, field), | ||
}; | ||
@@ -61,3 +61,3 @@ } | ||
/** | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} value | ||
* @param {Array<Answerable<string | number | string[] | number[]>>} values | ||
* The value to be entered | ||
@@ -69,6 +69,6 @@ * | ||
constructor( | ||
private readonly value: Array<Answerable<string | number | string[] | number[]>>, | ||
private readonly values: Array<Answerable<string | number | string[] | number[]>>, | ||
private readonly field: Answerable<PageElement> /* todo | Question<AlertPromise> | AlertPromise */, | ||
) { | ||
super(formatted `#actor enters ${ value.join(', ') } into ${ field }`); | ||
super(formatted `#actor enters ${ values.join(', ') } into ${ field }`); | ||
} | ||
@@ -91,7 +91,8 @@ | ||
async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> { | ||
const values = await Promise.all(this.value.map(part => actor.answer(part))); | ||
const field = await this.resolve(actor, this.field); | ||
return field.enterValue(values.flat()); | ||
const valuesToEnter = await asyncMap(this.values, value => actor.answer(value)) | ||
return field.enterValue(valuesToEnter.flat()); | ||
} | ||
} |
import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, LogicError, UsesAbilities } from '@serenity-js/core'; | ||
import { formatted } from '@serenity-js/core/lib/io'; | ||
import { asyncMap, formatted } from '@serenity-js/core/lib/io'; | ||
import { Name, TextData } from '@serenity-js/core/lib/model'; | ||
@@ -224,13 +224,14 @@ | ||
*/ | ||
performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): PromiseLike<void> { | ||
// todo: interaction should store the result on the ability; the ability should not have a side-effect (?) | ||
return Promise.all(this.args.map(arg => actor.answer(arg))) | ||
.then(args => this.executeAs(actor, args)) | ||
.then(() => actor.collect( | ||
TextData.fromJSON({ | ||
contentType: 'text/javascript;charset=UTF-8', | ||
data: this.script.toString(), | ||
}), | ||
new Name('Script source'), | ||
)); | ||
async performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> { | ||
const args = await asyncMap(this.args, arg => actor.answer(arg)); | ||
await this.executeAs(actor, args); | ||
actor.collect( | ||
TextData.fromJSON({ | ||
contentType: 'text/javascript;charset=UTF-8', | ||
data: this.script.toString(), | ||
}), | ||
new Name('Script source'), | ||
); | ||
} | ||
@@ -237,0 +238,0 @@ |
import { Activity, Answerable, AnswersQuestions, Interaction, Question, UsesAbilities } from '@serenity-js/core'; | ||
import { formatted } from '@serenity-js/core/lib/io'; | ||
import { asyncMap, formatted } from '@serenity-js/core/lib/io'; | ||
@@ -152,8 +152,8 @@ import { Key } from '../../input'; | ||
answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Array<string | Key>> { | ||
return Promise.all( | ||
this.keys.map(part => actor.answer(part)) | ||
).then(keys => { | ||
return keys.flat().filter(key => !! key) | ||
}) | ||
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Array<string | Key>> { | ||
const keys = await asyncMap(this.keys, key => actor.answer(key)); | ||
return keys | ||
.flat() | ||
.filter(key => !! key); | ||
} | ||
@@ -160,0 +160,0 @@ |
import { Answerable, q } from '@serenity-js/core'; | ||
import { commaSeparated, formatted } from '@serenity-js/core/lib/io'; | ||
import { asyncMap, commaSeparated, formatted } from '@serenity-js/core/lib/io'; | ||
import { inspected } from '@serenity-js/core/lib/io/inspected'; | ||
import { Interaction } from '@serenity-js/core/lib/screenplay'; | ||
import { PageElement, PageElements } from '../models'; | ||
import { By, PageElement, PageElements } from '../models'; | ||
import { SelectBuilder } from './SelectBuilder'; | ||
@@ -71,3 +71,3 @@ | ||
Interaction.where(formatted `#actor selects value ${ value } from ${ pageElement }`, async actor => { | ||
return PageElement.locatedByCss(q`option[value=${ value }]`) | ||
return PageElement.located(By.css(q`option[value=${ value }]`)) | ||
.of(pageElement) | ||
@@ -133,12 +133,15 @@ .click() | ||
const desiredValues = (await Promise.all(values.map(value => actor.answer(value)))).flat(); // eslint-disable-line unicorn/no-await-expression-member | ||
const answers = await asyncMap(values, value => actor.answer(value)); | ||
const desiredValues = answers.flat(); | ||
const options: PageElements = await PageElements.locatedByCss(`option`).of(pageElement).answeredBy(actor); | ||
const shouldSelect: boolean[] = await options.map(optionsToSelect(hasValueEqualOneOf(desiredValues))); | ||
const options: PageElement[] = await PageElements.located(By.css(`option`)) | ||
.of(pageElement) | ||
.answeredBy(actor); | ||
return options.forEach((option, index) => { | ||
if (shouldSelect[index]) { | ||
return option.click() | ||
for (const option of options) { | ||
const shouldSelect = await optionsToSelect(hasValueEqualOneOf(desiredValues))(option); | ||
if (shouldSelect) { | ||
await option.click(); | ||
} | ||
}); | ||
} | ||
}), | ||
@@ -202,3 +205,3 @@ }; | ||
Interaction.where(formatted `#actor selects ${ value } from ${ pageElement }`, async actor => { | ||
return PageElement.locatedByCssContainingText('option', value) | ||
return PageElement.located(By.cssContainingText('option', value)) | ||
.of(pageElement) | ||
@@ -266,12 +269,13 @@ .click() | ||
const desiredOptions = (await Promise.all(values.map(value => actor.answer(value)))).flat(); // eslint-disable-line unicorn/no-await-expression-member | ||
const answers = await asyncMap(values, value => actor.answer(value)); | ||
const desiredOptions = answers.flat(); | ||
const options: PageElements = await PageElements.locatedByCss(`option`).of(pageElement).answeredBy(actor); | ||
const shouldSelect: boolean[] = await options.map(optionsToSelect(hasTextEqualOneOf(desiredOptions))); | ||
const options: PageElement[] = await PageElements.located(By.css(`option`)).of(pageElement).answeredBy(actor); | ||
return options.forEach((option, index) => { | ||
if (shouldSelect[index]) { | ||
return option.click() | ||
for (const option of options) { | ||
const shouldSelect = await optionsToSelect(hasTextEqualOneOf(desiredOptions))(option); | ||
if (shouldSelect) { | ||
await option.click() | ||
} | ||
}); | ||
} | ||
}), | ||
@@ -278,0 +282,0 @@ }; |
@@ -70,10 +70,10 @@ import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, UsesAbilities } from '@serenity-js/core'; | ||
*/ | ||
performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): PromiseLike<void> { | ||
return Promise.all([ | ||
BrowseTheWeb.as(actor).takeScreenshot(), | ||
actor.answer(this.name), | ||
]).then(([ screenshot, name ]) => actor.collect( | ||
async performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> { | ||
const screenshot = await BrowseTheWeb.as(actor).takeScreenshot(); | ||
const name = await actor.answer(this.name); | ||
actor.collect( | ||
Photo.fromBase64(screenshot), | ||
new Name(name), | ||
)); | ||
); | ||
} | ||
@@ -80,0 +80,0 @@ |
export * from './Cookie'; | ||
export * from './CookieData'; | ||
export * from './ModalDialog'; | ||
export * from './NativeElementLocator'; | ||
export * from './Page'; | ||
export * from './PageElement'; | ||
export * from './PageElements'; | ||
export * from './PassThroughNativeElementLocator'; | ||
export * from './selectors'; |
import { Adapter, Answerable, format, LogicError, Question } from '@serenity-js/core'; | ||
import { BrowseTheWeb } from '../abilities'; | ||
import { NativeElementLocator } from './NativeElementLocator'; | ||
import { Selector } from './selectors'; | ||
const d = format({ markQuestions: false }); | ||
const f = format({ markQuestions: true }); | ||
export abstract class PageElement<NativeElementContext = any, NativeElement = any> { | ||
static of(childElement: Answerable<PageElement>, parentElement: Answerable<PageElement>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
export abstract class PageElement<Native_Element_Type = any> { | ||
static located<NET, ST>(selector: Answerable<Selector<ST>>): Question<Promise<PageElement<NET>>> & Adapter<PageElement<NET>> { | ||
return Question.about(d`page element located ${ selector }`, async actor => { | ||
const bySelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).locate<ST>(bySelector); | ||
}); | ||
} | ||
static of<NET>(childElement: Answerable<PageElement<NET>>, parentElement: Answerable<PageElement<NET>>): Question<Promise<PageElement<NET>>> & Adapter<PageElement<NET>> { | ||
return Question.about(d`${ childElement } of ${ parentElement })`, async actor => { | ||
@@ -18,55 +27,13 @@ const child = await actor.answer(childElement); | ||
static locatedByCss(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
return Question.about(f`page element located by css (${selector})`, async actor => { | ||
const cssSelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findByCss(cssSelector); | ||
}); | ||
} | ||
static locatedByCssContainingText(selector: Answerable<string>, text: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
return Question.about(f`page element located by css (${selector}) containing text ${ text }`, async actor => { | ||
const cssSelector = await actor.answer(selector); | ||
const desiredText = await actor.answer(text); | ||
return BrowseTheWeb.as(actor).findByCssContainingText(cssSelector, desiredText); | ||
}); | ||
} | ||
static locatedById(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
return Question.about(f`page element located by id (${selector})`, async actor => { | ||
const idSelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findById(idSelector); | ||
}); | ||
} | ||
static locatedByTagName(tagName: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
return Question.about(f`page element located by tag name (${tagName})`, async actor => { | ||
const tagNameSelector = await actor.answer(tagName); | ||
return BrowseTheWeb.as(actor).findByTagName(tagNameSelector); | ||
}); | ||
} | ||
static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElement>> & Adapter<PageElement> { | ||
return Question.about(f`page element located by xpath (${selector})`, async actor => { | ||
const xpathSelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findByXPath(xpathSelector); | ||
}); | ||
} | ||
constructor( | ||
protected readonly context: () => Promise<NativeElementContext> | NativeElementContext, | ||
protected readonly locator: (root: NativeElementContext) => Promise<NativeElement> | NativeElement | ||
protected readonly selector: Selector<unknown>, | ||
private readonly locator: NativeElementLocator<Native_Element_Type>, | ||
) { | ||
} | ||
abstract of(parent: PageElement): PageElement; | ||
abstract of(parent: PageElement<Native_Element_Type>): PageElement<Native_Element_Type>; | ||
async nativeElement(): Promise<NativeElement> { | ||
async nativeElement(): Promise<Native_Element_Type> { | ||
try { | ||
const context = await this.context(); | ||
return this.locator(context); | ||
return this.locator.locate(this.selector); | ||
} | ||
@@ -78,2 +45,10 @@ catch (error) { | ||
nativeElementLocator(): NativeElementLocator<Native_Element_Type> { | ||
return this.locator; | ||
} | ||
toString(): string { | ||
return `PageElement located ${ this.selector }` | ||
} | ||
abstract enterValue(value: string | number | Array<string | number>): Promise<void>; | ||
@@ -93,6 +68,6 @@ abstract clearValue(): Promise<void>; | ||
abstract isClickable(): Promise<boolean>; | ||
abstract isDisplayed(): Promise<boolean>; | ||
abstract isEnabled(): Promise<boolean>; | ||
abstract isPresent(): Promise<boolean>; | ||
abstract isSelected(): Promise<boolean>; | ||
abstract isVisible(): Promise<boolean>; | ||
} |
@@ -1,70 +0,64 @@ | ||
import { Adapter, Answerable, format, LogicError, Question } from '@serenity-js/core'; | ||
import { Answerable, AnswersQuestions, format, List, Question, UsesAbilities } from '@serenity-js/core'; | ||
import { BrowseTheWeb } from '../abilities'; | ||
import { PageElement } from './PageElement'; | ||
import { Selector } from './selectors'; | ||
const d = format({ markQuestions: false }); | ||
const f = format({ markQuestions: true }); | ||
export abstract class PageElements<NativeElementContext = any, NativeElementList = any, NativeElement = any> | ||
// todo: implements List (Attribute.spec.ts) | ||
export class PageElements<Native_Element_Type = any> | ||
extends List<PageElement<Native_Element_Type>> | ||
{ | ||
static of(childElements: Answerable<PageElements>, parentElement: Answerable<PageElement>): Question<Promise<PageElements>> & Adapter<PageElements> { | ||
return Question.about(d `${ childElements } of ${ parentElement })`, async actor => { | ||
const children = await actor.answer(childElements); | ||
const parent = await actor.answer(parentElement); | ||
return children.of(parent); | ||
}); | ||
static located<NET, ST>(selector: Answerable<Selector<ST>>): PageElements<NET> { | ||
return new PageElements(selector); | ||
} | ||
static locatedByCss(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> { | ||
return Question.about(f `page elements located by css (${selector})`, async actor => { | ||
const value = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findAllByCss(value); | ||
}); | ||
/** | ||
* @param {Answerable<Selector<unknown>>} selector | ||
* @param {Answerable<PageElement>} [parent] | ||
* if not specified, browser root selector is used | ||
*/ | ||
constructor( | ||
protected readonly selector: Answerable<Selector<unknown>>, | ||
private readonly parent?: Answerable<PageElement> | ||
) { | ||
super(new PageElementCollection<Native_Element_Type>(selector, parent)); | ||
} | ||
static locatedByTagName(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> { | ||
return Question.about(f `page elements located by tag name (${selector})`, async actor => { | ||
const tagNameSelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findAllByTagName(tagNameSelector); | ||
}); | ||
of(parent: Answerable<PageElement<Native_Element_Type>>): PageElements<Native_Element_Type> { | ||
return new PageElements<Native_Element_Type>(this.selector, parent) | ||
.describedAs(`<<${this.toString()}>>` + f`.of(${ parent })`); | ||
} | ||
} | ||
static locatedByXPath(selector: Answerable<string>): Question<Promise<PageElements>> & Adapter<PageElements> { | ||
return Question.about(f `page elements located by xpath (${selector})`, async actor => { | ||
const xpathSelector = await actor.answer(selector); | ||
return BrowseTheWeb.as(actor).findAllByXPath(xpathSelector); | ||
}); | ||
} | ||
/** | ||
* @package | ||
*/ | ||
class PageElementCollection<Native_Element_Type = any> | ||
extends Question<Promise<Array<PageElement<Native_Element_Type>>>> | ||
{ | ||
private subject: string; | ||
constructor( | ||
protected readonly context: () => Promise<NativeElementContext> | NativeElementContext, | ||
protected readonly locator: (root: NativeElementContext) => Promise<NativeElementList> | NativeElementList | ||
private readonly selector: Answerable<Selector<unknown>>, | ||
private readonly parent?: Answerable<PageElement>, | ||
) { | ||
super(); | ||
this.subject = `page elements located ${ selector.toString() }`; | ||
} | ||
abstract of(parent: PageElement): PageElements; | ||
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Array<PageElement<Native_Element_Type>>> { | ||
const bySelector = await actor.answer(this.selector); | ||
const parent = await actor.answer(this.parent); | ||
async nativeElementList(): Promise<NativeElementList> { | ||
try { | ||
const context = await this.context(); | ||
return this.locator(context); | ||
} | ||
catch (error) { | ||
throw new LogicError(`Couldn't find elements`, error); | ||
} | ||
return BrowseTheWeb.as(actor).locateAll(bySelector, parent ? parent.nativeElementLocator() : undefined); | ||
} | ||
abstract count(): Promise<number>; | ||
abstract first(): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
abstract last(): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
abstract get(index: number): Promise<PageElement<NativeElementContext, NativeElement>>; | ||
describedAs(subject: string): this { | ||
this.subject = subject; | ||
return this; | ||
} | ||
abstract map<O>(fn: (element: PageElement, index?: number, elements?: PageElements) => Promise<O> | O): Promise<O[]>; | ||
abstract filter(fn: (element: PageElement, index?: number) => Promise<boolean> | boolean): PageElements; | ||
abstract forEach(fn: (element: PageElement, index?: number) => Promise<void> | void): Promise<void>; | ||
toString(): string { | ||
return this.subject; | ||
} | ||
} |
import { Answerable, AnswersQuestions, LogicError, Question } from '@serenity-js/core'; | ||
import { formatted } from '@serenity-js/core/lib/io'; | ||
import { PageElement, PageElements } from '../models'; | ||
/** | ||
@@ -48,3 +46,3 @@ * @desc | ||
*/ | ||
protected async resolve<T=PageElement|PageElements>( | ||
protected async resolve<T>( | ||
actor: AnswersQuestions, | ||
@@ -51,0 +49,0 @@ element: Answerable<T>, |
import { Answerable, Question } from '@serenity-js/core'; | ||
import { formatted } from '@serenity-js/core/lib/io'; | ||
import { PageElement, PageElements } from '../models'; | ||
import { By, PageElement, PageElements } from '../models'; | ||
import { Text } from './Text'; | ||
import { Value } from './Value'; | ||
@@ -57,3 +59,3 @@ /** | ||
static valueOf(pageElement: Answerable<PageElement>): Question<Promise<string>> { | ||
return PageElement.locatedByCss('option:checked') | ||
return PageElement.located(By.css('option:checked')) | ||
.of(pageElement) | ||
@@ -106,5 +108,5 @@ .value() | ||
static valuesOf(pageElement: Answerable<PageElement>): Question<Promise<string[]>> { | ||
return PageElements.locatedByCss('option:checked') | ||
return PageElements.located(By.css('option:checked')) | ||
.of(pageElement) | ||
.map(item => item.value()) | ||
.eachMappedTo(Value) | ||
.describedAs(formatted `values selected in ${ pageElement }`) as Question<Promise<string[]>>; | ||
@@ -158,3 +160,3 @@ } | ||
static optionIn(pageElement: Answerable<PageElement>): Question<Promise<string>> { | ||
return PageElement.locatedByCss('option:checked') | ||
return PageElement.located(By.css('option:checked')) | ||
.of(pageElement) | ||
@@ -210,7 +212,7 @@ .text() | ||
static optionsIn(pageElement: Answerable<PageElement>): Question<Promise<string[]>> { | ||
return PageElements.locatedByCss('option:checked') | ||
return PageElements.located(By.css('option:checked')) | ||
.of(pageElement) | ||
.map(item => item.text()) | ||
.eachMappedTo(Text) | ||
.describedAs(formatted `options selected in ${ pageElement }`) as Question<Promise<string[]>>; | ||
} | ||
} |
import { Adapter, Answerable, AnswersQuestions, createAdapter, MetaQuestion, Question, UsesAbilities } from '@serenity-js/core'; | ||
import { asyncMap } from '@serenity-js/core/lib/io'; | ||
@@ -107,3 +108,3 @@ import { PageElement, PageElements } from '../models'; | ||
*/ | ||
static ofAll(elements: Answerable<PageElements>): | ||
static ofAll(elements: PageElements): | ||
Question<Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent | ||
@@ -142,3 +143,3 @@ MetaQuestion<Answerable<PageElement>, Promise<string[]>> & // eslint-disable-line @typescript-eslint/indent | ||
{ | ||
constructor(private readonly elements: Answerable<PageElements>) { | ||
constructor(private readonly elements: PageElements) { | ||
super(`the text of ${ elements }`); | ||
@@ -148,9 +149,11 @@ } | ||
of(parent: Answerable<PageElement>): Question<Promise<string[]>> { | ||
return new TextOfMultipleElements(PageElements.of(this.elements, parent)); | ||
// todo: return Question.about directly? | ||
return new TextOfMultipleElements(this.elements.of(parent)); | ||
} | ||
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string[]> { | ||
const elements = await this.resolve(actor, this.elements); | ||
return elements.map(element => element.text()); | ||
const elements: PageElement[] = await this.resolve(actor, this.elements); | ||
return asyncMap(elements, element => element.text()); | ||
} | ||
} |
@@ -67,6 +67,4 @@ import { Stage } from '@serenity-js/core'; | ||
try { | ||
const [ screenshot, capabilities ] = await Promise.all([ | ||
browseTheWeb.takeScreenshot(), | ||
browseTheWeb.browserCapabilities(), | ||
]); | ||
const capabilities = await browseTheWeb.browserCapabilities(); | ||
const screenshot = await browseTheWeb.takeScreenshot(); | ||
@@ -111,4 +109,6 @@ const | ||
private shouldIgnore(error: Error) { | ||
return error.name | ||
&& (error.name === 'NoSuchSessionError'); | ||
return error.name && ( | ||
error.name === 'NoSuchSessionError' || | ||
(error.name === 'ProtocolError' && error.message.includes('Target closed')) | ||
); | ||
// todo: add SauceLabs | ||
@@ -115,0 +115,0 @@ // [0-0] 2021-12-02T01:32:36.402Z ERROR webdriver: Request failed with status 404 due to no such window: no such window: target window already closed |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
528876
290
11605
+ Added@serenity-js/assertions@3.0.0-rc.3(transitive)
+ Added@serenity-js/core@3.0.0-rc.3(transitive)
- Removed@serenity-js/assertions@3.0.0-rc.2(transitive)
- Removed@serenity-js/core@3.0.0-rc.1(transitive)
Updated@serenity-js/core@3.0.0-rc.3
Updatedtiny-types@^1.17.0