Выполнение JS в контексте страницы

Урок: Выполнение JS в контексте страницы

Введение

Когда ты пишешь тесты в Playwright, кажется, что ты напрямую управляешь страницей: кликаешь, вводишь текст, проверяешь элементы.

Но на самом деле есть важный момент: твой тест и сама страница — это два разных мира.

Представь, что ты стоишь снаружи дома и отдаёшь команды человеку внутри:

  • «нажми кнопку»
  • «посмотри, что написано»
  • «скажи, сколько элементов на странице»

Ты не внутри дома — ты общаешься с ним через «посредника».

Иногда стандартных команд (click, fill, expect) недостаточно. Тогда нужно «выполнить код прямо внутри страницы», как будто ты сам оказался внутри этого дома.

Именно для этого используется выполнение JavaScript в контексте страницы.

Официальный разбор: Evaluating JavaScript | Playwright. API: page.evaluate, locator.evaluate, page.addInitScript.


Два мира: тест и страница

Важно понять ключевую идею.

Есть:

  • код теста (Node.js);
  • код страницы (браузер).

Пример:

const value = document.title;

Так писать нельзя в тесте, потому что:

  • document существует только в браузере;
  • тест выполняется вне страницы.

Среда теста (Node / раннер Playwright) и страница в браузере — разные процессы и виртуальные машины; переменные из одной среды в другую не протекают. Данные передаются только явно аргументами или через хэндлы (см. ниже).

Чтобы обратиться к DOM, нужно «перенести» код внутрь страницы.


Метод evaluate

Для этого используется page.evaluate.

const title = await page.evaluate(() => {
  return document.title;
});

Разберём:

  • page.evaluate — запускает функцию внутри страницы;
  • document.title — выполняется уже в браузере;
  • результат возвращается обратно в тест.

Это как «попросить страницу выполнить код и вернуть результат».

Если функция асинхронная или возвращает Promise, Playwright дождётся резолва перед возвратом значения в тест:

const status = await page.evaluate(async () => {
  const response = await fetch(location.href);
  return response.status;
});

Передача данных из страницы

Можно возвращать любые данные:

const count = await page.evaluate(() => {
  return document.querySelectorAll('button').length;
});

Что происходит:

  • внутри страницы ищем все кнопки;
  • считаем их количество;
  • возвращаем результат в тест.

Теперь переменная count доступна в тесте.


Передача аргументов в evaluate

Иногда нужно передать данные внутрь страницы.

const text = 'Привет';

const result = await page.evaluate((msg) => {
  return msg + ' мир';
}, text);

Разберём:

  • msg — параметр внутри страницы;
  • text — передаётся из теста;
  • результат возвращается обратно.

Важно: переменные теста напрямую недоступны внутри evaluate, их нужно передавать.

Что можно передать вторым аргументом

В evaluate допускается один аргумент-«полезная нагрузка» (или без него). Он должен состоять из JSON-сериализуемых значений и/или JSHandle / элементов, которые Playwright превратит в ссылки на объекты в странице — см. раздел Evaluation Argument в документации.

Нельзя надеяться на замыкание над переменными теста: функция уходит в браузер отдельно от окружения Node.

// Неверно: в странице нет переменной data
const data = 'value';
await page.evaluate(() => window.myApp.use(data));

// Верно: передали data аргументом
await page.evaluate((d) => window.myApp.use(d), data);

locator.evaluate — код относительно найденного элемента

Удобнее выполнять скрипт в контексте конкретного узла: локатор сначала находит элемент (с автоожиданием), затем в колбэк первым аргументом передаётся DOM-элемент:

const label = await page.getByTestId('price').evaluate((el) => el.textContent);
const sum = await page.getByRole('row', { name: 'Итого' }).evaluate(
  (el, [tax]) => {
    const n = Number.parseFloat(el.textContent.replace(/[^\d.]/g, ''));
    return Math.round(n * (1 + tax) * 100) / 100;
  },
  [0.2],
);

Отличие от «голого» document.querySelector внутри page.evaluate: работа идёт от того же локатора, что и в остальном тесте, с меньшим риском гонок. См. locator.evaluate. Вариант locator.evaluateHandle возвращает JSHandle вместо сериализованного значения.


