QA Interview Hub · База знань · Тести
TypeScript
Класи і модифікатори доступу
Модифікатори доступу і parameter properties
У TS є три модифікатори доступу:
public - за замовчуванням, доступно звідусіль.protected - доступно в класі і наслідниках.private - тільки всередині класу.
Важливо: це перевірки компілятора. У рантаймі private поле - звичайна властивість об'єкта. Справжню приватність дають JS private fields з #: #token недоступний ззовні навіть у рантаймі.
export class LoginPage {
private readonly emailInput: Locator;
readonly submitButton: Locator;
constructor(private readonly page: Page) {
this.emailInput = page.getByLabel('Email');
this.submitButton = page.getByRole('button', { name: 'Увійти' });
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.page.getByLabel('Пароль').fill(password);
await this.submitButton.click();
}
}
constructor(private readonly page: Page) - parameter property: одночасно оголошує поле і присвоює його. Зручно, але з TS 5.8 є опція erasableSyntaxOnly, яка забороняє такий синтаксис разом з enum, бо він генерує JS-код, а не просто стирається. Вона актуальна, якщо TS запускається напряму через type stripping у Node. У звичайному Playwright-проєкті parameter properties працюють.
readonly на полі означає, що його можна присвоїти тільки при оголошенні або в конструкторі.
Як я вирішую, що робити публічним у Page Object: публічні - дії бізнес-рівня (login, makeTransfer) і локатори, які потрібні для expect у тестах. Внутрішні деталі - private.
На співбесіді: public, protected і private перевіряються тільки компілятором, а для справжньої приватності в рантаймі є поля з #. У Page Object я роблю публічними бізнес-дії і локатори для перевірок, а page і допоміжні методи ховаю.
abstract, implements і наслідування в Page Object
Абстрактний клас не можна створити напряму. Він задає спільну логіку і змушує наслідників реалізувати абстрактні методи.
export abstract class BasePage {
protected abstract readonly path: string;
constructor(protected readonly page: Page) {}
async open(): Promise<void> {
await this.page.goto(this.path);
await this.waitForLoaded();
}
protected abstract waitForLoaded(): Promise<void>;
}
export class AccountsPage extends BasePage {
protected readonly path = '/accounts';
protected async waitForLoaded(): Promise<void> {
await expect(this.page.getByRole('heading', { name: 'Рахунки' })).toBeVisible();
}
}
implements перевіряє, що клас відповідає інтерфейсу, але нічого не успадковує. Це корисно для взаємозамінних реалізацій: наприклад, PaymentsApi і PaymentsMock реалізують один інтерфейс.
interface PaymentsGateway {
pay(amount: number, currency: string): Promise<{ txId: string }>;
}
class PaymentsApi implements PaymentsGateway {
async pay(amount: number, currency: string) {
return { txId: 'real-id' };
}
}
Чесна позиція щодо наслідування: глибокі ієрархії Page Object (BasePage, потім AuthorizedPage, потім DashboardPage) швидко стають крихкими. Я віддаю перевагу композиції: сторінка складається з компонентів (Header, TransferForm, Table), а спільні речі передаються через фікстури. Один рівень базового класу - нормально, три - вже сигнал до рефакторингу.
static - члени класу, а не екземпляра. Підходять для фабрик або констант, але не для стану: статичний стан ділиться між тестами в одному воркері і може дати flaky.
На співбесіді: Абстрактний клас дає спільну логіку і змушує наслідників реалізувати потрібні методи, а implements лише перевіряє контракт. У Page Object я тримаю максимум один рівень наслідування і віддаю перевагу композиції з компонентів.
Пастка з this і стрілкові методи
У JS значення this визначається способом виклику, а не місцем оголошення. Якщо передати метод класу як колбек, this загубиться.
class TokenStore {
private token = '';
set(value: string) { this.token = value; }
get() { return this.token; }
}
const store = new TokenStore();
const getter = store.get;
// getter(); // TypeError у рантаймі: this === undefined
Класи в JS завжди в strict mode, тому this буде undefined, а не глобальний об'єкт.
Рішення:
- Передавати стрілку:
items.map(x => helper.format(x)). - Прив'язати:
store.get.bind(store). - Оголосити метод як стрілкову властивість:
get = () => this.token;. Така функція захоплює this екземпляра. Мінус - вона створюється для кожного екземпляра і не лежить у прототипі.
TS частково допомагає: можна явно оголосити параметр this у сигнатурі (function f(this: Foo)), і тоді неправильний виклик не скомпілюється. Але для звичайних методів класу компілятор втрату this при передачі як колбека не ловить.
Де я бачив це в автоматизації: передача методу Page Object у test.step або в хелпер retry як retry(page.reload). Пишу retry(() => page.reload()), і проблеми немає.
На співбесіді: this у JS залежить від того, як функцію викликали, тому метод, переданий як колбек, втрачає контекст. Я передаю стрілкову функцію-обгортку або використовую bind, і в коді рев'ю звертаю на це увагу.