Component Object Pattern

Component Object Pattern (COP) — это подход в тестировании, который помогает организовать код тестов таким образом, чтобы каждый элемент интерфейса и его действия были инкапсулированы в отдельном объекте. В Playwright данный паттерн позволяет сделать тесты более поддерживаемыми, гибкими и читаемыми. Основной идеей является разделение логики тестирования интерфейса на отдельные компоненты, что позволяет легко работать с частями UI, а также упрощает повторное использование кода.

Структура компонентов

В контексте Playwright каждый компонент будет представлять собой класс или модуль, инкапсулирующий взаимодействие с конкретным элементом UI, например, кнопкой, полем ввода, меню или даже целым блоком страницы. Важно, что компоненты должны быть максимально независимыми и самодостаточными, предоставляя только те методы, которые необходимы для взаимодействия с данным элементом.

Пример структуры компонента:

class LoginButton {
    constructor(page) {
        this.page = page;
        this.selector = 'button#login';
    }

    async click() {
        await this.page.click(this.selector);
    }

    async isVisible() {
        return await this.page.isVisible(this.selector);
    }
}

В этом примере класс LoginButton инкапсулирует логику взаимодействия с кнопкой логина. Он имеет методы click для нажатия на кнопку и isVisible для проверки видимости кнопки. Такие методы можно использовать в тестах, не дублируя логику их выполнения каждый раз.

Разделение на страницы и компоненты

Playwright позволяет использовать Page Object Pattern, но для более сложных интерфейсов часто требуется более детализированный подход. Разделение на компоненты полезно, когда интерфейс состоит из повторяющихся или сложных блоков.

Предположим, у нас есть форма логина с несколькими элементами, такими как поля ввода для имени пользователя и пароля, а также кнопка логина. Вместо того чтобы все эти элементы инкапсулировать в одном классе LoginPage, лучше разделить их на отдельные компоненты:

class UsernameField {
    constructor(page) {
        this.page = page;
        this.selector = 'input#username';
    }

    async type(text) {
        await this.page.fill(this.selector, text);
    }

    async getValue() {
        return await this.page.inputValue(this.selector);
    }
}

class PasswordField {
    constructor(page) {
        this.page = page;
        this.selector = 'input#password';
    }

    async type(text) {
        await this.page.fill(this.selector, text);
    }
}

class LoginButton {
    constructor(page) {
        this.page = page;
        this.selector = 'button#login';
    }

    async click() {
        await this.page.click(this.selector);
    }
}

Каждый компонент теперь отвечает только за свой элемент формы. Это упрощает добавление новых методов и улучшает поддержку тестов. Когда необходимо внести изменения в поведение компонента (например, изменить селектор или добавить новый метод), это можно сделать локально, не затрагивая другие части системы.

Преимущества Component Object Pattern

  1. Читаемость и поддерживаемость Код становится более структурированным и понятным. Каждый компонент выполняет свою задачу, что упрощает диагностику проблем и поддержку тестов.

  2. Повторное использование Компоненты могут использоваться многократно в разных тестах. Это позволяет сократить дублирование кода и делает тесты более гибкими.

  3. Изоляция изменений Если потребуется изменить логику взаимодействия с элементом UI (например, изменить селектор или добавить дополнительную логику), это можно сделать только в одном месте, не затрагивая остальные тесты.

  4. Тестирование отдельных компонентов В случае необходимости, компоненты можно протестировать изолированно, что ускоряет процесс отладки и тестирования.

Пример использования компонента в тесте

Тест с использованием Component Object Pattern будет выглядеть гораздо чище и проще:

const { test, expect } = require('@playwright/test');
const { UsernameField, PasswordField, LoginButton } = require('./components');

test('Login functionality', async ({ page }) => {
    const usernameField = new UsernameField(page);
    const passwordField = new PasswordField(page);
    const loginButton = new LoginButton(page);

    await usernameField.type('user123');
    await passwordField.type('password123');
    await loginButton.click();

    // Проверка, что мы успешно зашли
    await expect(page).toHaveURL('https://example.com/dashboard');
});

В этом тесте мы создаем экземпляры компонентов для каждого элемента страницы, взаимодействуем с ними и проверяем, что логин прошел успешно. Каждый шаг взаимодействия с UI разделен и инкапсулирован в своих компонентах, что делает код теста кратким и понятным.

Динамическое создание компонентов

Для более гибкой работы можно использовать фабричный метод для создания экземпляров компонентов. Например, если на странице есть несколько кнопок с одинаковым поведением, их можно инкапсулировать в один общий компонент, используя параметры:

class Button {
    constructor(page, selector) {
        this.page = page;
        this.selector = selector;
    }

    async click() {
        await this.page.click(this.selector);
    }

    async isVisible() {
        return await this.page.isVisible(this.selector);
    }
}

class LoginPage {
    constructor(page) {
        this.page = page;
        this.usernameField = new UsernameField(page);
        this.passwordField = new PasswordField(page);
        this.loginButton = new Button(page, 'button#login');
    }

    async login(username, password) {
        await this.usernameField.type(username);
        await this.passwordField.type(password);
        await this.loginButton.click();
    }
}

В этом примере класс LoginPage использует компонент Button, передавая ему селектор кнопки, что позволяет переиспользовать компонент для других кнопок на странице.

Советы по организации кода

  • Не перегружайте компоненты Каждый компонент должен отвечать только за один элемент или блок на странице. Избегайте создания “тяжелых” компонентов, которые инкапсулируют слишком много логики.

  • Используйте наследование с осторожностью Если компоненты имеют схожие поведения, можно использовать наследование для общего функционала. Однако следует избегать сложной иерархии, которая может затруднить понимание и поддержку кода.

  • Поддерживайте модульность Чем меньше компонентов будет зависеть друг от друга, тем проще будет поддерживать тесты в будущем. Модули можно тестировать и обновлять независимо.

Заключение

Component Object Pattern помогает организовать код в Playwright, улучшая читаемость и поддержку тестов. Этот подход значительно упрощает управление сложными UI, позволяет избежать дублирования кода и облегчает работу с различными элементами интерфейса.