Skip to content

Commit

Permalink
feat(interaction): make scroll interaction configurable
Browse files Browse the repository at this point in the history
now it is possible to set where to place the element to be scrolled within the visible area
  • Loading branch information
damonpam committed Jun 21, 2022
1 parent dda2a6e commit 5245870
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 15 deletions.
88 changes: 78 additions & 10 deletions integration/web-specs/spec/screenplay/interactions/Scroll.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, isGreaterThan, isLessThan } from '@serenity-js/assertions';
import { Ensure, equals } from '@serenity-js/assertions';
import { actorCalled } from '@serenity-js/core';
import { By, ExecuteScript, LastScriptExecution, Navigate, PageElement, Scroll } from '@serenity-js/web';

Expand All @@ -10,26 +10,94 @@ describe('Scroll', function () {

const Page = {
Execute_Button: PageElement.located(By.id('cast')).describedAs('the "Cast!" button'),
Body: PageElement.located(By.tagName('body'))
};

before(() => actorCalled('Gandalf').attemptsTo(
Navigate.to('/screenplay/interactions/scroll/long_page.html')
));

afterEach(() => actorCalled('Gandalf').attemptsTo(
Scroll.to(Page.Body)
));

/** @test {Scroll.to} */
it('allows the actor to scroll to a given target so that it appears in the viewport', () =>
actorCalled('Gandalf').attemptsTo(
Navigate.to('/screenplay/interactions/scroll/long_page.html'),

ExecuteScript.sync(`return arguments[0].getBoundingClientRect().top;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), isGreaterThan(9000)),
Ensure.that(LastScriptExecution.result<number>(), equals(10000)),

Scroll.to(Page.Execute_Button),

ExecuteScript.sync(`return arguments[0].getBoundingClientRect().top;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), isLessThan(9000)),
Ensure.that(LastScriptExecution.result<number>(), equals(0))
));

/** @test {Scroll.to} */
/** @test {Scroll#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(Scroll.to(Page.Execute_Button).toString())
.to.equal(`#actor scrolls to the "Cast!" button`);
/** @test {Scroll.alignedTo(true)} */
it('allows the actor to scroll to a given target so that it appears in the top of the viewport', () =>
actorCalled('Gandalf').attemptsTo(
ExecuteScript.sync(`return arguments[0].getBoundingClientRect().top;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(10000)),

Scroll.alignedTo(true).to(Page.Execute_Button),

ExecuteScript.sync(`return arguments[0].getBoundingClientRect().top;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(0))
));

it('allows the actor to scroll to a given target so that it appears in the bottom of the viewport', () =>
actorCalled('Gandalf').attemptsTo(
ExecuteScript.sync(`return arguments[0].getBoundingClientRect().bottom;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(10021)),

Scroll.alignedTo(false).to(Page.Execute_Button),

ExecuteScript.sync(`return arguments[0].getBoundingClientRect().bottom;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(21))
));

it('allows the actor to scroll to a given target so that it appears in the bottom of the viewport (using options object)', () =>
actorCalled('Gandalf').attemptsTo(
ExecuteScript.sync(`return arguments[0].getBoundingClientRect().bottom;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(10021)),

Scroll.alignedTo({ block: 'end' }).to(Page.Execute_Button),

ExecuteScript.sync(`return arguments[0].getBoundingClientRect().bottom;`).withArguments(Page.Execute_Button),
Ensure.that(LastScriptExecution.result<number>(), equals(21))
));

describe('toString', function () {
/** @test {Scroll.to} */
/** @test {Scroll#toString} */
it('provides a sensible description of the interaction being performed', () => {
expect(Scroll.to(Page.Execute_Button).toString())
.to.equal(`#actor scrolls to the "Cast!" button`);
});

/** @test {Scroll.alignedTo} */
/** @test {Scroll#toString} */
it('provides a sensible description of the interaction being performed when the element is aligned to the top', () => {
expect(Scroll.alignedTo(true).to(Page.Execute_Button).toString())
.to.equal(`#actor scrolls to the 'top' of the "Cast!" button aligned to the 'top' of the visible area of the scrollable ancestor`);
});

/** @test {Scroll.alignedTo} */
/** @test {Scroll#toString} */
it('provides a sensible description of the interaction being performed when the element is aligned to the bottom', () => {
expect(Scroll.alignedTo(false).to(Page.Execute_Button).toString())
.to.equal(`#actor scrolls to the 'bottom' of the "Cast!" button aligned to the 'bottom' of the visible area of the scrollable ancestor`);
});

/** @test {Scroll.alignedTo} */
/** @test {Scroll#toString} */
it('provides a sensible description of the interaction being performed when the element is aligned using specific configuration', () => {
expect(Scroll.alignedTo({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
}).to(Page.Execute_Button).toString())
.to.equal(`#actor scrolls to the "Cast!" button with the following view options '{"behavior":"smooth","block":"end","inline":"nearest"}'`);
});
});
});
2 changes: 1 addition & 1 deletion packages/protractor/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["es2019"],
"lib": ["es2019", "dom"],
"module": "commonjs",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
Expand Down
79 changes: 75 additions & 4 deletions packages/web/src/screenplay/interactions/Scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@seren
import { formatted } from '@serenity-js/core/lib/io';

