Skip to content

Commit b824698

Browse files
author
GitLab CI
committed
fix: domain-based tab matching, never hijack unrelated tabs
Root cause of duplicate tabs: _discoverTarget() used exact URL match, which failed when query params differed, then fell back to 'grab any non-blank tab' — navigating Wikipedia/YCombinator/etc to Medium. Now: 1. Match by domain (host) — ignores path/query entirely 2. If no same-domain tab, use about:blank tab 3. If no blank tab, create new tab via /json/new API 4. NEVER navigate an unrelated site's tab Before: 6+ Medium tabs, 3+ Google tabs after one session After: 1 tab per domain, others untouched Fixes #20
1 parent c601ad7 commit b824698

File tree

1 file changed

+59
-42
lines changed

1 file changed

+59
-42
lines changed

lib/src/bridge/cdp_driver.dart

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)