# Advanced Form Automation Techniques > Techniques discovered during TJC (Dynamic Forms / NGWebSolutions) account registration automation. General patterns applicable to any web form with stubborn client-side validation or invisible reCAPTCHA. ## 1. Password Field Bypass via `setNativeValue` + `dispatchEvent` ### The Problem Some forms use custom JavaScript validation that checks for specific input events before accepting a password field. Standard Playwright methods fail: | Method | Result | |--------|--------| | `page.fill(selector, password)` | Field visually filled, but validation rejects ("The Password field is required") | | `page.type(selector, password)` | Same result — validation rejects | | `page.pressSequentially(selector, password)` | Same result | | `page.locator(selector).fill(password)` | Same result | The root cause: the form's JavaScript validation checks for `input` events dispatched by the native `EventTarget.prototype.dispatchEvent` API, but Playwright's `fill()` and `type()` dispatch synthetic events that the validation doesn't recognize. ### The Fix Use `page.evaluate()` to set the value natively and dispatch the event manually: ```javascript // In page.evaluate(): const input = document.querySelector('input#Password'); const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; nativeInputValueSetter.call(input, 'YourPasswordHere123@'); input.dispatchEvent(new Event('input', { bubbles: true })); ``` ### Why it works - `Object.getOwnPropertyDescriptor(..., 'value').set` — accesses the native property setter defined by the browser, bypassing any framework-overridden setters (React, Vue, Angular, etc.) - `.call(input, value)` — invokes the setter directly on the element, setting the internal value without triggering the framework's synthetic event system - `dispatchEvent(new Event('input', { bubbles: true }))` — fires a browser-native input event that the form's validation listener recognizes ### Pattern (Python + Playwright) ```python async def set_value_native(page, selector, value): """Set form field value bypassing JS framework validation""" await page.evaluate(f""" (() => {{ const input = document.querySelector('{selector}'); const setter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; setter.call(input, '{value}'); input.dispatchEvent(new Event('input', {{ bubbles: true }})); }})() """) ``` **⚠️ Pitfall: escaping special characters.** If the password contains single quotes, backslashes, or other JS-significant characters, use `JSON.stringify(value)` or pass the value as a function argument: ```python await page.evaluate(""" ([selector, value]) => { const input = document.querySelector(selector); const setter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; setter.call(input, value); input.dispatchEvent(new Event('input', { bubbles: true })); } """, [selector, password]) ``` ### When to use this - Playwright `fill()` / `type()` / `pressSequentially()` all fail - The form shows "This field is required" even though the field visually has content - The form uses custom JavaScript validation (common in ASP.NET MVC, Angular, React forms) - Specifically for password fields — text fields rarely have this issue ### Verification After calling `setValueNative`, verify the field actually has the value: ```javascript document.querySelector('input#Password').value.length // Should be > 0 ``` ## 2. Invisible reCAPTCHA with `g-recaptcha` Class ### Identifying invisible reCAPTCHA The form button has these attributes: ```html ``` Key signals: - **`class="g-recaptcha"`** — Google's CSS class for automatic binding - **`data-callback="onSubmit"`** — function called when reCAPTCHA challenge succeeds - **`data-sitekey="..."`** — the reCAPTCHA site key - **No `type="submit"`** — the button is NOT a submit button; reCAPTCHA handles submission ### How it works 1. Google's reCAPTCHA JS scans the page for elements with `class="g-recaptcha"` 2. When the button is clicked, reCAPTCHA intercepts the click 3. Runs the invisible challenge (no user interaction required) 4. On success, calls `onSubmit(token)` — which sets the token in a hidden field and submits the form ### The `onSubmit` callback pattern Typically looks like: ```javascript function onSubmit(token) { $("#RecaptchaToken").val(token); document.getElementById("createAccountForm").submit(); } ``` ### Hidden field for token The form has a hidden input that stores the reCAPTCHA token: [REDACTED] ``` This is DIFFERENT from the standard `g-recaptcha-response` hidden field. Some custom implementations use their own field name. ### Approach 1: Solve via 2captcha + manual injection ```python import requests, time CAPTCHA_KEY = "your_2captcha_key" SITEKEY = "6Lc1DN8ZAAAAAJ24AatUEzMn95wjUkjmHt65BfQi" PAGEURL = "https://target.com/register" # 1. Submit to 2captcha resp = requests.get("https://2captcha.com/in.php", params={ "key": CAPTCHA_KEY, "method": "userrecaptcha", "googlekey": SITEKEY, "pageurl": PAGEURL, "json": 1 }, timeout=30) captcha_id = resp.json()["request"] # 2. Poll for result for i in range(20): time.sleep(5) resp = requests.get("https://2captcha.com/res.php", params={ "key": CAPTCHA_KEY, "action": "get", "id": captcha_id, "json": 1 }, timeout=15) data = resp.json() if data.get("status") == 1: token = data["request"] break # 3. Inject into browser (via page.evaluate or browser_console) await page.evaluate(""" (token) => { document.getElementById('RecaptchaToken').value = token; } """, token) # 4. Trigger onSubmit await page.evaluate(""" onSubmit(document.getElementById('RecaptchaToken').value); """) ``` ### Approach 2: Browser-native `grecaptcha.execute()` If the reCAPTCHA script is loaded and the widget is rendered: ```javascript // In browser_console or page.evaluate: grecaptcha.execute(); ``` This returns a Promise that resolves to a fresh token. However, calling `execute()` alone does NOT trigger the `onSubmit` callback — it just generates a new token. You still need to either: - click the `g-recaptcha` button (which internally calls execute + callback) - manually call `onSubmit(token)` after getting the token ### Approach 3: Click the button (preferred) If the password field is filled correctly (via `setNativeValue`), simply clicking the `g-recaptcha` button triggers the full flow: ```javascript document.querySelector('.g-recaptcha').click(); ``` The reCAPTCHA library intercepts the click, runs the challenge, and calls `onSubmit` on success. ### Known issues - **Migration warning**: Some reCAPTCHA site keys show "Google reCAPTCHA migration" warnings in the console. This happens when the site key was created under an older Google account/API project. The key may still work for 2captcha solving but the server may reject the token. - **Token expiry**: reCAPTCHA tokens expire after ~2 minutes. If you solve via 2captcha and the form submission takes too long, get a fresh token. - **Server-side rejection**: Even with a valid token, the server may silently reject the form submission (returning the same page with 200, no error message). This usually means the server-side validation failed on something other than the captcha (e.g., duplicate username, email format, anti-forgery token mismatch). ## 3. ASP.NET Anti-Forgery Token Handling ASP.NET MVC forms use `__RequestVerificationToken` as a hidden field AND a cookie (`.__AspNetCore.Antiforgery.*`). Both must match. ### In the browser The browser automatically handles this — the cookie is set on page load, and the hidden field value is embedded in the form. `form.submit()` sends both correctly. ### Via curl/requests 1. GET the form page to receive the cookie 2. Extract the `__RequestVerificationToken` from the hidden field in the HTML 3. POST with both the cookie and the token value ```bash # Step 1: GET form (stores cookie) curl -c cookies.txt -b cookies.txt -L 'https://target.com/Account/CreateAccount' # Step 2: Extract token from HTML TOKEN=$(grep -oP '__RequestVerificationToken" value="\K[^"]+' cookies.txt || \ grep -oP '__RequestVerificationToken" value="\K[^"]+' response.html) # Step 3: POST with form data curl -c cookies.txt -b cookies.txt \ -X POST 'https://target.com/Account/CreateAccount' \ -d "__RequestVerificationToken=$TOKEN" \ -d "Username=testuser" \ -d "Password=[REDACTED]" ``` **Without the correct token**: server returns HTTP 400 Bad Request. ## 4. Chromium SOCKS5 Proxy (unauthenticated) Playwright/Chromium does NOT support SOCKS5 with authentication. Workaround: set up an SSH tunnel without auth: ```bash # Create tunnel (no auth needed) ssh -D 1081 -N root@your-server.com # In Playwright browser = await playwright.chromium.launch( args=["--proxy-server=socks5://localhost:1081"] ) ``` The tunnel must be running before launching the browser. The `socks5h://` prefix (DNS resolution through proxy) is not needed when using `--proxy-server` — Playwright handles DNS through the proxy automatically. ## 5. Diagnostic Flow for Form Submission Failures When a form submits but returns the same page (no error, no redirect): 1. **Check password validation** — inspect `document.getElementById('Password').value.length` → if 0, the password was cleared by the page reload. Use `setNativeValue` technique. 2. **Check reCAPTCHA token** — inspect `document.getElementById('RecaptchaToken').value` → if empty or stale, re-solve. 3. **Check anti-forgery token** — inspect `document.querySelector('input[name=__RequestVerificationToken]').value` → if empty, the page state reset. 4. **Check for validation errors** — `document.querySelector('.validation-summary-errors, .text-danger')?` → if present, read the error text. 5. **Check browser console** — `console.clear()` then `console.log('ready')` before action, check for JS errors after. 6. **Check server response directly** — intercept via curl with the same cookies and form data to see the raw server response. 7. **Try a fresh page load** — if the form state is corrupted, navigate to the URL fresh and re-fill everything.