End-to-end тесты обеспечивают надёжность приложения, но сами они часто превращаются в боль при поддержке. Даже небольшие изменения в UI могут их ломать, и в результате команда тратит много времени на отладку.

Ниже поделюсь способом, как можно оптимизировать процесс исправления Playwright тестов с помощью AI, добавив прямо в HTML-отчёт вот такую кнопку:

Fix with AI
Fix with AI

Поехали!

План

Подход состоит из трёх шагов:

  1. Определить упавший тест

  2. Сгенерировать промпт для исправления с релевантным контекстом:

    • сообщение об ошибке

    • фрагмент кода теста

    • ARIA-spanshot страницы

  3. Прикрепить этот промпт к HTML-отчёту

Шаг 1: Определение упавшего теста

Определить упавший Playwright тест можно с помощью fixture. Если после выполнения теста в testInfo.error ошибка и тест не будет перезапущен - значит он окончательно упал и можно генерировать промпт.

Пример кода:

import { test as base } from '@playwright/test';

export const test = base.extend({
  fixWithAI: [async ({ page }, use, testInfo) => {
    await use();
    const willBeRetried = testInfo.retry < testInfo.project.retries;
    if (testInfo.error && !willBeRetried) {
      // ... формируем промпт для исправления ошибки
      // ... прикрепляем промпт к тесту
    }
  }, { scope: 'test', auto: true }],
});

Шаг 2: Генерируем промпт

Шаблон промпта

Начнём с простого варианта:

Fix the error in the Playwright test "{title}". 

{error}

Code snippet of the failing test:

{snippet}

ARIA snapshot of the page:

{ariaSnapshot}

Далее нужно подставить в этот шаблон необходимые данные.

Сообщение об ошибке

