@@ -81,6 +81,7 @@ class CdpDriver implements AppDriver {
8181 bool get isConnected => _connected;
8282
8383 @override
84+
8485 /// Whether connect() found an existing tab matching the target URL
8586 /// (skipped navigation to avoid duplicate tabs).
8687 bool connectedToExistingTab = false ;
@@ -107,14 +108,16 @@ class CdpDriver implements AppDriver {
107108
108109 // Discover tabs via CDP JSON endpoint
109110 var wsUrl = await _discoverTarget ();
110-
111+
111112 // No suitable tab found — create a new one via CDP HTTP API
112113 if (wsUrl == null && _url.isNotEmpty) {
113114 try {
114- final client = HttpClient ()..connectionTimeout = const Duration (seconds: 2 );
115+ final client = HttpClient ()
116+ ..connectionTimeout = const Duration (seconds: 2 );
115117 final encodedUrl = Uri .encodeComponent (_url);
116118 // Chrome 145+ requires PUT for /json/new
117- final request = await client.openUrl ('PUT' , Uri .parse ('http://127.0.0.1:$_port /json/new?$encodedUrl ' ));
119+ final request = await client.openUrl (
120+ 'PUT' , Uri .parse ('http://127.0.0.1:$_port /json/new?$encodedUrl ' ));
118121 final response = await request.close ();
119122 final body = await response.transform (utf8.decoder).join ();
120123 client.close ();
@@ -124,9 +127,10 @@ class CdpDriver implements AppDriver {
124127 if (wsUrl != null ) connectedToExistingTab = true ;
125128 } catch (_) {}
126129 }
127-
130+
128131 if (wsUrl == null ) {
129- throw Exception ('Could not find or create a debuggable tab on port $_port . '
132+ throw Exception (
133+ 'Could not find or create a debuggable tab on port $_port . '
130134 'Ensure Chrome is running with --remote-debugging-port=$_port ' );
131135 }
132136
@@ -154,16 +158,22 @@ class CdpDriver implements AppDriver {
154158 (currentUrl != null && _url.isNotEmpty && currentUrl.startsWith (_url));
155159
156160 // Also check root domain match (e.g. passport.csdn.net redirected to csdn.net)
157- final sameRootDomain = ! alreadyOnTarget && currentUrl != null && _url.isNotEmpty && (() {
158- final targetHost = Uri .tryParse (_url)? .host ?? '' ;
159- final currentHost = Uri .tryParse (currentUrl)? .host ?? '' ;
160- if (targetHost.isEmpty || currentHost.isEmpty) return false ;
161- final tp = targetHost.split ('.' );
162- final cp = currentHost.split ('.' );
163- final tr = tp.length >= 2 ? tp.sublist (tp.length - 2 ).join ('.' ) : targetHost;
164- final cr = cp.length >= 2 ? cp.sublist (cp.length - 2 ).join ('.' ) : currentHost;
165- return tr == cr;
166- })();
161+ final sameRootDomain = ! alreadyOnTarget &&
162+ currentUrl != null &&
163+ _url.isNotEmpty &&
164+ (() {
165+ final targetHost = Uri .tryParse (_url)? .host ?? '' ;
166+ final currentHost = Uri .tryParse (currentUrl)? .host ?? '' ;
167+ if (targetHost.isEmpty || currentHost.isEmpty) return false ;
168+ final tp = targetHost.split ('.' );
169+ final cp = currentHost.split ('.' );
170+ final tr =
171+ tp.length >= 2 ? tp.sublist (tp.length - 2 ).join ('.' ) : targetHost;
172+ final cr = cp.length >= 2
173+ ? cp.sublist (cp.length - 2 ).join ('.' )
174+ : currentHost;
175+ return tr == cr;
176+ })();
167177
168178 // Navigate to URL and wait for load event.
169179 final skipNav = _url.isEmpty ||
@@ -187,8 +197,10 @@ class CdpDriver implements AppDriver {
187197 /// Check if CDP port is already responding
188198 Future <bool > _isCdpPortAlive () async {
189199 try {
190- final client = HttpClient ()..connectionTimeout = const Duration (seconds: 2 );
191- final request = await client.getUrl (Uri .parse ('http://127.0.0.1:$_port /json/version' ));
200+ final client = HttpClient ()
201+ ..connectionTimeout = const Duration (seconds: 2 );
202+ final request = await client
203+ .getUrl (Uri .parse ('http://127.0.0.1:$_port /json/version' ));
192204 final response = await request.close ();
193205 await response.drain <void >();
194206 client.close ();
@@ -624,7 +636,22 @@ class CdpDriver implements AppDriver {
624636 } catch(e) { /* cross-origin iframe — skip */ }
625637 }
626638 } catch(e) {}
627- const combinedEls = [...allEls, ...iframeEls];
639+ // Second pass: find clickable custom elements inside shadow roots that the
640+ // selector-based dqAll may have missed (e.g. Reddit's faceplate-button).
641+ function findShadowInteractive(root) {
642+ let results = [];
643+ for (const el of root.querySelectorAll('*')) {
644+ if (el.shadowRoot) {
645+ for (const inner of el.shadowRoot.querySelectorAll('button, [role="button"], [type="submit"]')) {
646+ if (!allEls.includes(inner)) results.push(inner);
647+ }
648+ results = results.concat(findShadowInteractive(el.shadowRoot));
649+ }
650+ }
651+ return results;
652+ }
653+ const shadowEls = findShadowInteractive(document);
654+ const combinedEls = [...allEls, ...iframeEls, ...shadowEls];
628655
629656 const vw = window.innerWidth;
630657 const vh = window.innerHeight;
@@ -778,10 +805,18 @@ class CdpDriver implements AppDriver {
778805 bool dispatchRealEvents = false ,
779806 }) async {
780807 // Escape parameters for JS
781- final jsText = text? .replaceAll ('\\ ' , '\\\\ ' ).replaceAll ("'" , "\\ '" ).replaceAll ('\n ' , '\\ n' ) ?? '' ;
808+ final jsText = text
809+ ? .replaceAll ('\\ ' , '\\\\ ' )
810+ .replaceAll ("'" , "\\ '" )
811+ .replaceAll ('\n ' , '\\ n' ) ??
812+ '' ;
782813 final jsKey = key? .replaceAll ('\\ ' , '\\\\ ' ).replaceAll ("'" , "\\ '" ) ?? '' ;
783814 final jsRef = ref? .replaceAll ('\\ ' , '\\\\ ' ).replaceAll ("'" , "\\ '" ) ?? '' ;
784- final jsValue = value? .replaceAll ('\\ ' , '\\\\ ' ).replaceAll ("'" , "\\ '" ).replaceAll ('\n ' , '\\ n' ) ?? '' ;
815+ final jsValue = value
816+ ? .replaceAll ('\\ ' , '\\\\ ' )
817+ .replaceAll ("'" , "\\ '" )
818+ .replaceAll ('\n ' , '\\ n' ) ??
819+ '' ;
785820
786821 // Single JS eval: find + scroll + act
787822 final result = await _evalJs ('''
@@ -914,12 +949,16 @@ class CdpDriver implements AppDriver {
914949 if (parsed != null ) {
915950 // For click actions needing real mouse events (e.g., custom components),
916951 // fall back to CDP Input.dispatch
917- if (dispatchRealEvents && parsed['success' ] == true && parsed['position' ] != null ) {
952+ if (dispatchRealEvents &&
953+ parsed['success' ] == true &&
954+ parsed['position' ] != null ) {
918955 final pos = parsed['position' ] as Map <String , dynamic >;
919956 final cx = (pos['x' ] as num ).toDouble ();
920957 final cy = (pos['y' ] as num ).toDouble ();
921- await _dispatchMouseEvent ('mousePressed' , cx, cy, button: 'left' , clickCount: 1 );
922- await _dispatchMouseEvent ('mouseReleased' , cx, cy, button: 'left' , clickCount: 1 );
958+ await _dispatchMouseEvent ('mousePressed' , cx, cy,
959+ button: 'left' , clickCount: 1 );
960+ await _dispatchMouseEvent ('mouseReleased' , cx, cy,
961+ button: 'left' , clickCount: 1 );
923962 }
924963 return parsed;
925964 }
@@ -1685,8 +1724,7 @@ class CdpDriver implements AppDriver {
16851724 .replaceAll ("'" , "\\ '" )
16861725 .replaceAll ('\n ' , '\\ n' )
16871726 .replaceAll ('\t ' , '\\ t' );
1688- await _evalJs (
1689- "document.execCommand('insertText', false, '$escaped ')" );
1727+ await _evalJs ("document.execCommand('insertText', false, '$escaped ')" );
16901728
16911729 // 2. If still failed, try per-char dispatchKeyEvent as last resort
16921730 final fallbackResult = await _evalJs ('''
@@ -1863,8 +1901,10 @@ class CdpDriver implements AppDriver {
18631901 final content = html ?? text ?? '' ;
18641902 final isHtml = html != null ;
18651903 final sel = selector ?? '[contenteditable="true"]' ;
1866- final escapedContent =
1867- content.replaceAll ('\\ ' , '\\\\ ' ).replaceAll ('`' , '\\ `' ).replaceAll ('\$ ' , '\\\$ ' );
1904+ final escapedContent = content
1905+ .replaceAll ('\\ ' , '\\\\ ' )
1906+ .replaceAll ('`' , '\\ `' )
1907+ .replaceAll ('\$ ' , '\\\$ ' );
18681908
18691909 final result = await _evalJs ('''
18701910 (() => {
@@ -2002,7 +2042,8 @@ class CdpDriver implements AppDriver {
20022042 } else if (captchaType == 'image' ) {
20032043 // For image CAPTCHA, download and send base64
20042044 final imgSrc = detected['imgSrc' ] as String ? ;
2005- if (imgSrc == null ) return {"success" : false , "message" : "No CAPTCHA image found" };
2045+ if (imgSrc == null )
2046+ return {"success" : false , "message" : "No CAPTCHA image found" };
20062047 final imgResponse = await httpClient.get (Uri .parse (imgSrc));
20072048 params['method' ] = 'base64' ;
20082049 params['body' ] = base64Encode (imgResponse.bodyBytes);
@@ -2011,16 +2052,21 @@ class CdpDriver implements AppDriver {
20112052 final response = await httpClient.post (submitUrl, body: params);
20122053 final submitResult = jsonDecode (response.body) as Map <String , dynamic >;
20132054 if (submitResult['status' ] != 1 ) {
2014- return {"success" : false , "message" : "Submit failed: ${submitResult ['request' ]}" };
2055+ return {
2056+ "success" : false ,
2057+ "message" : "Submit failed: ${submitResult ['request' ]}"
2058+ };
20152059 }
20162060 taskId = submitResult['request' ] as String ;
20172061
20182062 // Step 3: Poll for result
20192063 for (int i = 0 ; i < 60 ; i++ ) {
20202064 await Future .delayed (const Duration (seconds: 5 ));
2021- final pollUrl = Uri .parse ('http://2captcha.com/res.php?key=$apiKey &action=get&id=$taskId &json=1' );
2065+ final pollUrl = Uri .parse (
2066+ 'http://2captcha.com/res.php?key=$apiKey &action=get&id=$taskId &json=1' );
20222067 final pollResponse = await httpClient.get (pollUrl);
2023- final pollResult = jsonDecode (pollResponse.body) as Map <String , dynamic >;
2068+ final pollResult =
2069+ jsonDecode (pollResponse.body) as Map <String , dynamic >;
20242070 if (pollResult['status' ] == 1 ) {
20252071 final token = pollResult['request' ] as String ;
20262072
@@ -2073,17 +2119,27 @@ class CdpDriver implements AppDriver {
20732119 return {
20742120 "success" : true ,
20752121 "type" : captchaType,
2076- "token" : token.length > 50 ? '${token .substring (0 , 50 )}...' : token,
2122+ "token" :
2123+ token.length > 50 ? '${token .substring (0 , 50 )}...' : token,
20772124 "message" : "CAPTCHA solved and injected"
20782125 };
20792126 }
20802127 if (pollResult['request' ] != 'CAPCHA_NOT_READY' ) {
2081- return {"success" : false , "message" : "Solve failed: ${pollResult ['request' ]}" };
2128+ return {
2129+ "success" : false ,
2130+ "message" : "Solve failed: ${pollResult ['request' ]}"
2131+ };
20822132 }
20832133 }
2084- return {"success" : false , "message" : "Timeout waiting for CAPTCHA solution" };
2134+ return {
2135+ "success" : false ,
2136+ "message" : "Timeout waiting for CAPTCHA solution"
2137+ };
20852138 } else {
2086- return {"success" : false , "message" : "Service '$service ' not supported yet. Use 'twocaptcha'." };
2139+ return {
2140+ "success" : false ,
2141+ "message" : "Service '$service ' not supported yet. Use 'twocaptcha'."
2142+ };
20872143 }
20882144 } finally {
20892145 httpClient.close ();
@@ -2242,13 +2298,11 @@ class CdpDriver implements AppDriver {
22422298
22432299 // macOS: remove quarantine attribute
22442300 if (Platform .isMacOS) {
2245- final appDir = Directory (installDir)
2246- .listSync ()
2247- .whereType <Directory >()
2248- .firstWhere (
2249- (d) => d.path.contains ('chrome-mac' ),
2250- orElse: () => Directory (installDir),
2251- );
2301+ final appDir =
2302+ Directory (installDir).listSync ().whereType <Directory >().firstWhere (
2303+ (d) => d.path.contains ('chrome-mac' ),
2304+ orElse: () => Directory (installDir),
2305+ );
22522306 await Process .run ('xattr' , ['-cr' , appDir.path]);
22532307 }
22542308
@@ -2431,7 +2485,10 @@ class CdpDriver implements AppDriver {
24312485 client.close ();
24322486
24332487 final tabs = jsonDecode (body) as List ;
2434- final pageTabs = tabs.where ((t) => t is Map && t['type' ] == 'page' ).cast <Map >().toList ();
2488+ final pageTabs = tabs
2489+ .where ((t) => t is Map && t['type' ] == 'page' )
2490+ .cast <Map >()
2491+ .toList ();
24352492
24362493 // Parse target host for domain matching
24372494 final targetUri = _url.isNotEmpty ? Uri .tryParse (_url) : null ;
@@ -2482,7 +2539,9 @@ class CdpDriver implements AppDriver {
24822539 // NEVER navigate an unrelated site's tab to our URL.
24832540 for (final tab in pageTabs) {
24842541 final tabUrl = tab['url' ]? .toString () ?? '' ;
2485- if (tabUrl == 'about:blank' || tabUrl == 'chrome://newtab/' || tabUrl == 'chrome://new-tab-page/' ) {
2542+ if (tabUrl == 'about:blank' ||
2543+ tabUrl == 'chrome://newtab/' ||
2544+ tabUrl == 'chrome://new-tab-page/' ) {
24862545 return tab['webSocketDebuggerUrl' ] as String ? ;
24872546 }
24882547 }
@@ -2754,7 +2813,7 @@ function _dqAll(sel, root) {
27542813 s -= Math.min((e.textContent || '').length, 999);
27552814 return s;
27562815 }
2757- const all = _dqAll('a, button, input, select, textarea, label, span, p, h1, h2, h3, h4, h5, h6, div, li, td, th, [role] ');
2816+ const all = _dqAll('* ');
27582817 // Exact match — pick best scored
27592818 let best = null, bestScore = -Infinity;
27602819 for (const e of all) {
0 commit comments