Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serenity/JS should provide a way to pass custom scroll options to the scrollIntoView() method #2000

Open
2 of 3 tasks
Himanshu21git opened this issue Oct 17, 2023 · 10 comments
Labels
enhancement A good idea that should be implemented @serenity-js/web Screenplay Pattern APIs for interacting with the Web

Comments

@Himanshu21git
Copy link
Contributor

What's the problem you're trying to solve?

The current functionality of scrollIntoView() places a page element at the top of the page. However, specific scenarios, like when a navigation bar overlaps the page's header section, can result in unintended consequences. In such instances, using scrollIntoView() may lead to the element being hidden by the navigation bar. As a consequence, when trying to click on the element, the click action could be intercepted by elements within the navigation bar. This predicament arises due to the absence of an option to customize scroll behavior within the method.

This scenario introduces difficulties in Serenity/JS interactions, as the default scrolling behavior can impede efficiency. More precisely, the element may not become accessible for interaction during the screenplay, leading to unsuccessful tasks and eventual test failures.

How would you like to solve it?

Offering users the capability to specify custom scroll options for the scrollIntoView() method, akin to what's available in other automated testing frameworks, stands as the favored solution. These options can be drawn from the MDN documentation.

Introducing comparable methods found in other testing frameworks would greatly benefit Serenity/JS. Furthermore, maintaining functionality closely aligned with native JavaScript would be an added advantage. Users of the framework would likely feel more at ease and accustomed to working with Serenity/JS.

Are there any alternatives?

I experimented with several alternative approaches but was unable to attain the desired outcome through any of them. Additionally, I'm interested in having the capability to craft custom activities, which would allow me to extend the functionality of the Click activity and experiment with modifying the behavior of the scrollIntoView method.

How can we make it happen?

@jan-molak
Copy link
Member

jan-molak commented Oct 17, 2023

I like this idea! However, some differences across the supported web integration tools make the implementation a bit more challenging.

You can see the first attempt at implementing this feature at #1235

Happy to guide you through raising a PR if this feature is something you'd like to pick up.

@jan-molak jan-molak added enhancement A good idea that should be implemented @serenity-js/web Screenplay Pattern APIs for interacting with the Web labels Oct 17, 2023
@Himanshu21git
Copy link
Contributor Author

Indeed, I'm interested in working on this. I would greatly appreciate your guidance in creating a custom interaction that mirrors the Click activity. This will allow me to explore and experiment with the scrollIntoView behavior within this custom activity, which I'll refer to as "CustomClick."

@jan-molak
Copy link
Member

jan-molak commented Oct 17, 2023

Serenity/JS interactions that accept custom configuration or overrides typically have a .using(configuration) method. For example:

