mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
fix(review): content-free IME traces, guarded onData self-heal, Android-only retap recovery (PR #143)
- BLOCKER (privacy): the CJK diagnostic trace logged typed CONTENT — _esc(e.key) per keystroke, up to 24 chars of textarea value on focus/blur/compstart/ compend/input, and the flushed text — mirrored into _crashDiag, which persists to localStorage and beacons to POST /api/crash-diag. Traces are now content-free: key CLASS via _kdesc (any single code point → 'printable', named keys pass through), value lengths + phantom presence via _vdesc (len=N[+ph]), and 'flush send len=N'. _esc removed. - MAJOR: the onData self-heal refocused the CJK field whenever gated data arrived with focus elsewhere — but onData also fires for xterm's SELF-GENERATED query replies (DA/DSR/CPR/OSC during Ink redraws), so it stole focus from rename/search/settings inputs while output streamed. Now requires document.activeElement === this.terminal.textarea (genuine typed input) and bails when shouldSuppressTerminalQueryResponse(data) matches. - MAJOR: the pointerdown blur→setTimeout(focus,0) wedged-IME recovery ran on ALL platforms; on iOS tapping the focused empty field is normal and the async refocus is outside the user-gesture stack. The listener is now only registered when /Android/i.test(navigator.userAgent). - tests: trace-privacy test (no typed character or textarea value ever appears in the trace; lengths/key classes still recorded), iOS harness asserts the pointerdown recovery never cycles, self-heal source guard asserts both new conditions; vm harness gained a ua option (navigator injected, Android UA default so the existing wedged-IME test still exercises the recovery). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+60
-1
@@ -60,7 +60,10 @@ function makeTextarea(): FakeTextarea {
|
||||
};
|
||||
}
|
||||
|
||||
function loadCjkHarness() {
|
||||
const ANDROID_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0';
|
||||
const IOS_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)';
|
||||
|
||||
function loadCjkHarness({ ua = ANDROID_UA }: { ua?: string } = {}) {
|
||||
const textarea = makeTextarea();
|
||||
const sent: string[] = [];
|
||||
const windowObj: Record<string, unknown> = {};
|
||||
@@ -74,6 +77,7 @@ function loadCjkHarness() {
|
||||
const context = vm.createContext({
|
||||
window: windowObj,
|
||||
document: documentObj,
|
||||
navigator: { userAgent: ua },
|
||||
setTimeout: (fn: () => void, ms?: number) => setTimeout(fn, ms),
|
||||
clearTimeout: (t: ReturnType<typeof setTimeout>) => clearTimeout(t),
|
||||
performance: { now: () => performance.now() },
|
||||
@@ -203,6 +207,52 @@ describe('CJK input module', () => {
|
||||
expect(blurred).toBe(1);
|
||||
});
|
||||
|
||||
it('does not run the wedged-IME pointerdown recovery on iOS (Android-only)', () => {
|
||||
// On iOS, tapping the focused empty field is normal (paste callout,
|
||||
// habitual tap), and the async refocus runs outside the user-gesture
|
||||
// stack — the blur→focus cycle must never fire there.
|
||||
const { textarea, documentObj } = loadCjkHarness({ ua: IOS_UA });
|
||||
documentObj.activeElement = textarea;
|
||||
let blurred = 0;
|
||||
let focused = 0;
|
||||
textarea.blur = () => {
|
||||
blurred++;
|
||||
};
|
||||
textarea.focus = () => {
|
||||
focused++;
|
||||
};
|
||||
|
||||
textarea.fire('pointerdown');
|
||||
vi.advanceTimersByTime(10);
|
||||
expect(blurred).toBe(0);
|
||||
expect(focused).toBe(0);
|
||||
});
|
||||
|
||||
it('never records typed content in the diagnostic trace (privacy)', () => {
|
||||
// The trace mirrors into crash-diag, which persists to localStorage and
|
||||
// beacons to the server — it must stay content-free: lengths, key
|
||||
// classes, and event names only. Drive every path that formerly embedded
|
||||
// the value or key literal (composition, keydown send, input, blur).
|
||||
const { CjkInput, textarea, sent } = loadCjkHarness();
|
||||
|
||||
textarea.fire('compositionstart');
|
||||
textarea.value = PHANTOM + '秘密口令';
|
||||
textarea.fire('input', { isComposing: true, inputType: 'insertCompositionText' });
|
||||
textarea.fire('compositionend');
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
textarea.fire('keydown', { key: '囍', ctrlKey: false, altKey: false, metaKey: false });
|
||||
textarea.value = PHANTOM + '秘密';
|
||||
textarea.fire('blur');
|
||||
expect(sent).toEqual(['秘密口令', '囍']);
|
||||
|
||||
const trace = CjkInput.getTrace().join('\n');
|
||||
for (const ch of '秘密口令囍') expect(trace).not.toContain(ch);
|
||||
// Lengths and key classes are still traced for diagnostics.
|
||||
expect(trace).toContain('flush send len=4');
|
||||
expect(trace).toContain('keydown printable');
|
||||
});
|
||||
|
||||
it('skips redundant textarea writes when already reset (Android IME desync guard)', () => {
|
||||
const { CjkInput, textarea } = loadCjkHarness();
|
||||
const before = textarea.valueWrites;
|
||||
@@ -227,6 +277,15 @@ describe('CJK input module', () => {
|
||||
expect(src).toMatch(/this\.terminal\.focus = \(\) => \{/);
|
||||
expect(src).toContain("cjkEl?.classList.contains('cjk-input-visible')");
|
||||
expect(src).toContain('CJK regain-focus (onData swallowed input)');
|
||||
|
||||
// The self-heal must only fire for GENUINE typed input: onData also fires
|
||||
// for xterm's self-generated query replies (DA/DSR/CPR/OSC during Ink
|
||||
// redraws), which arrive while e.g. the rename input or search box has
|
||||
// focus — refocusing on those steals focus mid-typing. Guard: focus must
|
||||
// be on xterm's own textarea AND the data must not be a query reply.
|
||||
const selfHeal = src.slice(src.indexOf('Self-heal'), src.indexOf('CJK regain-focus'));
|
||||
expect(selfHeal).toContain('document.activeElement === this.terminal.textarea');
|
||||
expect(selfHeal).toContain('shouldSuppressTerminalQueryResponse(data)');
|
||||
});
|
||||
|
||||
it('sends text plus carriage return on Enter', () => {
|
||||
|
||||
Reference in New Issue
Block a user