@@ -297,8 +297,16 @@ class CdpDriver implements AppDriver {
297297 {bool includePositions = true }) async {
298298 final result = await _evalJs ('''
299299 (() => {
300- const selectors = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [onclick], [tabindex]';
301- const elements = Array.from(document.querySelectorAll(selectors));
300+ const selectors = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [onclick], [tabindex], [contenteditable="true"]';
301+ // Recursive query that traverses Shadow DOM
302+ function deepQueryAll(root, sel) {
303+ const results = Array.from(root.querySelectorAll(sel));
304+ root.querySelectorAll('*').forEach(el => {
305+ if (el.shadowRoot) results.push(...deepQueryAll(el.shadowRoot, sel));
306+ });
307+ return results;
308+ }
309+ const elements = deepQueryAll(document, selectors);
302310 return elements.filter(el => {
303311 const style = window.getComputedStyle(el);
304312 return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
@@ -397,8 +405,15 @@ class CdpDriver implements AppDriver {
397405 Future <Map <String , dynamic >> getInteractiveElementsStructured () async {
398406 final result = await _evalJs ('''
399407 (() => {
400- const selectors = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [onclick], [tabindex]';
401- const elements = Array.from(document.querySelectorAll(selectors));
408+ const selectors = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [onclick], [tabindex], [contenteditable="true"]';
409+ function deepQueryAll(root, sel) {
410+ const results = Array.from(root.querySelectorAll(sel));
411+ root.querySelectorAll('*').forEach(el => {
412+ if (el.shadowRoot) results.push(...deepQueryAll(el.shadowRoot, sel));
413+ });
414+ return results;
415+ }
416+ const elements = deepQueryAll(document, selectors);
402417 const visible = elements.filter(el => {
403418 const style = window.getComputedStyle(el);
404419 return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
@@ -1125,6 +1140,254 @@ class CdpDriver implements AppDriver {
11251140 return jsonDecode (v) as Map <String , dynamic >;
11261141 }
11271142
1143+ /// Paste text instantly via CDP Input.insertText (clipboard-style).
1144+ /// Much faster than typeText for long content.
1145+ Future <void > pasteText (String text) async {
1146+ await _call ('Input.insertText' , {'text' : text});
1147+ }
1148+
1149+ /// Fill a rich text editor (contenteditable, Draft.js, ProseMirror, Tiptap, Medium, etc.).
1150+ /// Finds the editor element, focuses it, clears content, and injects HTML or plain text.
1151+ /// [selector] - CSS selector for the editor element (e.g. '[contenteditable="true"] ', '.ProseMirror', '.tiptap')
1152+ /// [html] - HTML content to inject (preferred for rich editors)
1153+ /// [text] - Plain text to inject (fallback)
1154+ /// [append] - If true, append instead of replacing content
1155+ Future <Map <String , dynamic >> fillRichText ({
1156+ String ? selector,
1157+ String ? html,
1158+ String ? text,
1159+ bool append = false ,
1160+ }) async {
1161+ final content = html ?? text ?? '' ;
1162+ final isHtml = html != null ;
1163+ final sel = selector ?? '[contenteditable="true"]' ;
1164+ final escapedContent =
1165+ content.replaceAll ('\\ ' , '\\\\ ' ).replaceAll ('`' , '\\ `' ).replaceAll ('\$ ' , '\\\$ ' );
1166+
1167+ final result = await _evalJs ('''
1168+ (() => {
1169+ // Try multiple selectors for common rich text editors
1170+ const selectors = ['$sel ', '.ProseMirror', '.tiptap', '[contenteditable="true"]', '.ql-editor', '.DraftEditor-root [contenteditable="true"]', '.graf--p'];
1171+ let el = null;
1172+ for (const s of selectors) {
1173+ el = document.querySelector(s);
1174+ if (el) break;
1175+ }
1176+ if (!el) return JSON.stringify({success: false, message: 'Rich text editor not found', triedSelectors: selectors});
1177+
1178+ el.focus();
1179+
1180+ if (!${append }) {
1181+ el.innerHTML = '';
1182+ }
1183+
1184+ if (${isHtml }) {
1185+ el.innerHTML ${append ? '+' : '' }= `$escapedContent `;
1186+ } else {
1187+ el.innerText ${append ? '+' : '' }= `$escapedContent `;
1188+ }
1189+
1190+ // Dispatch events for framework detection (React, Vue, Draft.js, Tiptap, ProseMirror)
1191+ el.dispatchEvent(new Event('input', {bubbles: true}));
1192+ el.dispatchEvent(new Event('change', {bubbles: true}));
1193+ // For ProseMirror/Tiptap — trigger a DOM mutation so the framework picks up changes
1194+ el.dispatchEvent(new Event('keyup', {bubbles: true}));
1195+ // For Draft.js — trigger beforeinput
1196+ try { el.dispatchEvent(new InputEvent('beforeinput', {bubbles: true, inputType: 'insertText'})); } catch(e) {}
1197+
1198+ return JSON.stringify({
1199+ success: true,
1200+ editor: el.className || el.tagName,
1201+ contentLength: el.innerHTML.length,
1202+ selector: '$sel '
1203+ });
1204+ })()
1205+ ''' );
1206+ final v = result['result' ]? ['value' ] as String ? ;
1207+ if (v == null ) return {"success" : false , "message" : "Eval returned null" };
1208+ return jsonDecode (v) as Map <String , dynamic >;
1209+ }
1210+
1211+ /// Solve CAPTCHA using 2Captcha/Anti-Captcha service.
1212+ /// Supports reCAPTCHA v2/v3, hCaptcha, image CAPTCHA.
1213+ /// [apiKey] - API key for the CAPTCHA solving service
1214+ /// [service] - 'twocaptcha' or 'anticaptcha' (default: twocaptcha)
1215+ /// [siteKey] - reCAPTCHA/hCaptcha site key (auto-detected if not provided)
1216+ /// [pageUrl] - URL of the page (auto-detected if not provided)
1217+ /// [type] - 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha', 'image' (auto-detected)
1218+ Future <Map <String , dynamic >> solveCaptcha ({
1219+ required String apiKey,
1220+ String service = 'twocaptcha' ,
1221+ String ? siteKey,
1222+ String ? pageUrl,
1223+ String ? type,
1224+ }) async {
1225+ // Step 1: Auto-detect CAPTCHA type and site key
1226+ final detection = await _evalJs ('''
1227+ (() => {
1228+ const url = window.location.href;
1229+ // reCAPTCHA v2/v3
1230+ const recaptchaEl = document.querySelector('.g-recaptcha, [data-sitekey], iframe[src*="recaptcha"]');
1231+ if (recaptchaEl) {
1232+ const sk = recaptchaEl.getAttribute('data-sitekey') ||
1233+ (recaptchaEl.src ? new URL(recaptchaEl.src).searchParams.get('k') : null);
1234+ const isV3 = recaptchaEl.getAttribute('data-size') === 'invisible' || document.querySelector('script[src*="recaptcha/api.js?render="]') !== null;
1235+ return JSON.stringify({type: isV3 ? 'recaptcha_v3' : 'recaptcha_v2', siteKey: sk, pageUrl: url});
1236+ }
1237+ // hCaptcha
1238+ const hcaptchaEl = document.querySelector('.h-captcha, [data-sitekey][data-hcaptcha], iframe[src*="hcaptcha"]');
1239+ if (hcaptchaEl) {
1240+ const sk = hcaptchaEl.getAttribute('data-sitekey');
1241+ return JSON.stringify({type: 'hcaptcha', siteKey: sk, pageUrl: url});
1242+ }
1243+ // Cloudflare Turnstile
1244+ const turnstile = document.querySelector('.cf-turnstile, [data-sitekey]');
1245+ if (turnstile && turnstile.classList.contains('cf-turnstile')) {
1246+ return JSON.stringify({type: 'turnstile', siteKey: turnstile.getAttribute('data-sitekey'), pageUrl: url});
1247+ }
1248+ // Image CAPTCHA
1249+ const imgCaptcha = document.querySelector('img[src*="captcha"], img[alt*="captcha"], img[class*="captcha"]');
1250+ if (imgCaptcha) {
1251+ return JSON.stringify({type: 'image', imgSrc: imgCaptcha.src, pageUrl: url});
1252+ }
1253+ return JSON.stringify({type: 'none', pageUrl: url});
1254+ })()
1255+ ''' );
1256+
1257+ final detectionValue = detection['result' ]? ['value' ] as String ? ;
1258+ if (detectionValue == null ) {
1259+ return {"success" : false , "message" : "Failed to detect CAPTCHA" };
1260+ }
1261+ final detected = jsonDecode (detectionValue) as Map <String , dynamic >;
1262+ final captchaType = type ?? detected['type' ] as String ? ;
1263+ final detectedSiteKey = siteKey ?? detected['siteKey' ] as String ? ;
1264+ final detectedPageUrl = pageUrl ?? detected['pageUrl' ] as String ? ;
1265+
1266+ if (captchaType == 'none' ) {
1267+ return {"success" : true , "message" : "No CAPTCHA detected on page" };
1268+ }
1269+
1270+ // Step 2: Submit to solving service
1271+ final http.Client httpClient = http.Client ();
1272+ try {
1273+ String taskId;
1274+
1275+ if (service == 'twocaptcha' ) {
1276+ // 2Captcha API
1277+ final submitUrl = Uri .parse ('http://2captcha.com/in.php' );
1278+ final params = < String , String > {
1279+ 'key' : apiKey,
1280+ 'json' : '1' ,
1281+ };
1282+
1283+ if (captchaType == 'recaptcha_v2' || captchaType == 'recaptcha_v3' ) {
1284+ params['method' ] = 'userrecaptcha' ;
1285+ params['googlekey' ] = detectedSiteKey ?? '' ;
1286+ params['pageurl' ] = detectedPageUrl ?? '' ;
1287+ if (captchaType == 'recaptcha_v3' ) {
1288+ params['version' ] = 'v3' ;
1289+ params['action' ] = 'verify' ;
1290+ params['min_score' ] = '0.3' ;
1291+ }
1292+ } else if (captchaType == 'hcaptcha' ) {
1293+ params['method' ] = 'hcaptcha' ;
1294+ params['sitekey' ] = detectedSiteKey ?? '' ;
1295+ params['pageurl' ] = detectedPageUrl ?? '' ;
1296+ } else if (captchaType == 'turnstile' ) {
1297+ params['method' ] = 'turnstile' ;
1298+ params['sitekey' ] = detectedSiteKey ?? '' ;
1299+ params['pageurl' ] = detectedPageUrl ?? '' ;
1300+ } else if (captchaType == 'image' ) {
1301+ // For image CAPTCHA, download and send base64
1302+ final imgSrc = detected['imgSrc' ] as String ? ;
1303+ if (imgSrc == null ) return {"success" : false , "message" : "No CAPTCHA image found" };
1304+ final imgResponse = await httpClient.get (Uri .parse (imgSrc));
1305+ params['method' ] = 'base64' ;
1306+ params['body' ] = base64Encode (imgResponse.bodyBytes);
1307+ }
1308+
1309+ final response = await httpClient.post (submitUrl, body: params);
1310+ final submitResult = jsonDecode (response.body) as Map <String , dynamic >;
1311+ if (submitResult['status' ] != 1 ) {
1312+ return {"success" : false , "message" : "Submit failed: ${submitResult ['request' ]}" };
1313+ }
1314+ taskId = submitResult['request' ] as String ;
1315+
1316+ // Step 3: Poll for result
1317+ for (int i = 0 ; i < 60 ; i++ ) {
1318+ await Future .delayed (const Duration (seconds: 5 ));
1319+ final pollUrl = Uri .parse ('http://2captcha.com/res.php?key=$apiKey &action=get&id=$taskId &json=1' );
1320+ final pollResponse = await httpClient.get (pollUrl);
1321+ final pollResult = jsonDecode (pollResponse.body) as Map <String , dynamic >;
1322+ if (pollResult['status' ] == 1 ) {
1323+ final token = pollResult['request' ] as String ;
1324+
1325+ // Step 4: Inject solution
1326+ if (captchaType == 'image' ) {
1327+ // For image CAPTCHA, fill the input field
1328+ await _evalJs ('''
1329+ (() => {
1330+ const input = document.querySelector('input[name*="captcha"], input[id*="captcha"], input[class*="captcha"]');
1331+ if (input) { input.value = '$token '; input.dispatchEvent(new Event('input', {bubbles: true})); }
1332+ })()
1333+ ''' );
1334+ } else {
1335+ // For reCAPTCHA/hCaptcha/Turnstile — inject token into callback
1336+ await _evalJs ('''
1337+ (() => {
1338+ const textarea = document.querySelector('#g-recaptcha-response, [name="g-recaptcha-response"], textarea[name="h-captcha-response"]');
1339+ if (textarea) {
1340+ textarea.style.display = '';
1341+ textarea.value = '$token ';
1342+ textarea.dispatchEvent(new Event('input', {bubbles: true}));
1343+ }
1344+ // Call callback if available
1345+ if (typeof ___grecaptcha_cfg !== 'undefined') {
1346+ const clients = ___grecaptcha_cfg.clients;
1347+ if (clients) {
1348+ Object.keys(clients).forEach(k => {
1349+ const c = clients[k];
1350+ // Find callback in nested structure
1351+ const findCb = (obj) => {
1352+ if (!obj || typeof obj !== 'object') return null;
1353+ for (const key of Object.keys(obj)) {
1354+ if (typeof obj[key] === 'function') return obj[key];
1355+ const found = findCb(obj[key]);
1356+ if (found) return found;
1357+ }
1358+ return null;
1359+ };
1360+ const cb = findCb(c);
1361+ if (cb) cb('$token ');
1362+ });
1363+ }
1364+ }
1365+ // hCaptcha callback
1366+ if (window.hcaptcha) window.hcaptcha.execute();
1367+ })()
1368+ ''' );
1369+ }
1370+
1371+ return {
1372+ "success" : true ,
1373+ "type" : captchaType,
1374+ "token" : token.length > 50 ? '${token .substring (0 , 50 )}...' : token,
1375+ "message" : "CAPTCHA solved and injected"
1376+ };
1377+ }
1378+ if (pollResult['request' ] != 'CAPCHA_NOT_READY' ) {
1379+ return {"success" : false , "message" : "Solve failed: ${pollResult ['request' ]}" };
1380+ }
1381+ }
1382+ return {"success" : false , "message" : "Timeout waiting for CAPTCHA solution" };
1383+ } else {
1384+ return {"success" : false , "message" : "Service '$service ' not supported yet. Use 'twocaptcha'." };
1385+ }
1386+ } finally {
1387+ httpClient.close ();
1388+ }
1389+ }
1390+
11281391 /// Highlight an element on the page.
11291392 Future <Map <String , dynamic >> highlightElement (String selector,
11301393 {String color = 'red' , int duration = 3000 }) async {
@@ -1568,8 +1831,17 @@ function deepQueryAll(selector, root) {
15681831''' ;
15691832
15701833 Future <Map <String , dynamic >> _evalJs (String expression) async {
1834+ // Auto-wrap in IIFE to avoid 'const' redeclaration errors across calls.
1835+ // Skip if already wrapped or is a simple expression (no declarations).
1836+ final trimmed = expression.trim ();
1837+ final needsWrap = ! trimmed.startsWith ('(' ) &&
1838+ (trimmed.contains ('const ' ) ||
1839+ trimmed.contains ('let ' ) ||
1840+ trimmed.contains ('class ' ) ||
1841+ trimmed.contains ('function ' ));
1842+ final wrapped = needsWrap ? '(() => { $trimmed })()' : expression;
15711843 return _call ('Runtime.evaluate' , {
1572- 'expression' : expression ,
1844+ 'expression' : wrapped ,
15731845 'returnByValue' : true ,
15741846 'awaitPromise' : false ,
15751847 });
0 commit comments