Files
Codeman/test/accessory-keeps-terminal-focus.test.ts
T
JD d56b90b7c6 fix(mobile): keep the keyboard up when an accessory key is pressed
An accessory key that acts on the terminal (arrows, Tab, Esc, Ctrl, scroll, clear input) took focus on press, so every tap blurred the terminal and the click handler had to focus it again: a blur/focus pair per tap that started a keyboard dismissal and shifted the toolbar each time. Such a key now cancels the mousedown default instead, the same pattern as the Attach input, so the terminal never loses focus on the press; the click-time refocus stays as the fallback. A first tap on clear/compact only arms the confirm and is treated the same way.
2026-10-21 21:40:23 -04:00

106 lines
3.3 KiB
TypeScript

/**
* @fileoverview Accessory keys that keep the keyboard up must not take focus on
* press. A tap used to blur the terminal's textarea and the click handler then
* refocused it, so every arrow/Tab/Esc tap started a keyboard dismissal. The bar
* now cancels the mousedown default for exactly those keys (and the first,
* arming tap of clear/compact); every other key keeps the old behavior.
*
* Loaded with `vm` against a minimal fake DOM, like the other accessory-bar tests.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { describe, expect, it, vi } from 'vitest';
type Listener = (e: unknown) => void;
type Bar = {
init(): void;
_confirmAction: string | null;
syncReadMyMind(): void;
syncCodexKeys(): void;
_syncComposerDraftIndicator(): void;
};
function loadBar() {
const listeners: Record<string, Listener> = {};
const element = {
className: '',
innerHTML: '',
addEventListener: (type: string, fn: Listener) => {
listeners[type] = fn;
},
};
const context = vm.createContext({
console,
document: { createElement: () => element, querySelector: () => null },
MobileDetection: { isTouchDevice: () => true },
localStorage: { getItem: () => null, setItem: () => {} },
});
vm.runInContext(
`${readFileSync(resolve('src/web/public/keyboard-accessory.js'), 'utf8')}\nglobalThis.__bar = KeyboardAccessoryBar;`,
context
);
const bar = (context as unknown as { __bar: Bar }).__bar;
bar.syncReadMyMind = () => {};
bar.syncCodexKeys = () => {};
bar._syncComposerDraftIndicator = () => {};
bar.init();
// `null` is a press on the bar itself (padding, a gap): closest() finds no key.
const press = (action: string | null) => {
const preventDefault = vi.fn();
listeners.mousedown({
target: { closest: () => (action === null ? null : { dataset: { action } }) },
preventDefault,
});
return preventDefault.mock.calls.length > 0;
};
return { bar, press };
}
describe('accessory keys keep the terminal focused on press', () => {
it('cancels the press default for keys that leave the keyboard up', () => {
const { press } = loadBar();
for (const action of [
'scroll-up',
'scroll-down',
'arrow-left',
'arrow-right',
'tab',
'shift-tab',
'shift-left',
'shift-right',
'ctrl',
'ctrl-o',
'opt-enter',
'esc',
'effort-max',
'clear-input',
]) {
expect(press(action), action).toBe(true);
}
});
it('leaves keys that move focus or dismiss the keyboard alone', () => {
const { press } = loadBar();
for (const action of ['compose', 'readmymind', 'pick-path', 'paste', 'dismiss', 'init']) {
expect(press(action), action).toBe(false);
}
});
it('cancels only the arming tap of clear/compact, not the confirming one', () => {
const { bar, press } = loadBar();
bar._confirmAction = null;
expect(press('clear')).toBe(true);
bar._confirmAction = 'clear';
expect(press('clear')).toBe(false);
expect(press('compact')).toBe(true);
bar._confirmAction = 'compact';
expect(press('compact')).toBe(false);
});
it('ignores a press outside a key', () => {
const { press } = loadBar();
expect(press(null)).toBe(false);
});
});