Serenity-js: Question: On Silent operations like frame switching and waits : Serenity 2.0

Created on 5 Nov 2019  路  5Comments  路  Source: serenity-js/serenity-js

Then(/^(?:he|she|they) should be able to navigate to intervention page$/, function (this: WithStage) {
    FrameSwitch.switchFrame();
    return this.stage.theActorInTheSpotlight().attemptsTo(
      Wait.for(Duration.ofSeconds(10)),
      Ensure.that((SearchControls.interventionLable), equals("Search")));
  });

How to handle the Interactions that we don't want to show in reports, like in above scenario we need not to show Wait in reports, can you please shed light on how we should implement interactions in such scenarios.

question

Most helpful comment

hi @jan-molak - what do you think about the idea of having seamless iframe switching in serenity-js as in the case in serenity-core? I personally think that having to manually switch iframes before referring to an element is rather a chore and is better modelled by adding iframes to the required targets and letting serenity do this for you? It's about time I brewed up a PR for you 馃槃

All 5 comments

Hi there!

I'd generally recommend reporting all the interactions that take place, as this makes tests much easier to debug.

Let's consider a scenario as per your example. Assuming that all goes well and the intervention page loads correctly, we don't want to report the Wait because (I'm assuming?) we don't want to "pollute" the report.

But what if the Wait times out? Should it be reported then?
And what if because of this and other Waits in the scenario, the entire scenario times out? How would we know what caused it? How would we know what to report?

What I'd suggest is to introduce a higher-level Task that encapsulates what you'd like the actor to do. This will make the report cleaner and the code easier to re-use.

How about:

import { Duration, Task, Wait } from '@serenty-js/core';
import { Wait, isVisible } from '@serenity-js/protractor';
import { Ensure, equals } from '@serenity-js/assertions';

const EnsureThatSearchIsAvailable = () =>
    Task.where(`#actor ensures that search is available`,
        Wait.for(Duration.ofSeconds(10)),
        Ensure.that(SearchControls.interventionLabel, equals("Search")));
    );

We can now improve this further; Wait.for(Duration.ofSeconds(10)) will make the actor wait for 10 seconds even though the SearchControls.interventionLabel might be visible sooner.

You might want to consider replacing Wait.for with Wait.until:

import { Duration, Task, Wait } from '@serenty-js/core';
import { Wait } from '@serenity-js/protractor';
import { Ensure, equals } from '@serenity-js/assertions';

const EnsureThatSearchIsAvailable = () =>
    Task.where(`#actor ensures that search is available`,
        Wait.upTo(Duration.ofSeconds(10)).until(SearchControls.interventionLabel, isVisible()),
        Ensure.that(SearchControls.interventionLabel, equals("Search")));
    );

With the EnsureThatSearchIsAvailable task in place, we can now express the Cucumber step as follows:

Then(/^(?:he|she|they) should be able to navigate to intervention page$/, function (this: WithStage) {
    FrameSwitch.switchFrame();
    return this.stage.theActorInTheSpotlight().attemptsTo(
      EnsureThatSearchIsAvailable(),
    );
  });

Does this answer your question?

By the way, it might be nice to turn FrameSwitch.switchFrame() into a Serenity/JS interaction :-)

hi @jan-molak - what do you think about the idea of having seamless iframe switching in serenity-js as in the case in serenity-core? I personally think that having to manually switch iframes before referring to an element is rather a chore and is better modelled by adding iframes to the required targets and letting serenity do this for you? It's about time I brewed up a PR for you 馃槃

@nbarrett - that would be very nice indeed, old friend 馃槃

I was thinking of separating the logic of switching the frames from the one responsible for targetting the elements, if possible.

So something like

const someIFrame = Target.the('iframe').located(by.tagName('iframe'));
const someButtonWithinTheIFrame = Target.the('button').located(by.id('some-button'));

actor.attemptsTo(
    SwitchFrame.to(SomeIFrame),
    Click.on(someButtonWithinTheIFrame),
)

I agree it's an additional line or two of code, but those could be nicely wrapped into a self-contained task:

const DoStuffWithIframe = () =>
    Task.where(`#actor does stuff with the iFrame`,
        SwitchFrame.to(SomeIFrame),               // get in
        Click.on(someButtonWithinTheIFrame),      // do stuff
        SwitchFrame.toDefaultContent(),           // get out
    );

Happy to discuss it in more detail in a separate ticket :-)

Hmm, in my experience, the problem with switching iframes is that it can get quite complex when your page has multiple nested frames. You are only able to switch to an iframe that is a child of the currently selected iframe (or the default content if the element is not in an iframe). If you don鈥檛 get the switching sequence exactly right you鈥檒l get NoSuchFrameException. Conversely, if you try to refer to an element and you haven鈥檛 switched to the right iframe beforehand, you鈥檒l get NoSuchElementException. All of this puts a lot of burden on the writer of the test, who in the latter case might think that they need to wait longer for the element to appear when in fact they are just in the wrong iframe! It鈥檚 quite possible that in a test you might want to refer to a web element that is buried deep in a nested iframe, followed by a reference to one that鈥檚 not in an inframe at all. If you leave responsibility for this to the writer of the test, they would need to keep track of which iframe they were last on, in order to know whether a switch to default content is first required before they then switch to the child iframe. All this can get extremely messy, fast and clutters our beautiful test!. Another problem is that a Task might work fine at one point in a test but might fail later on because another Task switched to a different Iframe beforehand.

Given that the relationship between a web element and its iframe(s) never changes, to me it makes sense to define this relationship up-front within the Target, just like you would do with the locator.

In the java Serenity core library, an IFrame is an Optional type on the Target, so by default, a Target will exist in default content. An IFrame is constructed with a vararg of By locators which allows any nesting depth to be modelled. Switching of the IFrame is done transparently by means of the iFrameswitcher which is invoked within the TargetResolver. I relied heavily on the transparent Iframe switching capability when I worked with one particular client for a couple of years and I was very glad this feature was in serenity-core!

Given this background of how the problem was tackled in the java product, do you think this is something that could be of value in serenity-js?

Let's move the conversation about frames to #366.

I'll close this ticket as I believe I've answered the question, @rajeshwarp?

Was this page helpful?
0 / 5 - 0 ratings