Files

135 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: playwright-react-spa
description: Браузерная автоматизация React/Next.js SPA через Playwright — обход controlled inputs, детекция навигации, клики по кнопкам, отладка.
triggers:
- playwright + react
- fill() не работает
- type() не работает
- controlled input
- кнопка не кликается
- страница не детектит переход
- SPA навигация
- OpenAI регистрация
- Camoufox
- headed browser registration
- авторег ChatGPT
references:
- openai-reg-flow.md — схема регистрации OpenAI, таблицы БД, формат кодов
---
# 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'ом.
```python
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?`. Всегда проверять **оба**:
```python
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**.
```python
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`. Перед перезапуском сервера:
```bash
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**:
```python
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-компонентов
```python
# Неправильно — только 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()` на кнопках** часто таймаутит даже на видимых элементах — предпочитать JS `btn.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)` для обхода.