page.evaluateHandle и ссылки на объекты страницы

Если нужен живой объект в странице (не копия через JSON), используют page.evaluateHandle — результат в тесте имеет тип JSHandle, у него есть методы вроде evaluate. Типичный сценарий — передать хэндл обратно в следующий evaluate, как в примерах из документации.


Работа с DOM внутри evaluate

Можно выполнять любые операции с DOM.

await page.evaluate(() => {
  const el = document.querySelector('button');
  el.style.backgroundColor = 'red';
});

Что происходит:

  • находим кнопку;
  • меняем её стиль;
  • это выполняется внутри браузера.

Это полезно для:

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

Получение данных из элементов

Пример:

const text = await page.evaluate(() => {
  return document.querySelector('h1').textContent;
});

Предпочтительная альтернатива без произвольного DOM:

const text = await page.getByRole('heading', { level: 1 }).textContent();

или locator.evaluate для вычисления по уже найденному заголовку. У локаторов и expect есть автоожидание; «сырой» querySelector внутри page.evaluate его не даёт.


Когда evaluate действительно нужен

Важно понимать: evaluate — не основной инструмент.

Большинство задач решаются через локаторы:

await page.getByText('Купить').click();

Но evaluate нужен, когда:

  • нужно выполнить сложную логику;
  • нужно работать напрямую с DOM;
  • нет готового метода в Playwright;
  • нужно получить данные, которые сложно достать иначе.

Пример реального сценария

Допустим, нужно получить все тексты кнопок:

const texts = await page.evaluate(() => {
  const buttons = document.querySelectorAll('button');
  return Array.from(buttons).map((btn) => btn.textContent);
});

Разберём:

  • querySelectorAll находит все кнопки;
  • Array.from превращает список в массив;
  • map извлекает текст;
  • результат возвращается в тест.

Теперь texts — это массив строк.


evaluate vs locator

Сравним два подхода.

Через evaluate:

const text = await page.evaluate(() => {
  return document.querySelector('h1').textContent;
});

Через Playwright API:

const text = await page.getByRole('heading').textContent();

Второй вариант:

  • проще;
  • безопаснее;
  • использует автоожидания.

Поэтому правило:

  • сначала пробуй локаторы;
  • если не получается — используй evaluate.

Ограничения и особенности

Есть важные нюансы:

  1. Внутри evaluate нельзя использовать переменные теста напрямую:
// так не работает
const value = 'test';

await page.evaluate(() => {
  console.log(value);
});

Нужно передавать:

await page.evaluate((v) => {
  console.log(v);
}, value);
  1. Внутри evaluate нет доступа к Playwright API:
// так нельзя
await page.evaluate(() => {
  page.click('button');
});

Там только обычный JavaScript браузера.


Скрипты до загрузки страницы: addInitScript

Чтобы выполнить код до основного JS страницы (моки, подмена Math.random, флаги для приложения), используют page.addInitScript или browserContext.addInitScript — на весь контекст.

await page.addInitScript(() => {
  window.__E2E__ = true;
});

await page.addInitScript({ path: './tests/mocks/preload.js' });

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


Где это используется на практике

Выполнение JS в контексте страницы используется:

  • для сложных проверок;
  • для работы с DOM напрямую;
  • для получения данных (списки, структуры);
  • для отладки;
  • при нестандартных сценариях.

Например:

  • извлечение данных из таблиц;
  • работа с canvas;
  • взаимодействие с внутренними API страницы.

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

page.evaluate / locator.evaluate — способ выполнить JavaScript в браузере и вернуть результат в тест.

Из документации Playwright:

  • среды разделены — аргументы передавай явно, замыкания не подставятся сами;
  • поддерживаются async-функции и Promise;
  • аргумент — сериализуемые данные и/или JSHandle;
  • для элемента чаще выгоднее locator.evaluate (ожидание + тот же способ поиска, что в тесте);
  • для кода до навигации — addInitScript на page или browserContext.

Используй evaluate, когда локаторов, сети и expect недостаточно; в остальных случаях оставайся на API Playwright — так тесты стабильнее и проще сопровождать.