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, і в коді рев'ю звертаю на це увагу.