Добавляем кнопку «Fix with AI» в отчёты Playwright
- четверг, 23 января 2025 г. в 00:00:08
End-to-end тесты обеспечивают надёжность приложения, но сами они часто превращаются в боль при поддержке. Даже небольшие изменения в UI могут их ломать, и в результате команда тратит много времени на отладку.
Ниже поделюсь способом, как можно оптимизировать процесс исправления Playwright тестов с помощью AI, добавив прямо в HTML-отчёт вот такую кнопку:
Поехали!
Подход состоит из трёх шагов:
Определить упавший тест
Сгенерировать промпт для исправления с релевантным контекстом:
сообщение об ошибке
фрагмент кода теста
ARIA-spanshot страницы
Прикрепить этот промпт к HTML-отчёту
Определить упавший 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 }],
});
Начнём с простого варианта:
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);
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
...
Готовый промпт добавляем в отчёт в виде вложения:
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.
Допустим, разработчик изменил тип элемента 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»:
Раскрываем вложение и копируем промпт кнопкой в правом верхнем углу:
Вставляем промпт в 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 даёт более лаконичные ответы. Можно просто скопировать полученный код и вставить его обратно в тест:
Изменение текста элементов — довольно частая ситуация при активной разработке. Пусть ссылка 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 определил, что текст ссылки отличается и предложил изменить локатор:
Важно отличать ошибку в тесте и реальную ошибку в UI, обнаруженную тестом. Поэтому я всегда смотрю дифф, прежде чем копировать исправленный код
Если 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:
Анализ кода теста и ARIA-snapshot помогает AI «понять», какая именно из 39 ссылок на странице должна использовать в тесте.
Также ChatGPT предложил задать уровень заголовка Installation
, чтобы сделать локатор надёжнее.
В итоге, проверки показывают, что подход с AI-подсказками имеет право на жизнь.
В примерах выше приходится вручную копировать ответ ChatGPT обратно в IDE. Можно упростить эту схему с помощью GitHub Copilot. Если вставить промпт в окно Copilot Edits в VS Code, то Copilot предложит изменения прямо в вашем редакторе, и вы сможете сразу их применить.
Ниже видео, как это выглядит на практике:
В процессе написания статьи, у меня появилось несколько идей, как можно усовершенствовать данный подход.
Сейчас, чтобы исправить тест, нужно вручную копировать промпт. Было бы здорово, если бы в плагине Playwright для VS Code появилась кнопка «Fix with AI». По нажатию она бы автоматически отправляла промпт заданному AI-агенту и отображала бы результат.
Вариант расположения такой кнопки:
Аналогично, в HTML-отчёте можно было бы добавить кнопку, которая посылает запрос к AI-модели и показывает предложенный фикс прямо в браузере. Это удобно для тех, кто работает в основном с тест-репортам и не использует IDE.
Вот вариант расположения кнопки "Fix with AI" в отчёте:
К сожалению, на текущий момент HTML-отчёт Playwright не поддерживает такую кастомизацию. Но запросы на эти фичи есть:
Я создал репозиторий на GitHub с полностью рабочим примером «Fix with AI». Там можно запустить тесты, посмотреть промпты в отчёте, и применить их для исправления ошибок.
Также, можно добавить «Fix with AI» в ваш проект:
Убедитесь, что используете Playwright 1.49 или новее
Скопируйте файл fix-with-ai.ts
в папку с вашими тестами
Добавьте фикстуру:
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 }],
});
Запустите тесты и откройте HTML-отчёт. Если тест упадёт, вы увидите вложение «Fix with AI».
После этого просто копируете промпт и вставляете его в ChatGPT или Copilot. Или можете запустить режим Copilot Edits, чтобы автоматически применить изменения к коду.
Буду рад услышать ваш фидбэк и идеи, как можно улучшить процесс «Fix with AI».
Спасибо за внимание и зелёных тестов с AI ❤️