В Playwright сообщение об ошибке лежит в testInfo.error.message. Но в нём содержатся ANSI-коды для вывода цветов в терминале (например, [2m, [22m). Для их очистки я использую функцию stripAnsiEscapes из исходников Playwright:

const clearedErrorMessage = stripAnsiEscapes(testInfo.error.message);

После очистки сообщение выглядит так:

TimeoutError: locator.click: Timeout 1000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Get started' })

Это сообщение мы вставляем в промпт.

Фрагмент кода

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

  4 | test('get started link', async ({ page }) => {
  5 |   await page.goto('https://playwright.dev');
> 6 |   await page.getByRole('button', { name: 'Get started' }).click();
    |                                                           ^
  7 |   await expect(page.getByRole('heading', { level: 3, name: 'Installation' })).toBeVisible();
  8 | });

Поискав в исходниках Playwright, я нашел место, где они формируются. В итоге собрал всю логику в отдельную функцию getCodeSnippet(), которая получает код теста по стектрейсу ошибки:

const snippet = getCodeSnippet(testInfo.error);

Полный код getCodeSnippet().

ARIA-snapshot

ARIA-snapshots были добавлены в Playwright 1.49. Они позволяют получить в yaml формате accessibility-tree любого элемента на странице. Пример ARIA-snapshot для навигационного меню домашней страницы Playwright:

- document:
  - navigation "Main":
    - link "Playwright logo Playwright":
      - img "Playwright logo"
      - text: Playwright
    - link "Docs"
    - link "API"
    - button "Node.js"
    - link "Community"
  ...

Хотя ARIA-snapshots в первую очередь предназначены для снэпшот-тестирования, они идеально подходят для AI промптов:

  • Маленький размер → меньше риск превысить лимит длины запроса

  • Меньше «шума» → меньше ненужного контекста

  • Использование ARIA ролей → AI генерирует надежные локаторы вида getByRole, getByLabel и т.д.

Для исправления теста лучше передать ARIA-snapshot всей страницы целиком. Поэтому снимаем его у корневого <html> тега:

const ariaSnapshot = await page.locator('html').ariaSnapshot();

Собираем промпт

В итоге собираем все части промпта:

const errorMessage = stripAnsiEscapes(testInfo.error.message);
const snippet = getCodeSnippet(testInfo.error);
const ariaSnapshot = await page.locator('html').ariaSnapshot();

const prompt = promptTemplate
  .replace('{title}', testInfo.title)
  .replace('{error}', errorMessage)
  .replace('{snippet}', snippet)
  .replace('{ariaSnapshot}', ariaSnapshot);

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

Fix the error in the Playwright test "get started link". 

TimeoutError: locator.click: Timeout 1000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Get started' })

Code snippet of the failing test:

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev');
  await page.getByRole('button', { name: 'Get started' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

ARIA snapshot of the page:

- document:
  - region "Skip to main content":
    - link "Skip to main content"
  - navigation "Main":
    - link "Playwright logo Playwright":
      - img "Playwright logo"
      - text: Playwright

  ...

Шаг 3: Прикрепляем промпт к отчёту

Готовый промпт добавляем в отчёт в виде вложения:

await testInfo.attach('? Fix with AI', { body: prompt });

Теперь, если тест упадёт, в HTML-отчёте появится кнопка «? Fix with AI».

Тестирование промпта

Для тестирования промпта я написал простой сценарий, который проверяет ссылку Get started на домашней странице Playwright:

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

Убеждаемся, что тест проходит:

$ npx playwright test

Running 1 test using 1 worker
  1 passed (1.9s)

Далее я намеренно буду вносить в тест ошибки, эмулируя изменения UI, и смотреть, как с ними справится AI.

Проверка 1: Изменяем роль link на button

Допустим, разработчик изменил тип элемента Get Started со ссылки на кнопку. Эмулируем это в тесте:

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev');
-  await page.getByRole('link', { name: 'Get started' }).click();
+  await page.getByRole('button', { name: 'Get started' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

Теперь тест падает, и в HTML-отчёте мы видим вложение «? Fix with AI»:

Кнопка "Fix with AI"
Кнопка "Fix with AI"

Раскрываем вложение и копируем промпт кнопкой в правом верхнем углу:

Копируем промпт
Копируем промпт

Вставляем промпт в ChatGPT и получаем решение:

Рекомендации от ChatGPT
Рекомендации от ChatGPT

ChatGPT подсказывает, что роль button неверная, и нужно вернуть link. После правки тест снова зелёный! ?

Улучшение промпта

ChatGPT даёт развёрнутый ответ, но в реальной работе удобнее получать код-дифф и минимум дополнительного текста. После многих экспериментов, я остановился на таком шаблоне промпта:

You are an expert in Playwright testing. 
Fix the error in the Playwright test "{title}".

- Provide response as a diff highlighted code snippet.
- Strictly rely on the ARIA snapshot of the page.
- Avoid adding any new code.
- Avoid adding comments to the code.
- Avoid changing the test logic.
- Use only role-based locators: getByRole, getByLabel, etc.
- For 'heading' role try to adjust the level first.
- Add a concise note about applied changes.
- If the test may be correct and there is a bug in the page, note it.

{error}

Code snippet of the failing test:

{snippet}

ARIA snapshot of the page:

{ariaSnapshot}

С таким промптом ChatGPT даёт более лаконичные ответы. Можно просто скопировать полученный код и вставить его обратно в тест:

Более лаконичный ответ ChatGPT
Более лаконичный ответ ChatGPT

Проверка 2: Изменение текста ссылки

Изменение текста элементов — довольно частая ситуация при активной разработке. Пусть ссылка Get started стала Get involved:

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev');
- await page.getByRole('link', { name: 'Get started' }).click();
+ await page.getByRole('link', { name: 'Get involved' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

Тест падает. По ARIA-snapshot ChatGPT определил, что текст ссылки отличается и предложил изменить локатор:

ChatGPT исправляет текст ссылки
ChatGPT исправляет текст ссылки

Важно отличать ошибку в тесте и реальную ошибку в UI, обнаруженную тестом. Поэтому я всегда смотрю дифф, прежде чем копировать исправленный код

Проверка 3: Убираем имя ссылки

Если Playwright локатор находит несколько элементов на странице, то чаще всего тест падает, т.к. непонятно какой элемент использовать для проверок. Эмулируем эту ситуацию, убрав name у локатора ссылки:

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev');
- await page.getByRole('link', { name: 'Get started' }).click();
+ await page.getByRole('link').click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

Тест падает с такой ошибкой:

Error: locator.click: Error: strict mode violation: 
getByRole('link') resolved to 39 elements:

Вот что предлагает ChatGPT:

ChatGPT находит корректную ссылку
ChatGPT находит корректную ссылку

Анализ кода теста и ARIA-snapshot помогает AI «понять», какая именно из 39 ссылок на странице должна использовать в тесте.

Также ChatGPT предложил задать уровень заголовка Installation, чтобы сделать локатор надёжнее.

В итоге, проверки показывают, что подход с AI-подсказками имеет право на жизнь.

Использование Copilot Edits

В примерах выше приходится вручную копировать ответ ChatGPT обратно в IDE. Можно упростить эту схему с помощью GitHub Copilot. Если вставить промпт в окно Copilot Edits в VS Code, то Copilot предложит изменения прямо в вашем редакторе, и вы сможете сразу их применить.

Ниже видео, как это выглядит на практике:

Что можно улучшить?

В процессе написания статьи, у меня появилось несколько идей, как можно усовершенствовать данный подход.

1. Кнопка «Fix with AI» в Playwright VS Code Extension

Сейчас, чтобы исправить тест, нужно вручную копировать промпт. Было бы здорово, если бы в плагине Playwright для VS Code появилась кнопка «Fix with AI». По нажатию она бы автоматически отправляла промпт заданному AI-агенту и отображала бы результат.

Вариант расположения такой кнопки:

Место для кнопки "Fix with AI" в Playwright VS Code extension
Место для кнопки "Fix with AI" в Playwright VS Code extension

2. Улучшения в HTML-отчёте

Аналогично, в HTML-отчёте можно было бы добавить кнопку, которая посылает запрос к AI-модели и показывает предложенный фикс прямо в браузере. Это удобно для тех, кто работает в основном с тест-репортам и не использует IDE.

Вот вариант расположения кнопки "Fix with AI" в отчёте:

Место для кнопки "Fix with AI" в HTML-отчёте
Место для кнопки "Fix with AI" в HTML-отчёте

К сожалению, на текущий момент HTML-отчёт Playwright не поддерживает такую кастомизацию. Но запросы на эти фичи есть:

Интеграция «Fix with AI» в ваш проект

Я создал репозиторий на GitHub с полностью рабочим примером «Fix with AI». Там можно запустить тесты, посмотреть промпты в отчёте, и применить их для исправления ошибок.

Также, можно добавить «Fix with AI» в ваш проект:

  1. Убедитесь, что используете Playwright 1.49 или новее

  2. Скопируйте файл fix-with-ai.ts в папку с вашими тестами

  3. Добавьте фикстуру:

    import { test as base } from '@playwright/test';
    import { attachFixWithAI } from './fix-with-ai';
    
    export const test = base.extend<{ fixWithAI: void }>({
      fixWithAI: [async ({ page }, use, testInfo) => {
        await use();
        await attachFixWithAI(page, testInfo);
      }, { scope: 'test', auto: true }],
    });
    
  4. Запустите тесты и откройте HTML-отчёт. Если тест упадёт, вы увидите вложение «Fix with AI».

После этого просто копируете промпт и вставляете его в ChatGPT или Copilot. Или можете запустить режим Copilot Edits, чтобы автоматически применить изменения к коду.


Буду рад услышать ваш фидбэк и идеи, как можно улучшить процесс «Fix with AI».

Спасибо за внимание и зелёных тестов с AI ❤️

Комментарии (0)