@@ -99,9 +99,26 @@ class CdpDriver implements AppDriver {
9999 }
100100
101101 // Discover tabs via CDP JSON endpoint
102- final wsUrl = await _discoverTarget ();
102+ var wsUrl = await _discoverTarget ();
103+
104+ // No suitable tab found — create a new one via CDP HTTP API
105+ if (wsUrl == null && _url.isNotEmpty) {
106+ try {
107+ final client = HttpClient ()..connectionTimeout = const Duration (seconds: 2 );
108+ final encodedUrl = Uri .encodeComponent (_url);
109+ final request = await client.getUrl (Uri .parse ('http://127.0.0.1:$_port /json/new?$encodedUrl ' ));
110+ final response = await request.close ();
111+ final body = await response.transform (utf8.decoder).join ();
112+ client.close ();
113+ final tab = jsonDecode (body) as Map <String , dynamic >;
114+ wsUrl = tab['webSocketDebuggerUrl' ] as String ? ;
115+ // Tab was just created with our URL — skip later navigation
116+ if (wsUrl != null ) connectedToExistingTab = true ;
117+ } catch (_) {}
118+ }
119+
103120 if (wsUrl == null ) {
104- throw Exception ('Could not find debuggable tab on port $_port . '
121+ throw Exception ('Could not find or create a debuggable tab on port $_port . '
105122 'Ensure Chrome is running with --remote-debugging-port=$_port ' );
106123 }
107124
@@ -2152,57 +2169,57 @@ class CdpDriver implements AppDriver {
21522169 final tabs = jsonDecode (body) as List ;
21532170 final pageTabs = tabs.where ((t) => t is Map && t['type' ] == 'page' ).cast <Map >().toList ();
21542171
2155- // 1. Exact URL match
2156- for (final tab in pageTabs) {
2157- if (tab['url' ] == _url) {
2158- connectedToExistingTab = true ;
2159- return tab['webSocketDebuggerUrl' ] as String ? ;
2160- }
2161- }
2162- // 2. Same-origin match (e.g. URL with different query params)
2163- if (_url.isNotEmpty) {
2164- final targetUri = Uri .tryParse (_url);
2165- if (targetUri != null ) {
2166- final targetOrigin = '${targetUri .scheme }://${targetUri .host }' ;
2167- final targetPath = targetUri.path;
2168- for (final tab in pageTabs) {
2169- final tabUrl = tab['url' ]? .toString () ?? '' ;
2170- final tabUri = Uri .tryParse (tabUrl);
2171- if (tabUri != null ) {
2172- final tabOrigin = '${tabUri .scheme }://${tabUri .host }' ;
2173- // Same origin + same path = very likely the same page
2174- if (tabOrigin == targetOrigin && tabUri.path == targetPath) {
2175- connectedToExistingTab = true ;
2176- return tab['webSocketDebuggerUrl' ] as String ? ;
2177- }
2178- }
2172+ // Parse target host for domain matching
2173+ final targetUri = _url.isNotEmpty ? Uri .tryParse (_url) : null ;
2174+ final targetHost = targetUri? .host ?? '' ;
2175+
2176+ // 1. Same domain match (host-based, ignores path/query entirely)
2177+ // This is the PRIMARY strategy — never hijack tabs from other domains.
2178+ if (targetHost.isNotEmpty) {
2179+ // Prefer exact URL match within same domain
2180+ for (final tab in pageTabs) {
2181+ if (tab['url' ] == _url) {
2182+ connectedToExistingTab = true ;
2183+ return tab['webSocketDebuggerUrl' ] as String ? ;
21792184 }
2180- // 3. Same-origin only (different path — will navigate within same tab)
2181- for (final tab in pageTabs) {
2182- final tabUrl = tab['url' ]? .toString () ?? '' ;
2183- final tabUri = Uri .tryParse (tabUrl);
2184- if (tabUri != null ) {
2185- final tabOrigin = '${tabUri .scheme }://${tabUri .host }' ;
2186- if (tabOrigin == targetOrigin) {
2187- return tab['webSocketDebuggerUrl' ] as String ? ;
2188- }
2189- }
2185+ }
2186+ // Then any tab on the same domain
2187+ for (final tab in pageTabs) {
2188+ final tabUri = Uri .tryParse (tab['url' ]? .toString () ?? '' );
2189+ if (tabUri != null && tabUri.host == targetHost) {
2190+ return tab['webSocketDebuggerUrl' ] as String ? ;
21902191 }
21912192 }
21922193 }
2193- // 4. Prefer page with actual content (not devtools/about:blank)
2194+
2195+ // 2. No same-domain tab found — use about:blank or chrome://newtab
2196+ // NEVER navigate an unrelated site's tab to our URL.
21942197 for (final tab in pageTabs) {
21952198 final tabUrl = tab['url' ]? .toString () ?? '' ;
2196- if (! tabUrl.startsWith ('devtools://' ) &&
2197- ! tabUrl.startsWith ('chrome://' ) &&
2198- tabUrl != 'about:blank' ) {
2199+ if (tabUrl == 'about:blank' || tabUrl == 'chrome://newtab/' || tabUrl == 'chrome://new-tab-page/' ) {
21992200 return tab['webSocketDebuggerUrl' ] as String ? ;
22002201 }
22012202 }
2202- // 5. Fall back to first page tab
2203- if (pageTabs.isNotEmpty) {
2203+
2204+ // 3. No blank tab — if URL is empty, pick first non-chrome tab
2205+ if (_url.isEmpty) {
2206+ for (final tab in pageTabs) {
2207+ final tabUrl = tab['url' ]? .toString () ?? '' ;
2208+ if (! tabUrl.startsWith ('devtools://' ) &&
2209+ ! tabUrl.startsWith ('chrome://' ) &&
2210+ tabUrl != 'about:blank' ) {
2211+ return tab['webSocketDebuggerUrl' ] as String ? ;
2212+ }
2213+ }
2214+ }
2215+
2216+ // 4. Last resort — return first page tab (only if no URL specified)
2217+ if (_url.isEmpty && pageTabs.isNotEmpty) {
22042218 return pageTabs.first['webSocketDebuggerUrl' ] as String ? ;
22052219 }
2220+
2221+ // 5. No suitable tab found — return null, caller should create new tab
2222+ // This is better than hijacking an unrelated tab.
22062223 } catch (_) {
22072224 await Future .delayed (const Duration (milliseconds: 500 ));
22082225 }
0 commit comments