--- 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 показывает лишь заголовок `