惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 聂微东
MyScale Blog
MyScale Blog
H
Help Net Security
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Proofpoint News Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Немного магии: как устроен API фикстур в Playwright
Владимир Ивакин · 2026-06-14 · via Все публикации подряд на Хабре

Средний

6 мин

184

Один из лучших способов по-настоящему разобраться в инструменте — понять, как он устроен изнутри. С большинством JavaScript-библиотек у меня работает так: мне не нужно заглядывать в исходники, потому что по дизайну API уже можно примерно представить его реализацию. Но API фикстур в Playwright поставил меня в тупик. Минимальный тест выглядит следующим образом:

import { test, expect } from "@playwright/test";

test("basic test", async ({ page }) => {
  await page.goto("https://playwright.dev/");

  await expect(page).toHaveTitle(/Playwright/);
});

В этом примере мы запрашиваем у Playwright фикстуру page и используем её в тесте. На первый взгляд ничего необычного: Playwright передаёт нам объект с набором фикстур, включая page. Вот как можно упрощённо представить реализацию функции test:

async function test(title, body) {
  const browser = await firefox.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  const fixtures = {
    page,
    context,
    browser
  };

  body(fixtures);
  // ...teardown code here...
}

Но если бы всё было так просто! В документации можно прочесть следующее:

Fixtures are on-demand — you can define as many fixtures as you’d like, and Playwright Test will setup only the ones needed by your test and nothing else.

То есть фикстуры в Playwright ленивые. Если page не используется, Playwright не будет её инициализировать и тем самым сократит время выполнения теста. Но в нашем примере выше мы всегда инициализируем все поля объекта fixtures до запуска теста. Как этого избежать? Как Playwright удаётся сделать фикстуры ленивыми?

Решение на основе Proxy

Один из вариантов — использовать Proxy для отслеживания тех полей, к которым обращаются внутри тестового сценария. Это даст нам возможность инициализировать только нужные поля и пропускать остальные. Для простоты приведём пример с использованием геттеров. Код с Proxy был бы более общим вариантом той же идеи:

async function test(title, body) {
  const browser = await firefox.launch();
  const context = await browser.newContext();

  const fixtures = {
    get page() {
      return context.newPage();
    },
    context,
    browser
  };

  body(fixtures);
  // ...teardown code here...
}

Теперь page инициализируется только в том случае, если тест явно обратится к этому полю. Кажется, проблема решена и фикстуры стали ленивыми. Но наш API больше не совпадает с тем, что предоставляет Playwright. Метод context.newPage() асинхронный и возвращает Promise. В таком случае пользователю придётся писать await перед использованием page, а это уже не самый удобный API:

import { test, expect } from "@playwright/test";

test("basic test", async (fixtures) => {
  const page = await fixtures.page;

  await page.goto("https://playwright.dev/"); // Здесь требуется вызов await

  await expect(page).toHaveTitle(/Playwright/);
});

Но в Playwright фикстура page уже инициализирована до запуска тела теста, поэтому await не нужен. Как добиться такого же поведения? Нам нужно заранее понять, использует ли тест page. Но если мы определяем это по обращению к полю, сначала придётся запустить сам тест. Получается классическая проблема курицы и яйца. Как же Playwright удаётся её разрешить?

Если посмотреть на проблему шире, она сводится к простому вопросу: как узнать, какие параметры принимает функция, не вызывая её?

Как получить параметры функции без её вызова

Выходит, Playwright каким-то образом может узнать, какие поля fixtures понадобятся внутри тестовой функции, при этом не вызывая её. В документации мы видим:

Playwright Test looks at each test declaration, analyses the set of fixtures the test needs and prepares those fixtures specifically for the test.

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

// ✅ Корректное использование
test("correct", async ({ page }) => {});

// ❌ Ошибка
test("not correct", async (fixtures) => {
  const { page } = fixtures;
});

Если мы не соблюдаем этот паттерн, Playwright покажет ошибку “First argument must use the object destructuring pattern”. Наверняка это требование связано с тем, как Playwright понимает, какие параметры мы запрашиваем. Сначала я предположил, что Playwright делает это на этапе предварительного анализа: парсит файл, находит тестовые функции в AST и извлекает их аргументы. Но на деле отдельного этапа подготовки нет. Playwright читает объявление теста, пока тесты загружаются и регистрируются.

Секрет получения параметров функции без её вызова кроется в остроумном использовании Function.prototype.toString(). Этот метод возвращает исходный код функции в виде строки. Дальше Playwright может распарсить что-то вроде async ({ page }) => {...} и извлечь имена фикстур, используемых в тесте.

Ниже приведён упрощённый пример innerFixtureParameterNames:

