serenity-bdd / the-serenity-book

A book about Serenity BDD
40 stars 58 forks source link

problems modeling complex controls #31

Open SiKing opened 4 years ago

SiKing commented 4 years ago

I am having problems modelling slightly complex controls, and the documentation has only trivial buttons and links.

I have a situation like this:

  1. Our app has navigation tabs along the top of the page for different sections of the app. As an example, we can use stackoverflow.com and the tabs (buttons?) along the top: "Interesting", "Bountied", "Hot", "Week", "Month".
  2. I wanted to have one method, to which I can pass the name of the tab as a param. Something like NavigateTo.page(String), as in actor.attemptsTo(NavigateTo.page("Interesting")).

As a hint I was able to find this, but it is not clear where the find(...) static import comes from.

I constantly find myself struggling to model something in Screenplay, that I used to whip up without a second thought using PageObjects:

public class NavigateTo implements Task {

    private String pageLabel;

    public NavigateTo(String pageLabel) {
    this.pageLabel = pageLabel;
    }

    @Step("{0} navigates to page #pageLabel")
    @Override
    public <T extends Actor> void performAs(T actor) {
    // TODO: all this should be some kind of PageObject thing?
    List<WebElementFacade> labels = BrowseTheWeb.as(actor).findAll(By.className("s-btn"));
    for (WebElementFacade label : labels) {
        if (label.getText().contains(pageLabel)) {
        actor.attemptsTo(Click.on(label));
        return;
        }
    }
    throw new NavigationPageNotFound(pageLabel);
    }

    public static NavigateTo page(String pageLabel) {
    return Instrumented.instanceOf(NavigateTo.class).withProperties(pageLabel);
    }
}

Can you help?

globalworming commented 4 years ago

find is a method of the PageObject. extract() though I can't find anywhere, maybe the docs are outdated? find only makes sense when you do actions via page objects and you don't really want that. Best practice is to decouple actions from page objects. see https://johnfergusonsmart.com/beyond-page-objects-liberate-chains-ui-think/ and https://www.infoq.com/articles/Beyond-Page-Objects-Test-Automation-Serenity-Screenplay/

I would propose something like this:

... actor.attemptsTo(LookupQuestions.labeled("Hot")) ...

class LookupQuestions extends Performable { ...
    actor.attemptsTo(Open.browserOn(new QuestionsPage()))
    actor.attemptsTo(Click.on(QuestionsPage.displayByLabel("Hot"))) ...

@DefautlUrl("https://stackoverflow.com/")
class QuestionsPage extends PageObject {
  static Target displayByLabel(String s) {
    return Target.the("button labeled" + s).locatedBy("some css locator using " +s)
wakaleo commented 4 years ago

You could also wrap the CSS locators in enums and have an enum value for each tab.

SiKing commented 4 years ago

I eventually came up with:

public class NavigateToSection implements Task {

    private String label;

    public NavigateToSection(String label) {
    this.label = label;
    }

    @Step("{0} navigates to section '#label'")
    @Override
    public <T extends Actor> void performAs(T actor) {
    actor.should(seeThat(the(Section.labeled(label)), isCurrentlyVisible()).orComplainWith(NavigationSectionNotFound.class, label));
    actor.attemptsTo(Click.on(Section.labeled(label)));
    }

    public static NavigateToSection labeled(String pageLabel) {
    return Instrumented.instanceOf(NavigateToSection.class).withProperties(pageLabel);
    }
}
public class Section {

    public static Target labeled(String label) {

    return Target.the("application section " + label).locatedBy("//a[contains(., '" + label + "')]");
    }
}

This seems to work for my use-case, for now. I seem to be struggling more with how to express what I want in English and where are all the bits and pieces. I refactor almost everything every day, as I build up more stuff. That whole Open-Closed Principle is completely not working for me. :(