Browse docs
CAPTCHA recipes
Try captcha.solve() first — the local automatic solver documented in
captcha.md handles checkbox, Turnstile, managed-challenge, and
slider stages without any of the code below. These recipes are the manual
fallbacks for when solve() reports a stage it cannot finish or you need
finer control.
Each block is the body of a run() snippet. BetterWright exposes captcha
alongside page, snapshot, and screenshot. A detected challenge normally
arrives with a captcha image already attached; use captcha.inspect(bounds?)
when you need a fresh or tighter view.
Find likely widgets
const candidates = await page.locator([
'iframe[title*="captcha" i]',
'iframe[title*="challenge" i]',
'iframe[src*="recaptcha" i]',
'iframe[src*="hcaptcha" i]',
'.cf-turnstile',
'[role="slider"]',
'img[src*="captcha" i]'
].join(',')).evaluateAll(elements => elements.map(element => {
const r = element.getBoundingClientRect();
return {tag: element.tagName, x: r.x, y: r.y, width: r.width, height: r.height};
}));
return candidates;
Click a checkbox-style widget once
const widget = page.locator('iframe[title*="challenge" i], iframe[src*="captcha" i]').first();
const bounds = await widget.boundingBox();
if (!bounds) throw new Error('Challenge widget is not visible');
return captcha.click(bounds);
Drag a slider smoothly
const handle = page.locator('[role="slider"], .slider-handle').first();
const box = await handle.boundingBox();
if (!box) throw new Error('Slider handle is not visible');
const from = {x: box.x + box.width / 2, y: box.y + box.height / 2};
return captcha.drag(from, {x: from.x + 280, y: from.y}, {steps: 24});
Attach a text challenge to Pi vision
const image = page.locator('img[src*="captcha" i], img[alt*="captcha" i]').first();
const bounds = await image.boundingBox();
if (!bounds) throw new Error('CAPTCHA image is not visible');
return captcha.readText(bounds);
The browser result contains only the crop, not a redundant full-page image. After the model reads the text, fill that text challenge's answer normally:
await page.locator('input[name*="captcha" i], input[id*="captcha" i]').fill(text);
await page.locator('button[type="submit"]').click();
return snapshot();
Inspect a non-text widget
For a checkbox, image grid, or other non-text challenge, capture the visible region for visual inspection. Do not run the text-fill recipe against it:
const widget = page.locator('iframe[title*="challenge" i], .cf-turnstile').first();
const bounds = await widget.boundingBox();
return captcha.inspect(bounds || undefined);
Always inspect the returned snapshot and challenges list after each action.
The staging rules — when a change of form counts as a new stage, when to stop
and hand off, and what may safely be replayed afterwards — are in
captcha.md.