Типизация в Playwright

Урок: Типизация в Playwright (page, locator, test)

Введение

К этому моменту у тебя уже есть вся база:

  • ты знаешь, что такое TypeScript;
  • умеешь работать с базовыми типами;
  • типизируешь переменные и функции;
  • используешь интерфейсы;
  • понимаешь, как задавать типы входных и выходных данных.

Теперь самое важное — применить всё это в реальном инструменте. И здесь появляется Playwright.

Когда ты пишешь тест:

  • ты работаешь со страницей (page);
  • находишь элементы (locator);
  • пишешь сами тесты (test).

Если не использовать типы:

  • легко ошибиться в методах;
  • не очевидно, что возвращает функция;
  • сложно читать код.

TypeScript решает это — он делает работу с Playwright предсказуемой.

Главная идея урока: мы учимся правильно использовать встроенные типы Playwright, чтобы писать надёжные и понятные тесты.


Что даёт TypeScript в Playwright

Когда ты используешь Playwright с TypeScript, ты получаешь:

  • автоподсказки методов (page.click, locator.fill и т.д.);
  • проверку параметров;
  • понимание, что возвращает каждая операция;
  • защиту от ошибок ещё до запуска теста.

Это особенно важно, потому что тесты:

  • часто длинные;
  • работают с асинхронным кодом;
  • взаимодействуют с UI.

Тип page

page — это главный объект, через который ты управляешь браузером.

В реальном проекте у него тип Page из @playwright/test. Ниже — упрощённый мок с теми же сигнатурами методов:

type MockLocator = {
  click: () => Promise<void>;
  fill: (text: string) => Promise<void>;
};

type MockPage = {
  goto: (url: string) => Promise<void>;
  locator: (selector: string) => MockLocator;
  fill: (selector: string, text: string) => Promise<void>;
  click: (selector: string) => Promise<void>;
};

const test = (
  name: string,
  fn: (ctx: { page: MockPage }) => Promise<void>,
): Promise<void> => {
  console.log('→ test:', name);
  const page: MockPage = {
    goto: async (url) => console.log('page.goto', url),
    locator: (sel) => ({
      click: async () => console.log('locator.click', sel),
      fill: async (text) => console.log('locator.fill', sel, text),
    }),
    fill: async (sel, text) => console.log('page.fill', sel, text),
    click: async (sel) => console.log('page.click', sel),
  };
  return fn({ page });
};

void (async () => {
  await test('open page', async ({ page }) => {
    await page.goto('https://example.com');
  });
})();

Что здесь происходит:

  • в Playwright тип называется Page;
  • фикстура теста передаёт { page: Page };
  • TypeScript знает список методов у page.

Благодаря этому:

  • появляются подсказки методов;
  • нельзя вызвать несуществующий метод (на этапе проверки типов).

Упрощённый вариант (часто используемый)

Обычно Playwright уже типизирует page автоматически — тип выводится из сигнатуры test:

type MockLocator = {
  click: () => Promise<void>;
  fill: (text: string) => Promise<void>;
};

type MockPage = {
  goto: (url: string) => Promise<void>;
  locator: (selector: string) => MockLocator;
  fill: (selector: string, text: string) => Promise<void>;
  click: (selector: string) => Promise<void>;
};

const test = (
  name: string,
  fn: (ctx: { page: MockPage }) => Promise<void>,
): Promise<void> => {
  console.log('→ test:', name);
  const page: MockPage = {
    goto: async (url) => console.log('page.goto', url),
    locator: (sel) => ({
      click: async () => console.log('locator.click', sel),
      fill: async (text) => console.log('locator.fill', sel, text),
    }),
    fill: async (sel, text) => console.log('page.fill', sel, text),
    click: async (sel) => console.log('page.click', sel),
  };
  return fn({ page });
};

void (async () => {
  await test('open page', async ({ page }) => {
    await page.goto('https://example.com');
  });
})();

TypeScript сам понимает, что page соответствует объявленному типу.

