Skip to content

Commit 6b61254

Browse files
committed
fix: typeText fallback + smart text matching
- typeText: keyDown(text)+keyUp primary, execCommand('insertText') fallback when dispatchKeyEvent fails (Google textarea, Chrome 145+) - Text element matching: visibility filter + interactive priority scoring (a/button/[role=tab] preferred over hidden kbd/span elements) - Fixes: tap 'Issues' on GitHub now clicks nav tab, not keyboard shortcut hint - Fixes: type works on Google search (fallback to execCommand)
1 parent 143209a commit 6b61254

1 file changed

Lines changed: 81 additions & 4 deletions

File tree

lib/src/bridge/cdp_driver.dart

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,11 +1503,58 @@ class CdpDriver implements AppDriver {
15031503

15041504
/// Type text character by character (more realistic than enterText).
15051505
Future<void> typeText(String text) async {
1506+
// Snapshot focused element's value before typing
1507+
final beforeResult = await _evalJs('''
1508+
(() => {
1509+
const el = document.activeElement;
1510+
if (!el) return JSON.stringify({tag: null});
1511+
return JSON.stringify({tag: el.tagName, val: el.value || '', len: (el.value || '').length});
1512+
})()
1513+
''');
1514+
final beforeParsed = _parseJsonEval(beforeResult);
1515+
final beforeLen = (beforeParsed?['len'] as num?)?.toInt() ?? 0;
1516+
1517+
// Primary: keyDown(text) + keyUp per character
15061518
for (final char in text.split('')) {
1519+
final code = char.codeUnitAt(0);
1520+
final keyCode = code >= 97 && code <= 122 ? code - 32 : code;
1521+
final codeStr = code >= 65 && code <= 90 || code >= 97 && code <= 122
1522+
? 'Key${char.toUpperCase()}'
1523+
: '';
15071524
await _call('Input.dispatchKeyEvent', {
1508-
'type': 'char',
1525+
'type': 'keyDown',
15091526
'text': char,
1527+
'key': char,
1528+
'unmodifiedText': char,
1529+
if (codeStr.isNotEmpty) 'code': codeStr,
1530+
'windowsVirtualKeyCode': keyCode,
1531+
'nativeVirtualKeyCode': keyCode,
15101532
});
1533+
await _call('Input.dispatchKeyEvent', {
1534+
'type': 'keyUp',
1535+
'key': char,
1536+
if (codeStr.isNotEmpty) 'code': codeStr,
1537+
'windowsVirtualKeyCode': keyCode,
1538+
'nativeVirtualKeyCode': keyCode,
1539+
});
1540+
}
1541+
1542+
// Verify text was inserted; fallback to execCommand if not
1543+
final afterResult = await _evalJs('''
1544+
(() => {
1545+
const el = document.activeElement;
1546+
if (!el) return JSON.stringify({len: 0});
1547+
return JSON.stringify({len: (el.value || el.textContent || '').length});
1548+
})()
1549+
''');
1550+
final afterParsed = _parseJsonEval(afterResult);
1551+
final afterLen = (afterParsed?['len'] as num?)?.toInt() ?? 0;
1552+
1553+
if (afterLen <= beforeLen) {
1554+
// dispatchKeyEvent didn't insert text — use execCommand fallback
1555+
final escaped = text.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
1556+
await _evalJs(
1557+
"document.execCommand('insertText', false, '$escaped')");
15111558
}
15121559
}
15131560

@@ -2425,14 +2472,44 @@ function _dqAll(sel, root) {
24252472
$deepQ
24262473
let el = _dq('$selector');
24272474
if (el) return el;
2475+
// Visibility check helper
2476+
function _vis(e) {
2477+
const s = window.getComputedStyle(e);
2478+
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
2479+
const r = e.getBoundingClientRect();
2480+
return r.width > 0 && r.height > 0;
2481+
}
2482+
// Interactive tags get priority
2483+
const interactive = new Set(['A','BUTTON','INPUT','SELECT','TEXTAREA','LABEL']);
2484+
function _score(e) {
2485+
let s = 0;
2486+
if (_vis(e)) s += 1000;
2487+
if (interactive.has(e.tagName) || e.getAttribute('role') === 'button' || e.getAttribute('role') === 'link' || e.getAttribute('role') === 'tab') s += 500;
2488+
// Prefer smallest textContent (most specific match)
2489+
s -= Math.min((e.textContent || '').length, 999);
2490+
return s;
2491+
}
24282492
const all = _dqAll('a, button, input, select, textarea, label, span, p, h1, h2, h3, h4, h5, h6, div, li, td, th, [role]');
2493+
// Exact match — pick best scored
2494+
let best = null, bestScore = -Infinity;
24292495
for (const e of all) {
2430-
if (e.textContent && e.textContent.trim() === '$escaped') return e;
2496+
const t = (e.textContent || '').trim();
2497+
if (t === '$escaped') {
2498+
const sc = _score(e);
2499+
if (sc > bestScore) { best = e; bestScore = sc; }
2500+
}
24312501
}
2502+
if (best) return best;
2503+
// Contains match — pick best scored
2504+
best = null; bestScore = -Infinity;
24322505
for (const e of all) {
2433-
if (e.textContent && e.textContent.trim().includes('$escaped')) return e;
2506+
const t = (e.textContent || '').trim();
2507+
if (t.includes('$escaped')) {
2508+
const sc = _score(e);
2509+
if (sc > bestScore) { best = e; bestScore = sc; }
2510+
}
24342511
}
2435-
return null;
2512+
return best;
24362513
})()''';
24372514
}
24382515
if (ref != null) {

0 commit comments

Comments
 (0)