await actor.attemptsTo(
  Send.a(GetRequest.to('/books/0-688-00230-7').using({ headers: { ... } }),
)

You might want to consider something like this:

Scroll.to(pageElement).using(configuration)

However, some differences between WebdriverIO, Protractor and Playwright might make deciding how this interaction should behave tricky.

For example:

Another interesting challenge is that the element.scrollIntoView API is not particularly well designed as it allows either a boolean or a configuration object (instead of just the configuration object).

Following that design could make Serenity/JS interaction look awkward:

Scroll.to(pageElement).using(true) // meh

However, since true is really just an alias for {block: "start", inline: "nearest"} and false means scrollIntoViewOptions: {block: "end", inline: "nearest"}, we could have some pre-set configurations to make people's lives easier:

Scroll.to(pageElement).using(Scroll.Top)
// where Scroll.Top == `{block: "start", inline: "nearest"}`

Alternatively, we could have alias methods on Scroll itself:

Scroll.toTopOf(pageElement)
// alias for Scroll.to(pageElement).using({block: "start", inline: "nearest"})

@Himanshu21git
Copy link
Contributor Author

Himanshu21git commented Oct 18, 2023

Hi @jan-molak,
While I'm currently working on this, could you propose an alternative solution for this issue, specially tailored for a Serenity + Webdriverio setup? I'm in the process of implementing Serenity/JS in one of my projects, and I've encountered an issue with the scrollIntoView method used within the Click interaction.

The problem is that scrollIntoView places the target element at the top of the page. Consequently, the element I want to click on gets positioned behind a navigation bar, and the click action is intercepted by two overlapping elements.

@jan-molak
Copy link
Member

jan-molak commented Oct 18, 2023

Certainly, you can use script injection:

import { Answerable, d, Interaction } from '@serenity-js/core'
import { BrowseTheWeb, PageElement } from '@serenity-js/web'

const ScrollToTopOf = (pageElement: Answerable<PageElement>) =>
  Interaction.where(d`#actor scrolls to top of ${ pageElement }`, async actor => {
    const element = await actor.answer(pageElement);
    const page = await BrowseTheWeb.as(actor).currentPage();

    await page.executeScript(
      function scrollIntoView(nativeElement: any) {
        nativeElement.scrollIntoView({block: "start", inline: "nearest"});
      },
      element,
    )
  })

@Himanshu21git
Copy link
Contributor Author

Certainly, you can use script injection:

import { Answerable, d, Interaction } from '@serenity-js/core'
import { BrowseTheWeb, PageElement } from '@serenity-js/web'

const ScrollToTopOf = (pageElement: Answerable<PageElement>) =>
  Interaction.where(d`#actor scrolls to top of ${ pageElement }`, async actor => {
    const element = await actor.answer(pageElement);
    const page = await BrowseTheWeb.as(actor).currentPage();

    await page.executeScript(
      function scrollIntoView(element: any) {
        element.scrollIntoView({block: "start", inline: "nearest"});
      },
      await field,
    )
  })

@jan-molak Would you mind providing clarification regarding the term 'field' in the code snippet you shared? My goal is to ensure that when the scrollIntoView method is employed, the element is centered within the viewport.

@jan-molak
Copy link
Member

jan-molak commented Oct 18, 2023

Sorry, it was a typo; I've updated the original snippet.
As you can see there, I use Page.executeScript to inject a function that then calls the native scrollIntoView, which you can parameterise as per the MDN docs.

@Himanshu21git
Copy link
Contributor Author

However, @jan-molak , this approach won't be sufficient for overriding the scrollIntoView() method used within the Click interaction. How can I address this? As you can observe from the source code of the Click Interaction here:

class Click extends PageElementInteraction_1.PageElementInteraction {
    /**
     * Instantiates this Interaction.
     *
     * @param pageElement
     *  The element to be clicked on
     */
    static on(pageElement) {
        return new Click(pageElement);
    }
    constructor(element) {
        super((0, core_1.d) `#actor clicks on ${element}`);
        this.element = element;
    }
    /**
     * @inheritDoc
     */
    async performAs(actor) {
        const element = await this.resolve(actor, this element);

        await element.scrollIntoView();  // I'm interested in customizing this scrolling behavior within the Click interaction
        await element.click();
    }
}

In this code, I'd like to emphasize my intent to customize the scrolling behavior within the Click interaction, which is not achievable using the previous approach. How should I proceed?

@jan-molak
Copy link
Member

jan-molak commented Oct 18, 2023

Click relies on whatever a given web integration tool considers the "default scrolling behaviour". If you'd like to use a different behaviour, then you could introduce custom interactions and then compose them into a task:

const MyClick = (pageElement: Answerable<PageElement>) =>
  Interaction.where(d`#actor clicks on ${pageElement}`, async actor => {
    const element = await actor.answer(pageElement);
    // note no scrolling
    await element.click();
  })

const MyScrollAndClick = (element: Answerable<PageElement>) => 
  Task.where(d`#actor clicks on ${ element }`,
    Scroll.toCentreOf(element), // as discussed previously
    MyClick(element),
  )

Another alternative is to:

  • make the scrolling behaviour of the built-in Click configurable
  • override the default behaviour of web integration tools to provide different defaults
  • or remove scrolling behaviour from Click altogether, relying instead on developers to add Scroll when needed

@Himanshu21git
Copy link
Contributor Author

Click relies on whatever a given web integration tool considers the "default scrolling behaviour". If you'd like to use a different behaviour, then you could introduce custom interactions and then compose them into a task:

const MyClick = (pageElement: Answerable<PageElement>) =>
  Interaction.where(d`#actor clicks on ${pageElement}`, async actor => {
    const element = await actor.answer(pageElement);
    // note no scrolling
    await element.click();
  })

const MyScrollAndClick = (element: Answerable<PageElement>) => 
  Task.where(d`#actor clicks on ${ element }`,
    Scroll.toCentreOf(element), // as discussed previously
    MyClick(element),
  )

Another alternative is to:

  • make the scrolling behaviour of the built-in Click configurable
  • override the default behaviour of web integration tools to provide different defaults
  • or remove scrolling behaviour from Click altogether, relying instead on developers to add Scroll when needed

Yes, it works. Thanks @jan-molak 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A good idea that should be implemented @serenity-js/web Screenplay Pattern APIs for interacting with the Web
Projects
None yet
Development

No branches or pull requests

2 participants