1

I currently work on UI-level e2e test automation of a web app, where entire flow is concentrated in one page. To avoid huge single page object size, I've split it into smaller components, listed out and presented in the pic below:

  1. Table (red) class with TableRow elements
  2. Toolbar (green) class with ToolbarBtn elements
  3. List (yellow) with ListElement elements, that in turn contain ListElementStatusBox element enter image description here

An example business flow goes as follows:
Click TableRow[i] -> Click ListElement[i] -> Click ToolbarBtn1 ->ListElementStatusBox[i] changes status to "Status 1"

The code for a decomposed page object and its components is presented below: list.component.ts

import { ElementStatus } from "../enums/element.status.enum";

export class List {

    public readonly listElement: Locator;

    constructor(page: Page) {
        this.listElement = page.getByTestId(`list-element`);
    }

    public async clickListElementWithTitle(title: string): Promise<void> {
        await this.listElement.getByText(title).click();
    }

    public async getElementStatus(elementTitle: string): Promise<ElementStatus> {
        const status = await this.listElement.getByText(elementTitle).locator(`list-element-status-box`);

        return status;
    }
}

table.component.ts

export class Table {

    private readonly _tableRow: Locator;

    constructor(page: Page) {
        this._tableRow = page.getByTestId(`left-table-row`);
    }

    public async clickTableRowWithTitle(title: string): Promise<void> {
        await this._tableRow.getByText(title).click();
    }
}

toolbar.component.ts

export class Toolbar {

    private readonly _toolbarBtn1: Locator;
    private readonly _toolbarBtn2: Locator;
    private readonly _toolbarBtn3: Locator;
    
    constructor(page: Page) {
        this._toolbarBtn1 = page.getByTestId(`toolbar-button-1`);
        this._toolbarBtn2 = page.getByTestId(`toolbar-button-2`);
        this._toolbarBtn3 = page.getByTestId(`toolbar-button-3`);
    }

    public async clickButton1(): Promise<void> {
        await this._toolbarBtn1.click();
    }

    public async clickButton2(): Promise<void> {
        await this._toolbarBtn2.click();
    }

    public async clickButton3(): Promise<void> {
        await this._toolbarBtn3.click();
    }
}

element.status.enum.ts

export enum ElementStatus {
    STATUS_1 = "Status 1",
    STATUS_2 = "Status 2"
}

main.page.ts

import { List } from "../components/list.component";
import { Table } from "../components/table.component";
import { Toolbar } from "../components/toolbar.component";
import { ElementStatus } from "../enums/element.status.enum";

export class MainPage {
  private readonly _leftTable: Table;
  private readonly _topToolbar: Toolbar;
  private readonly _centerList: List;

  constructor(private readonly page: Page) {
    this._leftTable = new Table(page);
    this._topToolbar = new Toolbar(page);
    this._centerList = new List(page);
  }

  public async goTo(): Promise<void> {
    await this.page.goto('https://playwright.dev/');
  }

  public async changeTheStatusOfItem(rowTitle: string, listElementTitle: string): Promise<void> {
    await this._leftTable.clickTableRowWithTitle(rowTitle);
    await this._centerList.clickListElementWithTitle(listElementTitle);
    await this._topToolbar.clickButton1();
  }

  public async getItemStatusForElementWithTitle(title: Locator): Promise<ElementStatus> {
    const status = await this._centerList.getElementStatus(title);

    return status;
  }
}

example.test.ts

import { test, expect } from '@playwright/test';
import { MainPage } from '../pages/main.page';
import { ElementStatus } from '../enums/element.status.enum';

const rowTitle = "Test Row Title 1";
const listItemTitle = "Test List title 1"

test('get started link', async ({ page }) => {
    const mainPage = new MainPage(page);
    
    await mainPage.goTo();
    await mainPage.changeTheStatusOfItem(rowTitle, listItemTitle);
    
    await expect.poll(async () => {
        const status = await mainPage.getItemStatusForElementWithTitle(listItemTitle);
        return status;
    }).toBe(ElementStatus.STATUS_1)
  });

I'm not sure if this decomposition is correct, as there are some code smells:

  1. page object mainPage become more like actions aggregator than actual page object modelling a page and its behavior.
  2. Some methods in mainPage are just wrappers around basically the same methods from components, e.g. getItemStatusForElementWithTitle(title), which violates DRY rule.

I'm also unsure about other topics, like:

  1. Would it be better to move expect.poll() to page object and replace it with verifyElementStatus method in test file?
  2. How to approach clicking nth element from the list? Would adding method in mainPage, like:
public getListElementWithNumber(number: number): Locator {
  return this._centerList.listElement.nth(number)
}

or adding getter (in case only 1st element is important)

public get firstListElement(): Locator {
  return this._centerList.listElement.first()
}

do the trick in a clean way? Or is there a design pattern that would make this code more elegant?

0

1 Answer 1

0

First thing first.

Let's step back and answer few questions honestly to get the clarity:

Why are we doing this decomposition in the first place? What are we trying to achieve?

What problem(s) we are trying to solve by this design?

Do we have any problem(s) in the first place?

What are some current actual burning problems to solve(by automation) in your context?

  • a) Are we going to need it in multiple pages even not all these components but few of them? And/Or even more complex version of these components?

  • b) By doing this - are we really achieving DRYness , modularity , reusability and maintainability?Any of them or ALL of them? Which are more important desirable attributes in your context?

  • c) How is it making scripting more easier, faster & reliable?

I think there are no right or wrong answers in the design but depends on the context.I think if we answer this questions honestly and deeply , we will get the clear picture .

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.