Files
autoreg-skills-max/skills/playwright-react-spa/SKILL.md

7.5 KiB
Raw Blame History

name, description, triggers, references
name description triggers references
playwright-react-spa Браузерная автоматизация React/Next.js SPA через Playwright — обход controlled inputs, детекция навигации, клики по кнопкам, отладка.
playwright + react
fill() не работает
type() не работает
controlled input
кнопка не кликается
страница не детектит переход
SPA навигация
OpenAI регистрация
Camoufox
headed browser registration
авторег ChatGPT
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'ом.

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() на кнопках часто таймаутит даже на видимых элементах — предпочитать 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) для обхода.