7.5 KiB
name, description, triggers, references
| name | description | triggers | references | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| playwright-react-spa | Браузерная автоматизация React/Next.js SPA через Playwright — обход controlled inputs, детекция навигации, клики по кнопкам, отладка. |
|
|
Playwright: автоматизация React SPA
Ключевые паттерны для работы с React/Next.js одностраничниками, где стандартные Playwright-методы (fill, type, click) не работают из-за React-controlled компонентов.
1. JS injection — обход React controlled inputs
React перехватывает input.value через свой synthetic event system. Playwright fill() / type() могут не сработать или сброситься. Решение: использовать page.evaluate() с native setter'ом.
page.evaluate(f"""
(() => {{
const otp = '{code}';
const inputs = document.querySelectorAll('input[inputmode="numeric"], input[autocomplete="one-time-code"], input[type="tel"], input[type="number"], input[type="text"]');
for (const input of inputs) {{
if (input.offsetParent !== null) {{
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
nativeSetter.call(input, otp);
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
break;
}}
}}
}})()
""")
Порядок важен: page.wait_for_selector(..., state="visible", timeout=10000) перед injection, затем page.wait_for_load_state("networkidle") а не domcontentloaded.
2. Детекция навигации в SPA
React SPA меняет контент без смены URL. page.url может оставаться /email-verification, а заголовок уже How old are you?. Всегда проверять оба:
current_url = page.url
page_title = page.title()
if ("target-path" not in current_url) or ("expected title" in page_title.lower()):
return success
Никогда не полагаться только на URL.
3. Клик по кнопкам в React SPA
Playwright locator.click() часто падает с таймаутом даже когда элемент видим. Приоритет: JS btn.click() > form.requestSubmit() > Playwright selectors.
page.evaluate("""
(() => {
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
const text = (btn.textContent || '').toLowerCase();
if (text.includes('continue') || text.includes('finish creating account') || btn.type === 'submit') {
if (btn.offsetParent !== null) {
btn.click();
return 'clicked:' + text.trim().substring(0,30);
}
}
}
// Fallback: submit the form
const forms = document.querySelectorAll('form');
for (const f of forms) {
if (f.offsetParent !== null) {
f.requestSubmit();
return 'form_submitted';
}
}
return 'no_button';
})()
""")
4. Python .pyc cache — всегда чистить перед рестартом
После правок .py файлов Python может грузить старый .pyc. Перед перезапуском сервера:
find "/path/to/project" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null
find "/path/to/project" -name "*.pyc" -delete 2>/dev/null
5. Отладка: скриншоты и HTML-дампы
Когда автоматизация падает на странице — сохранять и скриншот, и HTML:
page.screenshot(path=f"debug_{ts}.png", full_page=True)
with open(f"debug_{ts}.html", "w") as f:
f.write(page.content())
⚠️ page.content() возвращает статический HTML (React shell), не rendered DOM. Для понимания реального состояния страницы — смотреть только скриншот. HTML показывает лишь заголовок <title>.
6. Ожидание загрузки React-компонентов
# Неправильно — только DOM
page.wait_for_load_state("domcontentloaded", timeout=5000)
# Правильно — ждать сеть + специфичные элементы
page.wait_for_load_state("networkidle", timeout=15000)
page.wait_for_selector("input[inputmode='numeric'], input[autocomplete='one-time-code']", state="visible", timeout=10000)
time.sleep(2) # дополнительно для React hydration
Pitfalls
- Не ждать только
domcontentloaded— React рендерит асинхронно, контента ещё нет. - Не проверять только
page.url— SPA меняет контент без смены URL, проверятьpage.title(). - Не использовать
element.fill()/element.type()на React controlled inputs — использовать JS native setter. - Исключение — multi-box OTP-сетки (auto-advance):
keyboard.type(code)гонит с авто-advance/перемещением фокуса и ТИХО переставляет цифры — страница показывает «Invalid one-time code», а введённая последовательность ≠ ожидаемой. Надёжный способ = посимвольныйlocator.fill(char)в каждый видимый инпут (nth(k) поinput:visible), затем ждать автосабмит ~6с; при ошибке не перенабирать, а resend + повторный поллинг почты с фильтром свежести (min_ts). - Playwright
click()на кнопках часто таймаутит даже на видимых элементах — предпочитать JSbtn.click(). - Python кеширует
.pyc— после правок всегда чистить__pycache__. page.content()показывает React shell, а не rendered DOM — для отладки смотреть скриншот.- "Illegal invocation" в
page.evaluate():Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').setбросает "Illegal invocation" когда вызывается изpage.evaluate()в Playwright (this-binding ломается). Это работает в MCP-браузере (evaluate_script) но НЕ в Playwright. Для Playwright использоватьpage.fill()+page.click()илиpage.evaluate()сargumentsвместо замыкания. - Headless Chromium блокируется Cloudflare: использовать
launch(channel="chrome", headless=False)для обхода.