playwright-bdd icon indicating copy to clipboard operation
playwright-bdd copied to clipboard

Bug: Inheritance Issue when more than one child is issue in the same scenario

Open jzaratei opened this issue 1 year ago • 5 comments

Given Scenario: Add todos When I add todo "foo" And I add todo "bar" Then visible todos count is 2

When I create multiple child classes for todo page: AdminTodoPage, OtherTodoPage

export
@Fixture<typeof test>("adminTodoPage")
class AdminTodoPage extends TodoPage {
  constructor(public page: Page) {
    super(page);
  }

  @When("I add todo {string}")
  async addToDo(text: string) {
    await this.inputBox.fill(text);
    await this.inputBox.press("Enter");
  }
}
export
@Fixture<typeof test>("otherTodoPage")
class OtherTodoPage extends TodoPage {
  constructor(public page: Page) {
    super(page);
  }

  @When("I complete todo {string}")
  async completeTodo(hasText: string) {
    const checkbox = this.todoItems.filter({ hasText }).getByRole("checkbox");
    if (!(await checkbox.isChecked())) {
      await checkbox.click();
    }
  }
}

Then I run npx bddgen and I get

Error: Can't guess fixture for decorator step "I filter todos as "Completed"" in file: features\todopage.feature. Please refactor your Page Object classes or set one of the following tags: @fixture:adminTodoPage, @fixture:otherTodoPage

Adding @fixture:otherTodoPage or @fixture:adminTodoPage tag does not work

But I expect test cases to be generated successfully. The step "I filter todos as "Completed" should use parent fixture 'todoPage'

Isolated demo https://github.com/jzaratei/playwright-bdd-example/tree/inheritance-issue

Environment

jzaratei avatar Mar 08 '24 14:03 jzaratei

This is kind of "expected", but lets discuss the use case. Looking at the scenario itself, I've marked POM for each step:

  Scenario: Complete todos
    When I add todo "foo" # AdminTodoPage
    And I add todo "bar" # AdminTodoPage
    And I complete todo "bar" # OtherTodoPage
    And I filter todos as "Completed" # TodoPage
    Then visible todos count is 1 # TodoPage

So scenario uses 3 POMs that are in non-linear relationship:

TodoPage
AdminTodoPage extends TodoPage
OtherTodoPage extends TodoPage

Technically we can instantiate two POMs (AdminTodoPage and OtherTodoPage) and call common step (And I filter todos as "Completed") on one of them. But imagine if these POMs have some state created by previous steps? In that case calling common step on one of them can lead to unexpected behavior.

One of the ideas is to introduce config option statelessPoms: true and allow such situations. But I suggest to investigate more do we really need this. Maybe we can solve it with linear inheritance model. Could you share your real use-case for such inheritance?

vitalets avatar Mar 09 '24 08:03 vitalets

I got an e2e scenario where I need to select some cards, lets call it homeCardsPage (parent). Then I need to check (assert) those cards selected in two different pages: userCardsPage (child1) and emailCardPage(child2). These last two pages share the same list of cards, but they are different pages with different elements, that's why we could say they are siblings. They all share a global variable (fixture) cardList, with the card selected by the user.

When I select cards (homeCardPage)
Then I check cards selected in userPage (userCardPage)
And I send cards selected by email (emailCardPage)

I know that one way to cover this, would be to have two scenarios, one for checking the userCardPage and the other for emailCardPage. But In the case of an e2e scenario I should be able to assert the same element in different components (pages). So, making emailCardPage extends userCardPage, to cover the linear inheritance, could be a workaround, but I don't feel its the best solution, because I'm forcing emailCardPage inherit elements that are not needed.

jzaratei avatar Mar 11 '24 08:03 jzaratei

Fair enough! So in your example, on what fixture should we call the first step I select cards? I see 3 options:

  1. on homeCardPage
  2. on userCardPage
  3. on emailCardPage

vitalets avatar Mar 11 '24 08:03 vitalets

In homeCardPage, which is the first page, where the app display the cards, for the user to select them.

jzaratei avatar Mar 11 '24 08:03 jzaratei

@jzaratei I've improved guess-algorithm in v6.3.0. Now it assumes that all POM classes are stateless by default. It means that in case of several POMs involved in a single scenario, each decorator step will just bind to the corresponding class.

Taking the example above:

When I select cards (homeCardPage)
Then I check cards selected in userPage (userCardPage)
And I send cards selected by email (emailCardPage)

The generated test should be:

  test("my test", async ({ When, Then, And, homeCardPage, userCardPage, emailCardPage }) => {
    await When("I select cards", null, { homeCardPage });
    await Then("I check cards selected in userPage", null, { userCardPage });
    await And("I send cards selected by email", null, { emailCardPage });
  });

If it is still relevant for your project, could you re-check on your side?

Note: to return to previous behavior there is a new config option statefulPoms: true.

vitalets avatar Apr 23 '24 06:04 vitalets

Yeah, I am refactoring my page objects and now it seems to work as expected. Thank for this upgrade!

jzaratei avatar May 17 '24 06:05 jzaratei