Skip to content
Permalink
Browse files
fix(protractor): cleaned up the API docs and introduced interfaces to…
… simplify method signatures
  • Loading branch information
jan-molak committed Mar 8, 2020
1 parent 71c451e commit 8e85a54452dfb79ca04d94fa1d81e295be2be3ae
Showing 7 changed files with 219 additions and 4 deletions.
@@ -5,7 +5,7 @@ import { by } from 'protractor';
import { Navigate, Target, Text } from '../../../src';
import { pageFromTemplate } from '../../fixtures';

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

const shoppingListPage = pageFromTemplate(`
@@ -37,24 +37,40 @@ describe('Target', () => {

describe('allows the actor to locate', () => {

/**
* @test {Target}
* @test {TargetElement}
*/
it('a single web element matching the selector', () => actorCalled('Bernie').attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.of(ShoppingList.Header), equals('Shopping list')),
));

/**
* @test {Target}
* @test {TargetElements}
*/
it('all web elements matching the selector', () => actorCalled('Bernie').attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.ofAll(ShoppingList.Items), contain('oats')),
));

/**
* @test {Target}
* @test {TargetNestedElement}
*/
it('an element relative to another target', () => actorCalled('Bernie').attemptsTo(
Navigate.to(shoppingListPage),

Ensure.that(Text.of(ShoppingList.Number_Of_Items_Left), equals('2')),
));

/**
* @test {Target}
* @test {TargetNestedElements}
*/
it('all elements relative to another target', () => actorCalled('Bernie').attemptsTo(
Navigate.to(shoppingListPage),

@@ -66,32 +82,56 @@ describe('Target', () => {

describe('an element that', () => {

/**
* @test {Target}
* @test {TargetElement}
*/
it('is being targeted', () => {
expect(ShoppingList.Header.toString())
.to.equal('the header');
});

/**
* @test {Target}
* @test {TargetElement}
*/
it('has been located', () => {
expect(ShoppingList.Header.answeredBy(actorCalled('Bernie')).toString())
.to.equal('the header');
});

/**
* @test {Target}
* @test {TargetNestedElement}
*/
it('is nested', () =>
expect(ShoppingList.Number_Of_Items_Left.answeredBy(actorCalled('Bernie')).toString())
.to.equal('the number of items left of the progress bar of the shopping list app'));
});

describe('elements that', () => {

/**
* @test {Target}
* @test {TargetElements}
*/
it('are being targeted', () => {
expect(ShoppingList.Items.toString())
.to.equal('the items of the shopping list app');
});

/**
* @test {Target}
* @test {TargetElements}
*/
it('have been located', () =>
expect(ShoppingList.Items.answeredBy(actorCalled('Bernie')).toString())
.to.equal('the items of the shopping list app'));

/**
* @test {Target}
* @test {TargetNestedElements}
*/
it('are nested', () =>
expect(ShoppingList.Bought_Items.answeredBy(actorCalled('Bernie')).toString())
.to.equal('the bought items of the shopping list'));
@@ -124,20 +164,32 @@ describe('Target', () => {
static Topics = Target.all('topics').located(by.css('li'));
}

/**
* @test {Target}
* @test {TargetNestedElement}
*/
it('allows for Target<ElementFinder> to be nested within another Target<ElementFinder>', () => actorCalled('Bernie').attemptsTo(
Navigate.to(pageWithNestedTargets),

Ensure.that(Text.of(Page.Header.of(Page.Article)), equals('Title')),
Ensure.that(Page.Header.of(Page.Article).toString(), equals('the header of the article')),
));

/**
* @test {Target}
* @test {TargetNestedElement}
*/
it('allows for Target<ElementFinder> to form a chain with other Target<ElementFinder>s', () => actorCalled('Bernie').attemptsTo(
Navigate.to(pageWithNestedTargets),

Ensure.that(Text.of(Page.Title.of(Page.Header).of(Page.Article)), equals('Title')),
Ensure.that(Page.Title.of(Page.Header.of(Page.Article)).toString(), equals('the title of the header of the article')),
));

/**
* @test {Target}
* @test {TargetNestedElements}
*/
it('allows for Target<ElementArrayFinder> to be nested within another Target<ElementFinder>', () => actorCalled('Bernie').attemptsTo(
Navigate.to(pageWithNestedTargets),

@@ -1,15 +1,99 @@
import { Question } from '@serenity-js/core';
import { ElementFinder, Locator } from 'protractor';
import { NestedTargetBuilder, TargetBuilder } from './builders';
import { TargetElement } from './TargetElement';
import { TargetElements } from './TargetElements';
import { TargetNestedElement } from './TargetNestedElement';
import { TargetNestedElements } from './TargetNestedElements';

/**
* @public
* @desc
* Provides a convenient way to retrieve a single web element or multiple web elements,
* so that they can be used with Serenity/JS {@link @serenity-js/core/lib/screenplay~Interaction}s.
*
* Check out the examples below, as well as the unit tests demonstrating the usage.
*
* @example <caption>Imaginary website under test</caption>
* <body>
* <ul id="basket">
* <li><a href="#">Apple</a></li>
* <li><a href="#">Banana</a></li>
* <li><a href="#">Coconut</a></li>
* </ul>
* <div id="summary"><strong class="out-of-stock">Coconut</strong> is not available</div>
* <button type="submit">Proceed to Checkout</button>
* </body>
*
* @example <caption>Locating a single element</caption>
* import { Target } from '@serenity-js/protractor';
* import { by } from 'protractor';
*
* const proceedToCheckoutButton =
* Target.the('Proceed to Checkout button').located(by.css(`button[type='submit']`));
*
* @example <caption>Locating multiple elements</caption>
* import { Target } from '@serenity-js/protractor';
* import { by } from 'protractor';
*
* const basketItems =
* Target.all('items in the basket').located(by.css('ul#basket li'));
*
* @example <caption>Locating element relative to another element</caption>
* import { Target } from '@serenity-js/protractor';
* import { by } from 'protractor';
*
* const summary =
* Target.the('summary').located(by.id('message'));
*
* const outOfStockItem =
* Target.the('out of stock item').of(summary).located(by.css('.out-of-stock'))
*
* @example <caption>Clicking on an element</caption>
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, Click } from '@serenity-js/protractor';
* import { protractor } from 'protractor';
*
* actorCalled('Jane')
* .whoCan(BrowseTheWeb.using(protractor.browser))
* .attemptsTo(
* Click.on(proceedToCheckoutButton),
* );
*
* @example <caption>Retrieving text of multiple elements and performing an assertion</caption>
* import { Ensure, contain } from '@serenity-js/assertions';
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, Click, Text } from '@serenity-js/protractor';
* import { protractor } from 'protractor';
*
* const basketItemNames = Text.ofAll(basketItems);
*
* actorCalled('Jane')
* .whoCan(BrowseTheWeb.using(protractor.browser))
* .attemptsTo(
* Ensure.that(basketItemNames, contain('Apple'))
* );
*
* @example <caption>Waiting on an element</caption>
* import { actorCalled } from '@serenity-js/core';
* import { BrowseTheWeb, Click, Text, Wait, isClickable } from '@serenity-js/protractor';
* import { protractor } from 'protractor';
*
* actorCalled('Jane')
* .whoCan(BrowseTheWeb.using(protractor.browser))
* .attemptsTo(
* Wait.until(proceedToCheckoutButton, isClickable()),
* );
*/
export class Target {
static the(name: string) {

/**
* @desc
* Locates a single web element
*
* @param {string} name - A human-readable name of the element to be used in the report
* @returns {TargetBuilder<TargetElement> & NestedTargetBuilder<TargetNestedElement>}
*/
static the(name: string): TargetBuilder<TargetElement> & NestedTargetBuilder<TargetNestedElement> {
return {
located: (byLocator: Locator): TargetElement =>
new TargetElement(name, byLocator),
@@ -23,7 +107,14 @@ export class Target {
};
}

static all(name: string) {
/**
* @desc
* Locates a group of web elements
*
* @param {string} name - A human-readable name of the elements to be used in the report
* @returns {TargetBuilder<TargetElements> & NestedTargetBuilder<TargetNestedElements>}
*/
static all(name: string): TargetBuilder<TargetElements> & NestedTargetBuilder<TargetNestedElements> {
return {
located: (byLocator: Locator): TargetElements =>
new TargetElements(name, byLocator),
@@ -6,17 +6,32 @@ import { override } from './override';
import { TargetNestedElement } from './TargetNestedElement';

/**
* @desc
* Locates a single web element.
* Instead of using this class directly, please use {@link Target.the} instead.
*
* @public
* @see {@link Target}
*/
export class TargetElement
implements Question<ElementFinder>, RelativeQuestion<Question<ElementFinder> | ElementFinder, ElementFinder>
{
/**
* @desc
*
* @param {string} description - A human-readable description to be used in the report
* @param {protractor~Locator} locator - A locator to be used when locating the element
*/
constructor(
protected readonly description: string,
protected readonly locator: Locator,
) {
}

/**
* @param {Question<ElementFinder> | ElementFinder} parent
* @returns {TargetNestedElement}
*/
of(parent: Question<ElementFinder> | ElementFinder) {
return new TargetNestedElement(parent, this);
}
@@ -6,17 +6,33 @@ import { override } from './override';
import { TargetNestedElements } from './TargetNestedElements';

/**
* @desc
* Locates a group of web element.
* Instead of using this class directly, please use {@link Target.all} instead.
*
* @public
* @see {@link Target}
*/
export class TargetElements
implements Question<ElementArrayFinder>, RelativeQuestion<Question<ElementFinder> | ElementFinder, ElementArrayFinder>
{

/**
* @desc
*
* @param {string} description - A human-readable description to be used in the report
* @param {protractor~Locator} locator - A locator to be used when locating the element
*/
constructor(
private readonly description: string,
private readonly locator: Locator,
) {
}

/**
* @param {Question<ElementFinder> | ElementFinder} parent
* @returns {TargetNestedElements}
*/
of(parent: Question<ElementFinder> | ElementFinder) {
return new TargetNestedElements(parent, this);
}
@@ -5,17 +5,33 @@ import { RelativeQuestion } from '../RelativeQuestion';
import { override } from './override';

/**
* @desc
* Locates a single web element located within another web element.
* Instead of using this class directly, please use {@link Target.the} instead.
*
* @public
* @see {@link Target}
*/
export class TargetNestedElement
implements Question<ElementFinder>, RelativeQuestion<Question<ElementFinder> | ElementFinder, ElementFinder>
{

/**
* @desc
*
* @param {Question<ElementFinder> | ElementFinder} parent
* @param {Question<ElementFinder> | ElementFinder} child
*/
constructor(
private readonly parent: Question<ElementFinder> | ElementFinder,
private readonly child: Question<ElementFinder> | ElementFinder,
) {
}

/**
* @param {Question<ElementFinder> | ElementFinder} parent
* @returns {TargetNestedElement}
*/
of(parent: Question<ElementFinder> | ElementFinder) {
return new TargetNestedElement(parent, this);
}
@@ -5,18 +5,33 @@ import { RelativeQuestion } from '../RelativeQuestion';
import { override } from './override';

/**
* @desc
* Locates a group of web element located within another web element.
* Instead of using this class directly, please use {@link Target.all} instead.
*
* @public
* @see {@link Target}
*/
export class TargetNestedElements
implements Question<ElementArrayFinder>, RelativeQuestion<Question<ElementFinder> | ElementFinder, ElementArrayFinder>
{

/**
* @desc
*
* @param {Question<ElementFinder> | ElementFinder} parent
* @param {Question<ElementArrayFinder> | ElementArrayFinder} children
*/
constructor(
private readonly parent: Question<ElementFinder> | ElementFinder,
private readonly children: Question<ElementArrayFinder> | ElementArrayFinder,
) {
}

/**
* @param {Question<ElementFinder> | ElementFinder} parent
* @returns {TargetNestedElements}
*/
of(parent: Question<ElementFinder> | ElementFinder) {
return new TargetNestedElements(parent, this);
}

0 comments on commit 8e85a54

Please sign in to comment.