mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
test(embed): add IME/selection test harness + document how to run it
tools/ime-test/index.html is a single-page input+textarea harness with an on-page log timestamping focus/selection/input/composition events, paint latency, long-tasks, and main-thread blocks — the instrumentation used to pin the erase / caret-jump / first-letter-freeze bugs and what we'll use to profile the magnifier. README documents serving it (python http.server on 8765, 10.0.2.2 for emulator / adb reverse for USB) and opening it as an embedded tab via the in-app browser address bar. Plan gets a matching "How to test" section. Dev tool only — nothing under tools/ ships, so the [ImeDiag] strings stay out of src/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d351376e51
commit
8232cd50a3
@@ -133,6 +133,28 @@ Until pinned, treat this as a **WebView/Chromium off-window-surface limitation**
|
||||
the app layer cannot currently restore the highlight without recreating the
|
||||
surface (#1).
|
||||
|
||||
## How to test
|
||||
|
||||
The on-device harness lives at **`tools/ime-test/`** (`index.html` + `README.md`).
|
||||
It's a single page with an `<input>`, a `<textarea>`, and an on-page log that
|
||||
timestamps focus/selection/input/composition events, **paint latency**,
|
||||
long-tasks, and main-thread blocks — the instrumentation that pinned the erase,
|
||||
caret-jump, and first-letter-freeze bugs, and exactly what we'll want when
|
||||
profiling the magnifier.
|
||||
|
||||
Run it (full details in `tools/ime-test/README.md`):
|
||||
|
||||
1. `cd tools/ime-test && python3 -m http.server 8765`
|
||||
2. Reach it: emulator → `http://10.0.2.2:8765`; USB device →
|
||||
`adb reverse tcp:8765 tcp:8765` then `http://localhost:8765`.
|
||||
3. Open that URL in the **in-app browser** (`BrowserScreen` address bar) to load
|
||||
it as an *embedded* tab (the relay path). Opening the same URL full-screen and
|
||||
pressing `back` is how you reproduce the highlight bug below.
|
||||
|
||||
Console log lines are tagged `[ImeDiag]` and surface in `adb logcat` (the
|
||||
`:napplet` process owns the WebView console). This is a dev tool — nothing under
|
||||
`tools/` ships, which is why those diagnostic strings are kept out of `src/`.
|
||||
|
||||
## Key files
|
||||
|
||||
- `commons/src/commonMain/composeResources/files/napplet/shim.js` — DOM bridge.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# IME / text-selection test harness
|
||||
|
||||
A single-file web page (`index.html`) for exercising and profiling the embedded
|
||||
WebView IME + text-selection relay (see
|
||||
`amethyst/plans/2026-06-25-embed-text-selection-native-parity.md`). It has a
|
||||
plain `<input>` and a `<textarea>` plus an on-page green log that records, with
|
||||
millisecond timestamps:
|
||||
|
||||
- focus/blur, `selectionchange`, `keydown`/`beforeinput`/`input`, composition
|
||||
events, and the resulting `value`/selection — to catch erase, caret-jump, and
|
||||
focus-transfer regressions;
|
||||
- **paint latency** (`requestAnimationFrame` after each DOM change) — the metric
|
||||
that exposed the first-letter freeze;
|
||||
- **long-task** + **main-thread-block** detectors and a focus/selection
|
||||
**heartbeat** — to catch anything stalling the WebView main thread or
|
||||
spontaneously moving focus/selection.
|
||||
|
||||
The log lines are tagged `[ImeDiag]` and also go to `console.log`, so they show
|
||||
up in `adb logcat` (the `:napplet` process owns the WebView console). Nothing
|
||||
here ships in the app — it's a dev tool, which is why the `[ImeDiag]` strings
|
||||
live only under `tools/`.
|
||||
|
||||
## Run it
|
||||
|
||||
1. Serve this directory over HTTP from your dev machine:
|
||||
|
||||
```bash
|
||||
cd tools/ime-test && python3 -m http.server 8765
|
||||
```
|
||||
|
||||
2. Reach it from the device/emulator:
|
||||
- **Emulator:** the page is at `http://10.0.2.2:8765` (`10.0.2.2` is the
|
||||
emulator's alias for the host loopback).
|
||||
- **Physical device (USB):** `adb reverse tcp:8765 tcp:8765`, then the page is
|
||||
at `http://localhost:8765`.
|
||||
|
||||
3. Open that URL as an **embedded** tab (this is the path that uses the relay —
|
||||
*not* a full-screen activity):
|
||||
- Open the in-app browser (`BrowserScreen`) and type the URL into its address
|
||||
bar. The embedded browser handles `http`/`https`, so it loads into the
|
||||
`:napplet` SurfaceControlViewHost surface.
|
||||
|
||||
To compare against native behavior, open the same URL in a full-screen
|
||||
activity (where the WebView renders in-window with the native keyboard) — that
|
||||
is also how you reproduce the **full-screen round-trip highlight bug** (open
|
||||
full-screen, `back`, then selection highlight is dead across all embeds).
|
||||
|
||||
## Reading the log
|
||||
|
||||
- `INPUT … val=… sel=…` right after a keystroke with the right value = no erase.
|
||||
- `PAINT-LATENCY Nms` spiking to ~1000ms = the first-letter freeze (should stay
|
||||
low now that the surface no longer resizes on IME show).
|
||||
- `MAINTHREAD BLOCKED` / `LONGTASK` = something is stalling the WebView thread.
|
||||
- `HEARTBEAT` lines changing while idle = spontaneous focus/selection drift.
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>IME Test</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 16px; background:#f4f4fa; }
|
||||
h2 { margin: 24px 0 6px; }
|
||||
input, textarea { width: 100%; font-size: 18px; padding: 12px; box-sizing: border-box; border: 2px solid #88a; border-radius: 8px; }
|
||||
#log { white-space: pre-wrap; font: 11px monospace; background:#111; color:#0f0; padding:8px; height:200px; overflow:auto; margin-top:16px; border-radius:6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Plain text input</h2>
|
||||
<input id="inp" type="text" placeholder="type here" value="hello world">
|
||||
<h2>Textarea</h2>
|
||||
<textarea id="ta" rows="2" placeholder="type here">hello world</textarea>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
(function(){
|
||||
var t0 = performance.now();
|
||||
function ts(){ return (performance.now() - t0).toFixed(0); }
|
||||
var logEl = document.getElementById('log');
|
||||
function log(m){
|
||||
var line = '[ImeDiag] +' + ts() + 'ms ' + m;
|
||||
try { console.log(line); } catch(_){}
|
||||
if (logEl) { logEl.textContent += line + '\n'; logEl.scrollTop = logEl.scrollHeight; }
|
||||
}
|
||||
function selOf(n){ return (n && n.selectionStart != null) ? (n.selectionStart + '..' + n.selectionEnd) : '?'; }
|
||||
function active(){ var a = document.activeElement; return a ? (a.tagName + '#' + (a.id||'')) : 'none'; }
|
||||
|
||||
['inp','ta'].forEach(function(id){
|
||||
var n = document.getElementById(id);
|
||||
n.addEventListener('focus', function(){ log(id + ' FOCUS hasFocus=' + document.hasFocus()); });
|
||||
n.addEventListener('blur', function(){ log(id + ' BLUR'); });
|
||||
n.addEventListener('keydown', function(e){ log(id + ' keydown key=' + e.key + ' isComposing=' + e.isComposing); });
|
||||
n.addEventListener('beforeinput', function(e){ log(id + ' beforeinput type=' + e.inputType + ' data=' + JSON.stringify(e.data) + ' isComposing=' + e.isComposing); });
|
||||
n.addEventListener('input', function(e){
|
||||
log(id + ' INPUT type=' + e.inputType + ' data=' + JSON.stringify(e.data) + ' isComposing=' + e.isComposing + ' val=' + JSON.stringify(n.value) + ' sel=' + selOf(n));
|
||||
// Objective paint-latency: how long until the NEXT frame is produced after this DOM change.
|
||||
var t = performance.now();
|
||||
requestAnimationFrame(function(){ log(id + ' PAINT-LATENCY ' + (performance.now() - t).toFixed(0) + 'ms (DOM change -> next frame)'); });
|
||||
});
|
||||
n.addEventListener('compositionstart', function(e){ log(id + ' compositionSTART data=' + JSON.stringify(e.data)); });
|
||||
n.addEventListener('compositionupdate', function(e){ log(id + ' compositionUPDATE data=' + JSON.stringify(e.data)); });
|
||||
n.addEventListener('compositionend', function(e){ log(id + ' compositionEND data=' + JSON.stringify(e.data)); });
|
||||
n.addEventListener('select', function(){ log(id + ' SELECT event sel=' + selOf(n)); });
|
||||
});
|
||||
|
||||
document.addEventListener('selectionchange', function(){
|
||||
var a = document.activeElement;
|
||||
if (a && (a.id === 'inp' || a.id === 'ta')) log('selectionchange ' + a.id + ' sel=' + selOf(a) + ' hasFocus=' + document.hasFocus());
|
||||
}, true);
|
||||
|
||||
window.addEventListener('focus', function(){ log('WINDOW focus hasFocus=' + document.hasFocus()); }, true);
|
||||
window.addEventListener('blur', function(){ log('WINDOW blur hasFocus=' + document.hasFocus()); }, true);
|
||||
document.addEventListener('visibilitychange', function(){ log('visibilitychange hidden=' + document.hidden); });
|
||||
|
||||
// Heartbeat: detect any spontaneous oscillation of focus/selection while idle.
|
||||
var last = '';
|
||||
setInterval(function(){
|
||||
var a = document.activeElement;
|
||||
var snap = 'hasFocus=' + document.hasFocus() + ' vis=' + document.visibilityState + ' active=' + active() + ' sel=' + (a && a.selectionStart != null ? selOf(a) : '-');
|
||||
if (snap !== last) { last = snap; log('HEARTBEAT ' + snap); }
|
||||
}, 250);
|
||||
|
||||
// Long-task observer: catches any >50ms block of the WebView main thread and its attribution.
|
||||
try {
|
||||
new PerformanceObserver(function(list){
|
||||
list.getEntries().forEach(function(e){
|
||||
var attr = (e.attribution && e.attribution[0]) ? (e.attribution[0].name + '/' + e.attribution[0].containerType) : '';
|
||||
log('LONGTASK ' + e.duration.toFixed(0) + 'ms name=' + e.name + ' ' + attr);
|
||||
});
|
||||
}).observe({ entryTypes: ['longtask'] });
|
||||
} catch(_) { log('no longtask observer'); }
|
||||
|
||||
// Main-thread block detector via MessageChannel (fires a macrotask ASAP each tick; if it's late, main was blocked).
|
||||
var mc = new MessageChannel();
|
||||
var tickExpected = performance.now();
|
||||
mc.port1.onmessage = function(){
|
||||
var now = performance.now(), late = now - tickExpected;
|
||||
if (late > 150) log('MAINTHREAD BLOCKED ~' + late.toFixed(0) + 'ms');
|
||||
tickExpected = now + 8;
|
||||
setTimeout(function(){ mc.port2.postMessage(0); }, 8);
|
||||
};
|
||||
mc.port2.postMessage(0);
|
||||
|
||||
log('test page loaded vis=' + document.visibilityState);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user