Russian is below English
In this tutorial, we will explore different approaches to structuring your Playwright test classes using TypeScript. We'll start by understanding your initial code structure, then dive into two scenarios: extending abstract classes and extending the Playwright Page
class directly. Finally, we'll list the advantages and disadvantages of each approach and provide a recommendation based on the needs of your project.
This class serves as a base class holding a reference to the Playwright Page
instance and includes common methods.
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.page.goto(url);
}
// Example common method
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.page.screenshot({ path: filename });
}
}
This class extends AbstractPage
and adds more common functionality.
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.page.waitForSelector(selector);
}
}
This class extends PageObject
and includes specific functionalities for the login page.
import { PageObject } from './PageObject';
import { Button } from './Button';
import { Input } from './Input';
export class LoginPage extends PageObject {
private button: Button;
private input: Input;
constructor(page: Page) {
super(page);
this.button = new Button(page);
this.input = new Input(page);
}
async open(): Promise<void> {
await this.navigateTo('http://example.com/login');
}
async fillEmail(value: string): Promise<void> {
logger.info(`Filling email with value: ${value}`);
await this.input.setInputValue('#email', value);
}
async fillPassword(value: string): Promise<void> {
logger.info(`Filling password with value: ${value}`);
await this.input.setInputValue('#password', value);
}
async clickLoginButton(): Promise<void> {
logger.info('Clicking login button');
await this.button.clickButton('#login-button');
}
}
In this scenario, AbstractPage
holds a reference to the Page
instance, and PageObject
extends AbstractPage
.
- AbstractPage: Base class holding a reference to the
Page
instance. - PageObject: Extends
AbstractPage
and adds common functionality. - MyPage: Extends
PageObject
and adds page-specific functionality.
// AbstractPage Class
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.page.goto(url);
}
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.page.screenshot({ path: filename });
}
}
// PageObject Class
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.page.waitForSelector(selector);
}
}
// MyPage Class
import { PageObject } from './PageObject';
export class MyPage extends PageObject {
constructor(page: Page) {
super(page);
}
// Override the goto method
async goto(url: string, options?: { timeout?: number, waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit' }): Promise<void> {
logger.info(`Navigating to ${url} with custom behavior.`);
// Custom actions before navigation
await this.captureScreenshot('before-navigation.png');
// Call the original navigateTo method
await this.navigateTo(url);
// Custom actions after navigation
await this.captureScreenshot('after-navigation.png');
}
}
// Usage Example
const page = await browser.newPage();
const myPage = new MyPage(page);
await myPage.goto('http://example.com'); // Calls the overridden goto method
In this scenario, AbstractPage
extends the Playwright Page
class directly.
- AbstractPage: Extends the
Page
class directly. - PageObject: Extends
AbstractPage
and adds common functionality. - MyPage: Extends
PageObject
and adds page-specific functionality.
// AbstractPage Class
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage extends Page {
constructor(page: Page) {
super(page.context(), page._connection); // Initialize the Page class
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.goto(url); // Use the Page class's goto method
}
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.screenshot({ path: filename });
}
}
// PageObject Class
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.waitForSelector(selector);
}
}
// MyPage Class
import { PageObject } from './PageObject';
export class MyPage extends PageObject {
constructor(page: Page) {
super(page);
}
// Override the goto method
async goto(url: string, options?: { timeout?: number, waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit' }): Promise<void> {
logger.info(`Navigating to ${url} with custom behavior.`);
// Custom actions before navigation
await this.captureScreenshot('before-navigation.png');
// Call the original goto method with custom logic
await super.goto(url, options);
// Custom actions after navigation
await this.captureScreenshot('after-navigation.png');
}
}
// Usage Example
const page = await browser.newPage();
const myPage = new MyPage(page);
await myPage.goto('http://example.com'); // Calls the overridden goto method
Advantages:
- Modularity: High, allowing for clean separation of concerns.
- Customization: Extensive customization of methods and functionality.
- Maintainability: Better for managing and updating a complex project.
- Reusability: Promotes reusability of common components.
Disadvantages:
- Indirect Access: Methods from the
Page
class are accessed viathis.page
. - Additional Layer of Abstraction: Adds complexity and can make the code harder to follow.
- Performance Overhead: Slight performance overhead due to additional method calls.
Advantages:
- Direct Access: Directly inherits all methods from the
Page
class. - Simplicity: Provides a simpler structure with direct method access.
- Performance: Potentially better performance with fewer method calls.
Disadvantages:
- Reduced Modularity: Less modular and more tightly coupled.
- Tight Coupling: Harder to decouple or replace parts of the functionality.
- Less Separation of Concerns: Mixed logic reduces clarity and maintainability.
- Potential Overriding Issues: Risk of conflicts and unexpected behavior.
Based on the complexity of your project and the detailed testing scenarios we discussed, I recommend using Scenario 1 where PageObject
holds a reference to the Page
instance.
- Modularity and Customization: Allows for a modular and clean design with extensive customization.
- Separation of Concerns: Ensures a clear separation between common and page-specific logic, making the codebase more maintainable.
- Maintainability: Better suited for maintaining and updating complex projects with detailed test cases.
- Reusability: Promotes the reuse of common components, reducing code duplication.
Russian
Конечно! Вот полное руководство на русском языке:
В этом руководстве мы рассмотрим различные подходы к структурированию ваших тестовых классов Playwright с использованием TypeScript. Мы начнем с понимания вашей начальной структуры кода, затем погрузимся в два сценария: расширение абстрактных классов и расширение класса Playwright Page
напрямую. В конце мы перечислим преимущества и недостатки каждого подхода и предоставим рекомендации на основе потребностей вашего проекта.
Этот класс служит базовым классом, содержащим ссылку на экземпляр Playwright Page
и включает общие методы.
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.page.goto(url);
}
// Пример общего метода
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.page.screenshot({ path: filename });
}
}
Этот класс расширяет AbstractPage
и добавляет более общие функции.
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.page.waitForSelector(selector);
}
}
Этот класс расширяет PageObject
и включает специфические функции для страницы входа.
import { PageObject } from './PageObject';
import { Button } from './Button';
import { Input } from './Input';
export class LoginPage extends PageObject {
private button: Button;
private input: Input;
constructor(page: Page) {
super(page);
this.button = new Button(page);
this.input = new Input(page);
}
async open(): Promise<void> {
await this.navigateTo('http://example.com/login');
}
async fillEmail(value: string): Promise<void> {
logger.info(`Filling email with value: ${value}`);
await this.input.setInputValue('#email', value);
}
async fillPassword(value: string): Promise<void> {
logger.info(`Filling password with value: ${value}`);
await this.input.setInputValue('#password', value);
}
async clickLoginButton(): Promise<void> {
logger.info('Clicking login button');
await this.button.clickButton('#login-button');
}
}
В этом сценарии класс AbstractPage
содержит ссылку на экземпляр Page
, а PageObject
расширяет AbstractPage
.
- AbstractPage: Базовый класс, содержащий ссылку на экземпляр
Page
. - PageObject: Расширяет
AbstractPage
и добавляет общую функциональность. - MyPage: Расширяет
PageObject
и добавляет функциональность, специфическую для страницы.
// Класс AbstractPage
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.page.goto(url);
}
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.page.screenshot({ path: filename });
}
}
// Класс PageObject
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.page.waitForSelector(selector);
}
}
// Класс MyPage
import { PageObject } from './PageObject';
export class MyPage extends PageObject {
constructor(page: Page) {
super(page);
}
// Переопределение метода goto
async goto(url: string, options?: { timeout?: number, waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit' }): Promise<void> {
logger.info(`Navigating to ${url} with custom behavior.`);
// Пользовательские действия перед навигацией
await this.captureScreenshot('before-navigation.png');
// Вызов оригинального метода navigateTo
await this.navigateTo(url);
// Пользовательские действия после навигации
await this.captureScreenshot('after-navigation.png');
}
}
// Пример использования
const page = await browser.newPage();
const myPage = new MyPage(page);
await myPage.goto('http://example.com'); // Вызывает переопределенный метод goto
В этом сценарии класс AbstractPage
напрямую расширяет класс Playwright Page
.
- AbstractPage: Напрямую расширяет класс
Page
. - PageObject: Расширяет
AbstractPage
и добавляет общую функциональность. - MyPage: Расширяет
PageObject
и добавляет функциональность, специфическую для страницы.
// Класс AbstractPage
import { Page } from '@playwright/test';
import logger from '../lib/logger';
export abstract class AbstractPage extends Page {
constructor(page: Page) {
super(page.context(), page._connection); // Инициализация класса Page
}
async navigateTo(url: string): Promise<void> {
logger.info(`Navigating to ${url}`);
await this.goto(url); // Использование метода goto класса Page
}
async captureScreenshot(filename: string): Promise<void> {
logger.info(`Capturing screenshot: ${filename}`);
await this.screenshot({ path: filename });
}
}
// Класс PageObject
import { AbstractPage } from './AbstractPage';
export class PageObject extends AbstractPage {
constructor(page: Page) {
super(page);
}
async waitForElement(selector: string): Promise<void> {
logger.info(`Waiting for element with selector: ${selector}`);
await this.waitForSelector(selector);
}
}
// Класс MyPage
import { PageObject } from './PageObject';
export class MyPage extends PageObject {
constructor(page: Page) {
super(page);
}
// Переопределение метода goto
async goto(url: string, options?: { timeout?: number, waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit' }): Promise<void> {
logger.info(`Navigating to ${url} with custom behavior.`);
// Пользовательские действия перед навигацией
await this.captureScreenshot('before-navigation.png');
// Вызов оригинального метода goto с пользовательской логикой
await super.goto(url, options);
// Пользовательские действия после навигации
await this.captureScreenshot('after-navigation.png');
}
}
// Пример использования
const page = await browser.newPage();
const myPage = new MyPage(page);
await myPage.goto('http://example.com'); // Вызывает переопределенный метод goto
Преимущества:
- Модульность: Высокая, позволяет чисто разделять ответственность.
- Настраиваемость: Широкая возможность настройки методов и функциональности.
- Поддерживаемость: Легче управлять и обновлять сложный проект.
- Повторное использование: Способствует повторному использованию общих компонентов.
Недостатки:
- Косвенный доступ: Методы класса
Page
доступны черезthis.page
. - Дополнительный слой абстракции: Увеличивает сложность и может усложнить код.
- Производственные издержки: Небольшие производственные издержки из-за дополнительных вызовов методов.
Преимущества:
- Прямой доступ: Напрямую наследует все методы класса
Page
. - Простота: Обеспечивает более простую структуру с прямым доступом к методам.
- Производительность: Потенциально лучшая производительность с меньшим количеством вызовов методов.
Недостатки:
- Снижение модульности: Менее модульная и более жестко связанная структура.
- Тесная связь: Сложнее отделить или заменить части функциональности.
- Меньшее разделение обязанностей: Смешанная логика уменьшает ясность и поддерж