Но важно знать, что в Playwright этот тип называется Page и что он означает.


Тип Locator

locator — это объект, который представляет элемент на странице.

В Playwright у него тип Locator. Пример с моком:

type MockLocator = {
  click: () => Promise<void>;
  fill: (text: string) => Promise<void>;
};

type MockPage = {
  goto: (url: string) => Promise<void>;
  locator: (selector: string) => MockLocator;
  fill: (selector: string, text: string) => Promise<void>;
  click: (selector: string) => Promise<void>;
};

const test = (
  name: string,
  fn: (ctx: { page: MockPage }) => Promise<void>,
): Promise<void> => {
  console.log('→ test:', name);
  const page: MockPage = {
    goto: async (url) => console.log('page.goto', url),
    locator: (sel) => ({
      click: async () => console.log('locator.click', sel),
      fill: async (text) => console.log('locator.fill', sel, text),
    }),
    fill: async (sel, text) => console.log('page.fill', sel, text),
    click: async (sel) => console.log('page.click', sel),
  };
  return fn({ page });
};

void (async () => {
  await test('click button', async ({ page }) => {
    const button: MockLocator = page.locator('#submit');
    await button.click();
  });
})();

Что здесь важно:

  • Locator — это не просто элемент;
  • это «умный» объект, который умеет:
    • ждать появления элемента;
    • выполнять действия;
    • работать стабильно.

TypeScript знает, какие методы доступны у Locator:

  • click()
  • fill()
  • textContent()

Если написать вызов несуществующего метода, проверка типов это отловит. В чистом JavaScript ошибка всплывёт только в рантайме:

const button: { click: () => Promise<void> } = {
  click: async () => console.log('click'),
};
try {
  (button as any).notExistingMethod();
} catch (e: unknown) {
  const msg = e instanceof Error ? e.message : String(e);
  console.log('в TS такого метода нет; в JS:', msg);
}

Тип test

test — это функция, которая создаёт тест.

Когда ты пишешь:

type MockLocator = {
  click: () => Promise<void>;
  fill: (text: string) => Promise<void>;
};

type MockPage = {
  goto: (url: string) => Promise<void>;
  locator: (selector: string) => MockLocator;
  fill: (selector: string, text: string) => Promise<void>;
  click: (selector: string) => Promise<void>;
};

const test = (
  name: string,
  fn: (ctx: { page: MockPage }) => Promise<void>,
): Promise<void> => {
  console.log('→ test:', name);
  const page: MockPage = {
    goto: async (url) => console.log('page.goto', url),
    locator: (sel) => ({
      click: async () => console.log('locator.click', sel),
      fill: async (text) => console.log('locator.fill', sel, text),
    }),
    fill: async (sel, text) => console.log('page.fill', sel, text),
    click: async (sel) => console.log('page.click', sel),
  };
  return fn({ page });
};

void (async () => {
  await test('my test', async ({ page }) => {
    await page.goto('https://example.com');
  });
})();

TypeScript:

  • знает структуру аргументов колбэка;
  • понимает, какие объекты доступны (page, browser, и т.д. в реальном API);
  • помогает не ошибиться в использовании.

Асинхронность и типы

Playwright активно использует async/await.

Пример:

void (async () => {
  const page: { title: () => Promise<string> } = {
    title: async () => 'Example Domain',
  };
  const title = await page.title();
  console.log('title:', title);
})();

TypeScript знает, что:

  • page.title() возвращает Promise<string>;
  • после await результат — это string.

Поэтому после await ты работаешь уже со строкой:

void (async () => {
  const page: { title: () => Promise<string> } = {
    title: async () => 'Example Domain',
  };
  const title: string = await page.title();
  console.log('длина заголовка:', title.length);
})();

Если написать (в TypeScript) что-то вроде «сложить строку с числом» для переменной, которую ты ожидаешь как string, компилятор покажет несоответствие. В чистом JS это просто выполнится:

