General Playwright contribution guide
Page Objects
We use page objects/actions pattern to encapsulate all UI elements and operations.
Furthermore, every method in the page object class should have step decorator. This decorator wraps the method into playwright test.step(). This vastly improves readability of test report.
Example:
import { step } from '../common';
export class WalletPage {
    readonly searchInput: Locator;
    readonly accountChevron: Locator;
    constructor(private readonly page: Page) {
        this.searchInput = this.window.getByTestId('@wallet/accounts/search-icon');
        this.accountChevron = this.window.getByTestId('@account-menu/arrow');
    }
    @step()
    async filterTransactions(transaction: string) {
        await this.searchInput.click();
        await this.searchInput.fill(transaction, { force: true });
    }
    @step()
    async expandAllAccountsInMenu() {
        for (const chevron of await this.accountChevron.all()) {
            await chevron.click();
        }
    }
❌ Never pass Page instance as a method argument.
✅ Always create a construtor to pass the Page instance to the page action.
✅ Always add an descriptor @step() before every Page object method.
✅ Page objects naming should clearly indicate what kind object we are dealing with. We use suffixes: page for whole pages, section for just parts of page, and other suffixes reflecting type of elements they cover: modal, panel, input and prompt.
Fixtures
To further improve test readability we want to use fixtures to inject our page actions into the tests. Additionally, we use fixtures also for mocks (blockbookMock), setups (page) and watchers (exceptionLogger)
Example:
import { test as base } from '@playwright/test';
import { WalletPage } from './pageActions/walletActions';
const test = base.extend<{
    walletPage: WalletPage;
}>({
    walletPage: async ({ page }, use) => {
        const walletPage = new WalletActions(page);
        await use(walletPage);
    },
});
export { test };
✅ Correct way to use page fixture in the test:
test('Wallet test', async ({ walletPage }) => {
    await walletPage.clickAllAccountArrows();
    ...
});
❌ Wrong way to use page action in the test:
test('Wallet test', async ({ page }) => {
    const walletPage = new WalletPage(page);
    await walletPage.clickAllAccountArrows();
    ...
});
Locators
We want our locators to be defined as page object properties to further improve provide reusability, centralized maintenance, and improved readability.
Example:
export class SuiteGuide {
    private readonly window: Page;
    readonly guideButton: Locator;
    readonly guidePanel: Locator;
    readonly bugLocationDropdown: Locator;
    readonly bugLocationDropdownInput: Locator;
    readonly bugLocationDropdownOption = (location: FeedbackCategory) =>
        this.window.getByTestId(`@guide/feedback/suggestion-dropdown/select/option/${location}`);
    constructor(window: Page) {
        this.window = window;
        this.guideButton = this.window.getByTestId('@guide/button-open');
        this.guidePanel = this.window.getByTestId('@guide/panel');
        this.bugLocationDropdown = this.window.getByTestId('@guide/feedback/suggestion-dropdown');
        this.bugLocationDropdownInput = this.window.getByTestId(
            '@guide/feedback/suggestion-dropdown/select/input',
        );
    }
    @step()
    async openPanel() {
        await this.guideButton.click();
        await expect(this.guidePanel).toBeVisible();
    }
    @step()
    async selectBugLocation(location: FeedbackCategory) {
        await this.bugLocationDropdown.click();
        await this.bugLocationDropdownOption(location).click();
        await expect(this.bugLocationDropdownInput).toHaveText(capitalizeFirstLetter(location));
    }
Locators should mainly rely on testIds this.window.getByTestId('@guide/panel') that we add to our product as html attributes data-testid="@guide/panel".
Alternatively you can use user facing locators page.getByRole('button', { name: 'Submit' }).but testIds are our strong preference.
Adhere to consistent and clear naming. End the locator name with info of what kind of element it is dashboardMenuButton, discoveryHeader, discoveryBar, bugLocationDropdown.
There is vast documentation on how to work with Locators on PlayWraith webpages
❌ Never use XPath or CSS locator. They are fragile and break often.
await page
    .locator('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input')
    .click();
await page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click();