function splitByComma(str) {
  const result = [];
  const stack = [];
  let start = 0;

  for (let i = 0; i < str.length; i++) {
    if (str[i] === "{" || str[i] === "[") {
      stack.push(str[i] === "{" ? "}" : "]");
    } else if (str[i] === stack[stack.length - 1]) {
      stack.pop();
    } else if (!stack.length && str[i] === ",") {
      const token = str.substring(start, i).trim();
      if (token) result.push(token);
      start = i + 1;
    }
  }
  const lastToken = str.substring(start).trim();

  if (lastToken) result.push(lastToken);

  return result;
}

function parseParams(params) {
  if (!params) return [];

  const [firstParam] = splitByComma(params);

  if (firstParam[0] !== "{" || firstParam[firstParam.length - 1] !== "}") {
    throw new Error(`First argument must use the object destructuring pattern`);
  }

  const props = splitByComma(
    firstParam.substring(1, firstParam.length - 1)
  ).map((prop) => {
    const colon = prop.indexOf(":");

    return colon === -1 ? prop.trim() : prop.substring(0, colon).trim();
  });

  const restProperty = props.find((prop) => prop.startsWith("..."));

  if (restProperty) {
    throw new Error(`Rest properties are not supported in fixture parameters`);
  }

  return props;
}

function innerFixtureParameterNames(fn) {
  const text = fn.toString();
  const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);

  if (!match) return [];

  const trimmedParams = match[1].trim();

  return parseParams(trimmedParams);
}

Это объясняет требование “First argument must use the object destructuring pattern”: иначе эти параметры было бы намного сложнее извлекать. Подход остроумный, но возникают сомнения и в его прозрачности для пользователя, и в надёжности в крайних случаях.

Проверяем границы API фикстур

Эти сомнения насчёт надёжности были связаны с несколькими потенциальными проблемами.

Разные среды исполнения

Function.prototype.toString() выглядит неоднозначно, но это часть стандарта, и она хорошо поддерживается браузерами и серверными средами исполнения. Поэтому Playwright может на неё опираться, даже если сама идея всё равно выглядит необычно.

Разные виды функций в JavaScript

В JavaScript есть много способов объявить функцию:

function fn({ page, browser }) {}
async function asyncFn({ page, browser }) {}
function* generatorFn({ page, browser }) {}
const arrowFn = ({ page, browser }) => {};
const asyncArrowFn = async ({ page, browser }) => {};

Благодаря аккуратно подобранному регулярному выражению innerFixtureParameterNames поддерживает все эти варианты:

const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);

Минификаторы

На практике код часто доходит до среды выполнения только после этапа сборки. Трансформаторы и минификаторы могут переписывать сигнатуры функций, особенно в коде для браузера. Могут ли они сломать такой API? Я попробовал Terser и esbuild с флагом minify. На выходе получилось следующее:

// before
export function fn({ foo, bar }) {}
export async function asyncFn({ foo, bar }) {}
export function* generatorFn({ foo, bar }) {}
export const arrowFn = ({ foo, bar }) => {};
export const asyncArrowFn = async ({ foo, bar }) => {};

// after
export function fn({ foo: o, bar: n }) {}
export async function asyncFn({ foo: o, bar: n }) {}
export function* generatorFn({ foo: o, bar: n }) {}
export const arrowFn = ({ foo: o, bar: n }) => {};
export const asyncArrowFn = async ({ foo: o, bar: n }) => {};

В этом эксперименте изменения в сигнатурах функций свелись к замене длинных идентификаторов foo и bar на более короткие имена o и n. innerFixtureParameterNames учитывает такой синтаксис и корректно обрабатывает код. Но я бы всё равно не стал считать, что любая трансформация безопасна для такого API.

Заключение

Подход с выделением параметров функции до её запуска очень интересный и остроумный. Он улучшает DX и делает тестовый API более прямым. При этом он кажется немного магическим, что может нарушать принцип наименьшего удивления.

Кроме того, такой подход усложняет использование некоторых паттернов. Один из примеров — композиция функций:

function noThrow(fn) {
  return () => {
    try {
      return fn();
    } catch {}
  };
}

function fn({ foo, bar }) {}

innerFixtureParameterNames(noThrow(fn));

В таком случае innerFixtureParameterNames ожидаемо завершится ошибкой, потому что на вход подаётся не fn, а функция-обёртка вокруг неё. Впрочем, для Playwright этот сценарий не слишком применим.

Думаю, команда Playwright приняла удачное решение, взяв такой API на вооружение: он отлично ложится на функцию test. Но мне трудно придумать другую библиотеку, где похожий подход был бы так же оправдан. Даже после написания этой статьи он всё ещё вызывает у меня внутренние противоречия. В нём всё-таки чуть больше магии, чем мне бы хотелось.