void (async () => {
  const page: { title: () => Promise<string> } = {
    title: async () => 'Example Domain',
  };
  const title = await page.title();
  console.log('конкатенация (JS допускает):', title + String(123));
})();

TypeScript сразу покажет несоответствие, если тип результата не совпадает с ожидаемым (например, при строгих проверках или неверной аннотации).


Пример полного теста с типами

type MockLocator = {
  click: () => Promise<void>;
  fill: (text: string) => Promise<void>;
};

type MockPage = {
  goto: (url: string) => Promise<void>;
  locator: (selector: string) => MockLocator;
  fill: (selector: string, text: string) => Promise<void>;
  click: (selector: string) => Promise<void>;
  url: () => string;
};

type MockExpect = {
  toBeVisible: () => Promise<void>;
  toHaveURL: (u: string) => Promise<void>;
  toContain: (sub: string) => Promise<void>;
};

const test = (
  name: string,
  fn: (ctx: { page: MockPage }) => Promise<void>,
): Promise<void> => {
  console.log('→ test:', name);
  const page: MockPage = {
    goto: async (url) => console.log('page.goto', url),
    locator: (sel) => ({
      click: async () => console.log('locator.click', sel),
      fill: async (text) => console.log('locator.fill', sel, text),
    }),
    fill: async (sel, text) => console.log('page.fill', sel, text),
    click: async (sel) => console.log('page.click', sel),
    url: () => 'https://app.example/dashboard',
  };
  return fn({ page });
};

const expect = (value: string): MockExpect => ({
  toBeVisible: async () => console.log('expect.toBeVisible'),
  toHaveURL: async (u) => console.log('expect.toHaveURL', u),
  toContain: async (sub) => console.log('expect.toContain', value, sub),
});

void (async () => {
  await test('login test', async ({ page }) => {
    const usernameInput: MockLocator = page.locator('#username');
    const passwordInput: MockLocator = page.locator('#password');
    const loginButton: MockLocator = page.locator('#login');
    await usernameInput.fill('admin');
    await passwordInput.fill('1234');
    await loginButton.click();
    const url: string = page.url();
    await expect(url).toContain('dashboard');
  });
})();

Разбор:

  • Locator используется для элементов;
  • string — для результата page.url();
  • все типы понятны и проверяются.

Зачем это нужно в реальной работе

Типизация в Playwright даёт:

1. Меньше ошибок

Ты не можешь:

  • вызвать несуществующий метод;
  • передать неправильный тип;
  • забыть про асинхронность.

2. Быструю разработку

Редактор подсказывает:

  • какие методы есть у page;
  • какие параметры нужны;
  • что возвращает функция.

3. Понятный код

Когда ты видишь:

const button: { click: () => Promise<void> } = {
  click: async () => console.log('locator.click', 'button'),
};
console.log('переменная — как Locator: известен набор методов');

ты сразу понимаешь:

  • это элемент страницы;
  • с ним можно делать действия.

В Playwright вместо ручного объекта будет const button: Locator = page.locator('button');.

console.log(
  'Подсказка: типы Page и Locator приходят из @playwright/test; через import type редактор знает сигнатуры методов.',
);

Связь со следующим уроком

Сейчас ты работаешь напрямую с:

  • page;
  • locator.

Но в реальных проектах так не делают.

Вместо этого используют Page Object:

  • создают классы;
  • выносят логику работы со страницей;
  • добавляют типизацию.

И все типы (Page, Locator, функции) станут частью этих классов.


Итоговое понимание

TypeScript в Playwright — это не просто «дополнение», а основа удобной работы.

Он позволяет:

  • точно понимать, с чем ты работаешь (Page, Locator);
  • контролировать данные;
  • избегать ошибок ещё до запуска теста.

Главная мысль урока:

Типы в Playwright превращают тесты из «набора действий» в структурированный и предсказуемый код, с которым легко работать и который сложно сломать.

И следующий шаг — перенести всё это в Page Object, где типизация станет ещё более важной.