import { PageElement } from '../models';
import { ScrollBuilder } from './ScrollBuilder';

/**
* @desc
Expand Down Expand Up @@ -37,6 +38,30 @@ import { PageElement } from '../models';
* Ensure.that(Form.submitButton, isVisible()),
* );
*
* @example <caption>Scrolling to element view at the bottom</caption>
* import { actorCalled } from '@serenity-js/core';
* import { Ensure } from '@serenity-js/assertions';
* import { BrowseTheWeb, Scroll, isVisible } from '@serenity-js/webdriverio';
*
* actorCalled('Sara')
* .whoCan(BrowseTheWeb.using(browser))
* .attemptsTo(
* Scroll.alignedToTop(false).to(Form.submitButton),
* Ensure.that(Form.submitButton, isVisible()),
* );
*
* @example <caption>Scrolling to element view at the bottom</caption>
* import { actorCalled } from '@serenity-js/core';
* import { Ensure } from '@serenity-js/assertions';
* import { BrowseTheWeb, Scroll, isVisible } from '@serenity-js/webdriverio';
*
* actorCalled('Sara')
* .whoCan(BrowseTheWeb.using(browser))
* .attemptsTo(
* Scroll.alignedToTop({ behavior: "smooth", block: "end", inline: "nearest" }).to(Form.submitButton),
* Ensure.that(Form.submitButton, isVisible()),
* );
*
* @see {@link BrowseTheWeb}
* @see {@link Target}
* @see {@link isVisible}
Expand All @@ -51,19 +76,56 @@ export class Scroll extends Interaction {
* Instantiates this {@link @serenity-js/core/lib/screenplay~Interaction}.
*
* @param {Answerable<PageElement>} target
* The element to be scroll to
* The element to be scrolled to
*
* @returns {@serenity-js/core/lib/screenplay~Interaction}
*/
static to(target: Answerable<PageElement>): Scroll {
return new Scroll(target);
}

/**
* @desc
* Instantiates a version of this {@link @serenity-js/core/lib/screenplay~Interaction}
* configured to align the element into specific part of the viewport
*
* @param {boolean | ScrollIntoViewOptions} alignedToTop
* A boolean value:
* If true, the top of the element will be aligned to the top of the visible area of the scrollable ancestor.
* Corresponds to scrollIntoViewOptions: {block: "start", inline: "nearest"}. This is the default value.
*
* If false, the bottom of the element will be aligned to the bottom of the visible area of the scrollable ancestor.
* Corresponds to scrollIntoViewOptions: {block: "end", inline: "nearest"}.
*
* An Object with the following properties:
* behavior (optional)
* Defines the transition animation. One of auto or smooth. Defaults to auto.
*
* block (optional)
* Defines vertical alignment. One of start, center, end, or nearest. Defaults to start.
*
* inline (optional)
* Defines horizontal alignment. One of start, center, end, or nearest. Defaults to nearest.
*
* @returns {ScrollBuilder}
*/
static alignedTo(alignedToTop: boolean | ScrollIntoViewOptions): ScrollBuilder {
return {
to: (target: Answerable<PageElement>): Interaction =>
new Scroll(target, alignedToTop)
};
}

/**
* @param {Answerable<PageElement>} target
* The element to be scroll to
* The element to be scrolled to
* @param scrollIntoViewOptions
* The options to place the element in the visible area
*/
constructor(private readonly target: Answerable<PageElement>) {
constructor(
private readonly target: Answerable<PageElement>,
private readonly scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
) {
super();
}

Expand Down Expand Up @@ -94,6 +156,15 @@ export class Scroll extends Interaction {
* @returns {string}
*/
toString(): string {
return formatted `#actor scrolls to ${ this.target }`;
if (typeof this.scrollIntoViewOptions === 'boolean') {
const alignedTo = this.scrollIntoViewOptions ? 'top' : 'bottom';
return formatted`#actor scrolls to the ${alignedTo} of ${this.target} aligned to the ${alignedTo} of the visible area of the scrollable ancestor`;
}

if (typeof this.scrollIntoViewOptions === 'object') {
return formatted`#actor scrolls to ${this.target} with the following view options ${JSON.stringify(this.scrollIntoViewOptions)}`;
}

return formatted`#actor scrolls to ${this.target}`;
}
}
28 changes: 28 additions & 0 deletions packages/web/src/screenplay/interactions/ScrollBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Answerable, Interaction } from '@serenity-js/core';

import { PageElement } from '../models';

/**
* @desc
* Fluent interface to make the instantiation of
* the {@link @serenity-js/core/lib/screenplay~Interaction}
* to {@link Scroll} more configurable.
*
* @see {@link Scroll}
*
* @interface
*/
export interface ScrollBuilder {

/**
* @desc
* Instantiates an {@link @serenity-js/core/lib/screenplay~Interaction}
* to {@link Scroll}.
*
* @param {Answerable<PageElement>} target
* @returns {@serenity-js/core/lib/screenplay~Interaction}
*
* @see {@link Target}
*/
to(target: Answerable<PageElement>) : Interaction;
}

0 comments on commit 5245870

Please sign in to comment.