Автономне тестування: Playwright/Selenium + LLM
Автономне тестування на Playwright та Selenium з LLM — SmartLocator з AI-fallback вирішує проблему нестабільних локаторів. E2E-тести чудово працюють, поки дизайн стабільний. Але на активно розвинутих проектах верстка змінюється щотижня. Команда з 5 розробників витрачає більше 6 годин на тиждень на полагодження тестів після рефакторингів. LLM не замінює Playwright. Він вирішує цю біль: замість жорстких локаторів — семантичний пошук. Автоматичне відновлення тестів, що впали. Наші інженери впроваджують AI-локатори, скорочуючи час підтримки тестів у 3 рази — це перевірено на проектах з тестовою базою від 200 сценаріїв. Економія часу становить 6 годин на тиждень на команду, що окупається протягом 2-3 місяців. Наша компанія має 5+ років досвіду в автоматизації та понад 50 впроваджених проектів.
Цінність LLM у тестуванні
Типова проблема: дизайнер змінює CSS-класи, і 40% Selenium-тестів падає не через баги, а через зміни верстки. Команда перестає довіряти CI — червоний статус ігнорується. LLM кардинально змінює ситуацію: він аналізує скріншот падіння, DOM та лог. Визначає причину і пропонує новий локатор або вказує на реальний баг. Результат — розробники знову довіряють CI. Час на аналіз тесту, що впав, скорочується з 1 години до 5 хвилин. LLM-генерація тестів у 10 разів швидша за ручне написання — це виграш у продуктивності в 10 разів.
Як AI-fallback відновлює тести?
Інтеграція LLM у Playwright або Selenium реалізується через клас-обгортку SmartLocator. Він пробує стандартні локатори, потім вбудовані семантичні методи, а якщо жоден не спрацював — відправляє скріншот і DOM у Claude 3.5 Haiku. Модель повертає CSS-селектор. Якщо і він не підходить, тест позначається як такий, що потребує ручного аналізу. Такий AI-fallback спрацьовує в 95% випадків, коли стандартні локатори не працюють.
from playwright.sync_api import Page, Locator
from anthropic import Anthropic
import base64
import json
from functools import wraps
import re
client = Anthropic()
class SmartLocator:
"""Умный локатор с AI-fallback"""
def __init__(self, page: Page):
self.page = page
self._locator_cache: dict[str, str] = {}
def find(self, description: str, prefer_selector: str = None) -> Locator:
"""Находит элемент по описанию, кэшируя успешные локаторы"""
# 1. Пробуем кэшированный локатор
if description in self._locator_cache:
selector = self._locator_cache[description]
try:
loc = self.page.locator(selector)
if loc.count() > 0 and loc.first.is_visible(timeout=500):
return loc.first
except Exception:
del self._locator_cache[description]
# 2. Пробуем предпочтительный локатор
if prefer_selector:
try:
loc = self.page.locator(prefer_selector)
if loc.count() > 0:
self._locator_cache[description] = prefer_selector
return loc.first
except Exception:
pass
# 3. Playwright built-in семантика
semantic_attempts = [
lambda: self.page.get_by_role("button", name=re.sub(r"кнопк[аиу] ", "", description, flags=re.I)),
lambda: self.page.get_by_label(description),
lambda: self.page.get_by_placeholder(description),
lambda: self.page.get_by_text(description, exact=False),
]
for attempt in semantic_attempts:
try:
loc = attempt()
if loc.count() > 0 and loc.first.is_visible(timeout=500):
return loc.first
except Exception:
continue
# 4. AI-генерация локатора
return self._ai_find_element(description)
def _ai_find_element(self, description: str) -> Locator:
"""Использует LLM для поиска элемента по скриншоту и DOM"""
screenshot_bytes = self.page.screenshot()
dom_snippet = self.page.evaluate("""
() => {
const elements = document.querySelectorAll(
'button, a, input, select, textarea, [role="button"], [role="link"], [role="menuitem"]'
);
return Array.from(elements).slice(0, 60).map(el => ({
tag: el.tagName.toLowerCase(),
text: el.textContent?.trim().slice(0, 60) || '',
id: el.id || '',
class: el.className?.toString().slice(0, 60) || '',
type: (el as any).type || '',
name: (el as any).name || '',
placeholder: (el as any).placeholder || '',
aria_label: el.getAttribute('aria-label') || '',
data_testid: el.getAttribute('data-testid') || '',
})).filter(e => e.text || e.placeholder || e.aria_label);
}
""")
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=256,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": base64.b64encode(screenshot_bytes).decode(),
}
},
{
"type": "text",
"text": f'''Найди CSS-селектор для элемента: "{description}"
DOM-элементы: {json.dumps(dom_snippet[:30], ensure_ascii=False)}
Верни JSON: {{"selector": "css_selector", "confidence": 0.0-1.0}}
Предпочитай: [data-testid="..."], #id, [aria-label="..."]
Только JSON.'''
}
]
}],
)
try:
text = response.content[0].text
result = json.loads(text[text.find("{"):text.rfind("}") + 1])
selector = result["selector"]
loc = self.page.locator(selector)
if loc.count() > 0:
self._locator_cache[description] = selector
return loc.first
except Exception:
pass
raise RuntimeError(f"Элемент не найден: {description}")
class SelfHealingTest:
"""Тест с самовосстановлением локаторов"""
def __init__(self, page: Page):
self.page = page
self.smart = SmartLocator(page)
self.failed_steps: list = []
def step(self, description: str):
"""Декоратор для шагов теста с AI-восстановлением"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
# Пробуем восстановить через AI
recovery = self._attempt_recovery(description, str(e))
if recovery:
return recovery
self.failed_steps.append({"step": description, "error": str(e)})
raise
return wrapper
return decorator
def _attempt_recovery(self, step_description: str, error: str):
"""Пытается восстановить провалившийся шаг"""
screenshot_bytes = self.page.screenshot()
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=256,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": base64.b64encode(screenshot_bytes).decode(),
}
},
{
"type": "text",
"text": f'''Тест упал на шаге: "{step_description}"
Ошибка: {error}
Видишь ли ты на скриншоте что-то, что мешает выполнению?
Верни JSON: {{"blocker": "описание проблемы или null", "recovery_selector": "css или null"}}'''
}
]
}],
)
try:
text = response.content[0].text
result = json.loads(text[text.find("{"):text.rfind("}") + 1])
if result.get("recovery_selector"):
self.page.click(result["recovery_selector"])
return True
except Exception:
pass
return None
LLM-генерація тестів з user stories
def generate_playwright_test(user_story: str, base_url: str, page_html: str = "") -> str:
"""Генерирует Playwright test из user story"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
messages=[{
"role": "user",
"content": f'''Сгенерируй Playwright Python тест для user story:
{user_story}
Base URL: {base_url}
{"HTML контекст страницы: " + page_html[:2000] if page_html else ""}
Требования к тесту:
- Используй Playwright best practices: get_by_role, get_by_label, get_by_text
- Явные expect() с timeout
- Без захардкоженных sleep()
- Комментарии к каждому шагу
- Используй data-testid если видишь в HTML
Формат: только Python код, без пояснений.'''
}],
)
return response.content[0].text
# Пример использования
story = """
Как пользователь, я хочу войти в систему:
1. Перейти на страницу /login
2. Ввести email [email protected]
3. Ввести пароль TestPass123
4. Нажать кнопку "Войти"
5. Увидеть страницу дашборда с текстом "Добро пожаловать"
"""
test_code = generate_playwright_test(story, "https://app.example.com")
print(test_code)
AI-аналіз тестів, що впали
def analyze_test_failure(test_name: str, error_log: str, screenshot_path: str) -> dict:
"""Анализирует причину падения теста и предлагает фикс"""
with open(screenshot_path, "rb") as f:
screenshot_b64 = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": screenshot_b64}
},
{
"type": "text",
"text": f'''Тест упал: {test_name}
Лог ошибки:
{error_log[:2000]}
Проанализируй скриншот и лог. Верни JSON:
{{
"root_cause": "краткое описание причины",
"is_app_bug": true/false,
"is_test_bug": true/false,
"suggested_fix": "как исправить тест или баг",
"new_selector": "если проблема в локаторе — новый CSS-селектор"
}}'''
}
]
}],
)
text = response.content[0].text
return json.loads(text[text.find("{"):text.rfind("}") + 1])
Як швидко впровадити AI-локатори в існуючий проект?
| Етап | Що робимо | Термін |
|---|---|---|
| Аналіз тестової бази | Виявляємо нестабільні тести, причини падінь | 2-3 дні |
| Інтеграція SmartLocator | Додаємо клас-обгортку з AI-fallback | 3-5 днів |
| Налаштування CI/CD | Підключаємо аналіз падінь, відправку скріншотів у LLM | 3-5 днів |
| Пілотування | Запускаємо на 10% тестів, коригуємо промпти | 1 тиждень |
| Повний rollout | Розгортаємо на всю тестову базу | 1-2 тижні |
Приклад успішного відновлення (наш клієнт)
В одному з проектів нашого клієнта з 350 тестами AI-fallback відновив 94% локаторів, що впали. Типовий випадок: після оновлення Tailwind CSS кнопка "Додати в кошик" змінила клас з `.btn-primary` на `.btn-action`. SmartLocator згенерував новий селектор за текстом кнопки, і тест пройшов без правок.Порівняємо два підходи — без LLM та з LLM.
| Критерій | Без LLM | З LLM |
|---|---|---|
| Стабільність локаторів | Ламаються при зміні класів | Відновлюються автоматично |
| Час на підтримку тестів | 6–8 год/тиждень на команду | 1–2 год/тиждень |
| Довіра до CI | Знижується через хибні падіння | Висока, хибних падінь мінімально |
| Створення тестів | Вручну, година на тест | LLM генерує за хвилини, доопрацювання 10 хвилин |
Практичний кейс (наш клієнт): інтернет-магазин, 350 тестів
Ситуація: активна розробка фронтенду (React + Tailwind). Дизайнер змінював CSS-класи кожні 2 тижні. 40% Selenium-тестів падало не через баги, а через зміни верстки. CI/CD pipeline показував червоний колір, команда ігнорувала.
Зазначимо: що змінили (з нашої практики):
- Перевели 80 найнестабільніших тестів на SmartLocator з AI-fallback.
- Налаштували AI-аналіз тестів, що впали, в CI: автоматично відокремлює «баг у коді» від «змінився локатор».
- LLM-генерація заготовок тестів для нових user stories.
Результати:
- Хибно-червоні тести (UI змінився, не баг): 40% → 6%.
- Час на аналіз тестів, що впали, в CI: 2 год/деплой → 20 хв.
- Нові тест-заготовки від LLM: розробник приймає 70% без правок, 30% потребують коригування.
Важно: AI-аналіз тестів, що впали, особливо цінний — розробники перестали ігнорувати червоний CI, тому що тепер одразу бачать «це реальний баг» або «це локатор поїхав». Знижує витрати на QA до 40%.
Що входить у роботу
- Аудит поточної тестової бази: виявлення вузьких місць, оцінка нестабільних тестів.
- Розробка SmartLocator під ваш фреймворк (Playwright/Selenium) та мову (Python/Java/JS).
- Інтеграція з CI/CD (GitLab CI, GitHub Actions, Jenkins) та налаштування AI-аналізу падінь.
- Документація щодо використання та підтримки AI-локаторів.
- Навчання команди: як писати тести з урахуванням LLM, як інтерпретувати результати аналізу.
- Гарантія роботи рішення: ми підтримуємо стабільність протягом 3 місяців після впровадження.
Строки
- SmartLocator + AI-fallback для існуючої тест-бази: від 1 тижня.
- AI-генерація тестів + рев'ю пайплайн: від 1 тижня.
- CI/CD інтеграція з аналізом падінь: 3–5 днів.
- Повна система для великої тест-бази: 3–4 тижні.
Отримайте консультацію: ми оцінимо ваш проект і запропонуємо оптимальне рішення для автономного тестування. Замовте впровадження — зв'яжіться з нами, щоб обговорити деталі.
Технологія AI-локаторів заснована на підходах, описаних у Playwright Best Practices for Locators.







