mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(tiles): wire the Android soft-keyboard controller into every tile (#541 parity)
#541 fixed Android autocorrect duplicating the typed line in the primary pane: xterm's keyCode-229 textarea diff is append-only, so an autocorrect on space (delete a word, insert the corrected one) sent the whole line again. The fix, an edit-based diff that sends one DEL per deleted code point and then the inserted text, lives in terminal-keycode229-recovery.js together with #441's next-keydown drain (a character committed in the same task as Enter goes out ahead of the \r) and the original orphaned-insertText recovery. Only the primary pane created that controller, so a grid tile or the split's Pane B still ran xterm's stock behaviour. Both are gated on width alone (1180 CSS px), which a wide Android tablet clears. TerminalTile now creates its own controller in connect(), after the xterm opens and before the first await, handed this tile's textarea, this tile's CompositionHelper and _onTerminalData as the send path, so recovered bytes go to the tile's own session through the exactly-once queue. As in the primary pane, handleKeyEvent runs first in the custom key handler, above the keyCode-229 early return, and notifyCanonicalData sits in the onData lambda, gated on the same two CodemanTerminalInput predicates, never in _onTerminalData, which the recovered bytes also take. destroy() tears the controller down before disposing the xterm, which restores xterm's own diff and removes the capture listeners. No mode or device gate, matching the primary. The module itself is unchanged apart from its header; terminal-ui.js gains only a comment naming the twin. Tests: test/terminal-tile-input.test.ts now loads the real module into its vm harness (with window timers, without which create() would silently throw and every test would run against no controller) and drives a fake CompositionHelper carrying xterm's own append-only diff. It covers install and restore on the tile's own helper and textarea, autocorrect sent as an edit (with a control reproducing the device-log duplicate), the last character and an autocorrect each followed by Enter in one task, a self-rescued 229 key delivered once, the onData gate ignoring query replies and focus reports, two refused inserts after one keydown both recovered, robustness when the controller throws, per-tile controllers, and a source pin keeping the call above the early return. Removing the create, the handleKeyEvent call, the notify, its gate, or the destroy each turns at least one of them red, as does moving the notify into _onTerminalData. The browser suite gains a TerminalTile block in test/terminal-keycode229-recovery.browser.test.ts (real xterm, trusted execCommand input, chunks asserted to address the tile's session, with a destroyed-controller control). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,16 @@
|
||||
* the pane once (`onExit`); a late close from a REPLACED socket is ignored; and
|
||||
* destroy() cancels a pending reconnect.
|
||||
*
|
||||
* Real code under test: constants.js + app.js (the queue) + terminal-ui.js (the
|
||||
* shared input predicates) + terminal-tile.js, in one `vm` context. xterm, the
|
||||
* fit addon and WebSocket are fakes (test/mocks/terminal-tile-fakes.ts);
|
||||
* `connect()` runs for real.
|
||||
* The last blocks pin the soft-keyboard controller every tile wires
|
||||
* (terminal-keycode229-recovery.js, the primary pane's #441/#541 fixes): an
|
||||
* Android autocorrect is sent as an edit, not a duplicated line, a character
|
||||
* committed in the same task as Enter goes out ahead of the \r, and the
|
||||
* controller is bound to THIS tile's textarea, composition helper and session.
|
||||
*
|
||||
* Real code under test: constants.js + terminal-keycode229-recovery.js +
|
||||
* app.js (the queue) + terminal-ui.js (the shared input predicates) +
|
||||
* terminal-tile.js, in one `vm` context. xterm, the fit addon and WebSocket are
|
||||
* fakes (test/mocks/terminal-tile-fakes.ts); `connect()` runs for real.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
@@ -36,6 +42,12 @@ function loadContext() {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
CodemanBase: { base: '' },
|
||||
// The keyCode-229 controller defaults its timers to window's. Late-bound,
|
||||
// like the context's own, so vi.useFakeTimers() reaches it; without them
|
||||
// its create() throws into the tile's catch and every controller test
|
||||
// would run against no controller at all.
|
||||
setTimeout: (fn: () => void, ms?: number) => globalThis.setTimeout(fn, ms),
|
||||
clearTimeout: (id: ReturnType<typeof setTimeout>) => globalThis.clearTimeout(id),
|
||||
};
|
||||
const context = vm.createContext({
|
||||
console: { ...console, log: vi.fn(), debug: vi.fn() },
|
||||
@@ -63,7 +75,8 @@ function loadContext() {
|
||||
},
|
||||
});
|
||||
vm.runInContext(
|
||||
`${read('constants.js')}\n${read('app.js')}\n${read('terminal-ui.js')}\n${read('terminal-tile.js')}\n` +
|
||||
`${read('constants.js')}\n${read('terminal-keycode229-recovery.js')}\n${read('app.js')}\n` +
|
||||
`${read('terminal-ui.js')}\n${read('terminal-tile.js')}\n` +
|
||||
'globalThis.__CodemanApp = CodemanApp;',
|
||||
context
|
||||
);
|
||||
@@ -138,6 +151,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
FakeTerminal.coreFactory = null;
|
||||
FakeFit.proposed = { cols: 80, rows: 24 };
|
||||
FakeSocket.instances = [];
|
||||
fetchMock.mockReset();
|
||||
@@ -684,3 +698,398 @@ describe('the server coming back kicks Pane B', () => {
|
||||
expect(branch).toContain('this._splitPane?.reconnectNow?.();');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* xterm's CompositionHelper, reduced to what the keyCode-229 controller touches.
|
||||
* Its `_handleAnyTextareaChanges` is xterm's own append-only diff as shipped
|
||||
* (node_modules/@xterm/xterm/src/browser/input/CompositionHelper.ts), so a
|
||||
* control without the controller reproduces the device-log duplicate, and its
|
||||
* `triggerDataEvent` feeds the tile's onData, as xterm's core service does.
|
||||
*/
|
||||
type Helper = {
|
||||
_isComposing: boolean;
|
||||
_isSendingComposition: boolean;
|
||||
_dataAlreadySent: string;
|
||||
_coreService: { triggerDataEvent: (data: string, wasUserInput?: boolean) => void };
|
||||
_handleAnyTextareaChanges: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives every FakeTerminal created from now on a composition helper. Returns
|
||||
* them in creation order, with xterm's own diff each one started with.
|
||||
*/
|
||||
function withCompositionHelpers() {
|
||||
const helpers: Helper[] = [];
|
||||
const originals: Array<Helper['_handleAnyTextareaChanges']> = [];
|
||||
FakeTerminal.coreFactory = (term) => {
|
||||
const helper: Helper = {
|
||||
_isComposing: false,
|
||||
_isSendingComposition: false,
|
||||
_dataAlreadySent: '',
|
||||
_coreService: { triggerDataEvent: (data: string) => term.type(data) },
|
||||
_handleAnyTextareaChanges(this: Helper) {
|
||||
const oldValue = term.textarea.value;
|
||||
setTimeout(() => {
|
||||
if (this._isComposing) return;
|
||||
const newValue = term.textarea.value;
|
||||
const diff = newValue.replace(oldValue, '');
|
||||
this._dataAlreadySent = diff;
|
||||
if (newValue.length > oldValue.length) this._coreService.triggerDataEvent(diff, true);
|
||||
else if (newValue.length < oldValue.length) this._coreService.triggerDataEvent('\x7f', true);
|
||||
else if (newValue !== oldValue) this._coreService.triggerDataEvent(newValue, true);
|
||||
}, 0);
|
||||
},
|
||||
};
|
||||
helpers.push(helper);
|
||||
originals.push(helper._handleAnyTextareaChanges);
|
||||
return { _compositionHelper: helper };
|
||||
};
|
||||
return Object.assign(helpers, { originals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives a tile the way an Android soft keyboard drives xterm. The fake xterm
|
||||
* runs no CompositionHelper.keydown of its own, so `key229()` does what xterm
|
||||
* does, in xterm's order: the custom key handler first, then (keyCode 229, no
|
||||
* composition) the helper's `_handleAnyTextareaChanges()`, read off the helper
|
||||
* at call time so the controller's patch is what runs.
|
||||
*/
|
||||
function softKeyboard(term: FakeTerminal, helper: Helper) {
|
||||
const textarea = term.textarea;
|
||||
const key229 = () => {
|
||||
term.keyHandler!({ type: 'keydown', key: 'Unidentified', keyCode: 229 });
|
||||
helper._handleAnyTextareaChanges();
|
||||
};
|
||||
return {
|
||||
key229,
|
||||
/** One appended character, settled on its own timer before the next key. */
|
||||
typeKeys(text: string) {
|
||||
for (const ch of text) {
|
||||
key229();
|
||||
textarea.value += ch;
|
||||
vi.advanceTimersByTime(1);
|
||||
}
|
||||
},
|
||||
/** The textarea now reads `value` (what the keyboard's input event left there). */
|
||||
edit(value: string) {
|
||||
textarea.value = value;
|
||||
},
|
||||
/** Enter: the custom handler, then xterm's own \r, then xterm clearing its textarea. */
|
||||
enter() {
|
||||
const passed = term.keyHandler!({ type: 'keydown', key: 'Enter', keyCode: 13 });
|
||||
if (passed) term.type('\r');
|
||||
textarea.value = '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Every byte the tile sent as input, in order. */
|
||||
const joinFrames = (frames: Array<{ d?: string }>) => frames.map((f) => f.d ?? '').join('');
|
||||
const wireOf = (ws: FakeSocket) => joinFrames(ws.inputFrames());
|
||||
|
||||
/** The line a shell ends up with: every DEL erases the character before it. */
|
||||
function lineOf(frames: Array<{ d?: string }>) {
|
||||
const out: string[] = [];
|
||||
for (const ch of joinFrames(frames)) {
|
||||
if (ch === '\x7f') out.pop();
|
||||
else out.push(ch);
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
type ControllerTile = Tile & { _keyCode229Recovery: unknown };
|
||||
|
||||
describe("TerminalTile wires the primary pane's soft-keyboard controller (#441, #541)", () => {
|
||||
it("installs on THIS tile's composition helper and textarea, and destroy() restores xterm's own", async () => {
|
||||
const helpers = withCompositionHelpers();
|
||||
const { tile, term } = await connectTile(makeApp());
|
||||
const helper = helpers[0];
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).not.toBeNull();
|
||||
|
||||
// The controller patched this tile's helper (xterm's diff is no longer the one that runs) and
|
||||
// listens on this tile's textarea in the CAPTURE phase (see the module's measured table).
|
||||
expect(helper._handleAnyTextareaChanges).not.toBe(helpers.originals[0]);
|
||||
const captured = term.textareaListeners.filter((l) => l.capture === true).map((l) => l.type);
|
||||
expect(captured.sort()).toEqual(['compositionend', 'compositionstart', 'input']);
|
||||
|
||||
tile.destroy();
|
||||
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).toBeNull();
|
||||
expect(helper._handleAnyTextareaChanges).toBe(helpers.originals[0]);
|
||||
expect(term.textareaListeners).toEqual([]);
|
||||
});
|
||||
|
||||
it('an autocorrect on space is sent as an edit, not a duplicated line', async () => {
|
||||
vi.useFakeTimers();
|
||||
const helpers = withCompositionHelpers();
|
||||
const app = makeApp();
|
||||
app.activeSessionId = 'some-other-session';
|
||||
const { tile, ws, term } = await connectTile(app);
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).not.toBeNull();
|
||||
ws.open();
|
||||
const kb = softKeyboard(term, helpers[0]);
|
||||
|
||||
kb.typeKeys('testing the peompt');
|
||||
// The device log's shape: ONE keydown deleting five characters, a second inserting `rompt `,
|
||||
// both before any timer runs.
|
||||
kb.key229();
|
||||
kb.edit('testing the p');
|
||||
kb.key229();
|
||||
kb.edit('testing the prompt ');
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
const frames = ws.inputFrames();
|
||||
expect(lineOf(frames)).toBe('testing the prompt ');
|
||||
expect(frames.filter((f) => f.d === '\x7f')).toHaveLength(5);
|
||||
// Every byte went to THIS tile's session through the exactly-once queue, never the active one.
|
||||
expect(frames.every((f) => Number.isInteger(f.seq))).toBe(true);
|
||||
expect(app._pendingDeliveries.get('s-tile')?.map((r) => r.data)).toEqual(frames.map((f) => f.d));
|
||||
expect(app._pendingDeliveries.has('some-other-session')).toBe(false);
|
||||
});
|
||||
|
||||
it('control: without the controller, xterm alone duplicates the line exactly as the device did', async () => {
|
||||
vi.useFakeTimers();
|
||||
const helpers = withCompositionHelpers();
|
||||
const saved = windowStub.CodemanKeyCode229Recovery;
|
||||
delete windowStub.CodemanKeyCode229Recovery;
|
||||
try {
|
||||
const { tile, ws, term } = await connectTile(makeApp());
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).toBeNull();
|
||||
ws.open();
|
||||
const kb = softKeyboard(term, helpers[0]);
|
||||
|
||||
kb.typeKeys('testing the peompt');
|
||||
kb.key229();
|
||||
kb.edit('testing the p');
|
||||
kb.key229();
|
||||
kb.edit('testing the prompt ');
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
expect(lineOf(ws.inputFrames())).toBe('testing the peompttesting the prompt rompt ');
|
||||
} finally {
|
||||
windowStub.CodemanKeyCode229Recovery = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it('a 229 last character in the same task as Enter goes out ahead of the \\r (#441 + #541)', async () => {
|
||||
vi.useFakeTimers();
|
||||
const helpers = withCompositionHelpers();
|
||||
const { ws, term } = await connectTile(makeApp());
|
||||
ws.open();
|
||||
const kb = softKeyboard(term, helpers[0]);
|
||||
|
||||
kb.typeKeys('hell');
|
||||
// One task: the last character's keydown and edit, then Enter, no timer in between.
|
||||
kb.key229();
|
||||
kb.edit('hello');
|
||||
kb.enter();
|
||||
// Settled synchronously at the Enter keydown, not by its timer.
|
||||
expect(wireOf(ws)).toBe('hello\r');
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
const wire = wireOf(ws);
|
||||
expect(wire).toBe('hello\r');
|
||||
expect(wire).not.toContain('\x7f');
|
||||
});
|
||||
|
||||
it('an autocorrect plus Enter in one task submits the corrected line', async () => {
|
||||
vi.useFakeTimers();
|
||||
const helpers = withCompositionHelpers();
|
||||
const { ws, term } = await connectTile(makeApp());
|
||||
ws.open();
|
||||
const kb = softKeyboard(term, helpers[0]);
|
||||
|
||||
kb.typeKeys('testing the peompt');
|
||||
kb.key229();
|
||||
kb.edit('testing the p');
|
||||
kb.key229();
|
||||
kb.edit('testing the prompt ');
|
||||
kb.enter();
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
const frames = ws.inputFrames();
|
||||
expect(lineOf(frames)).toBe('testing the prompt \r');
|
||||
expect(frames.filter((f) => f.d === '\x7f')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('a 229 keystroke xterm diffed itself is delivered once, not again by the recovery', async () => {
|
||||
vi.useFakeTimers();
|
||||
const helpers = withCompositionHelpers();
|
||||
const { tile, ws, term } = await connectTile(makeApp());
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).not.toBeNull();
|
||||
ws.open();
|
||||
const kb = softKeyboard(term, helpers[0]);
|
||||
|
||||
kb.key229();
|
||||
kb.edit('y');
|
||||
term.textarea.fire('input', { inputType: 'insertText', data: 'y', isComposing: false });
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
expect(ws.inputFrames().map((f) => f.d)).toEqual(['y']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the tile's onData tells the controller only about what a human typed", () => {
|
||||
/** A keystroke xterm refused: a keydown, then the committed `insertText` it did not forward. */
|
||||
const orphan = (term: FakeTerminal, data: string) => {
|
||||
term.keyHandler!({ type: 'keydown', key: 'Unidentified', keyCode: 65 });
|
||||
term.textarea.fire('input', { inputType: 'insertText', data, isComposing: false });
|
||||
};
|
||||
|
||||
it('recovers a refused insertText through a query reply and a focus report, to this tile', async () => {
|
||||
vi.useFakeTimers();
|
||||
const app = makeApp();
|
||||
const { tile, ws, term } = await connectTile(app);
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).not.toBeNull();
|
||||
ws.open();
|
||||
|
||||
orphan(term, 'x');
|
||||
term.type('\x1b[?1;2c'); // a DA reply xterm answers on its own: dropped, and not "xterm spoke"
|
||||
term.type('\x1b[I'); // a focus report: sent ephemeral, and not "xterm spoke" either
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
const frames = ws.inputFrames();
|
||||
expect(frames.map((f) => f.d)).toEqual(['\x1b[I', 'x']);
|
||||
const recovered = frames.find((f) => f.d === 'x')!;
|
||||
expect(Number.isInteger(recovered.seq)).toBe(true);
|
||||
expect(app._pendingDeliveries.get('s-tile')?.map((r) => r.data)).toEqual(['x']);
|
||||
});
|
||||
|
||||
it("never counts the controller's own recovered bytes as xterm's: two refused inserts after one keydown both arrive", async () => {
|
||||
// Pins WHERE the notify lives: in the onData lambda, not in _onTerminalData(), which the
|
||||
// recovered bytes also go through. Counted there, the first recovery would read as "xterm
|
||||
// spoke" for the second candidate, which shares its keydown snapshot, and drop it.
|
||||
vi.useFakeTimers();
|
||||
const { ws, term } = await connectTile(makeApp());
|
||||
ws.open();
|
||||
|
||||
term.keyHandler!({ type: 'keydown', key: 'Unidentified', keyCode: 65 });
|
||||
term.textarea.fire('input', { inputType: 'insertText', data: 'a', isComposing: false });
|
||||
term.textarea.fire('input', { inputType: 'insertText', data: 'b', isComposing: false });
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
expect(ws.inputFrames().map((f) => f.d)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('stands down when xterm really did deliver the keystroke', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { ws, term } = await connectTile(makeApp());
|
||||
ws.open();
|
||||
|
||||
orphan(term, 'x');
|
||||
term.type('x'); // xterm's own canonical emission for this keystroke
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
expect(ws.inputFrames().map((f) => f.d)).toEqual(['x']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the controller can never break a tile', () => {
|
||||
type FakeController = {
|
||||
handleKeyEvent: ReturnType<typeof vi.fn>;
|
||||
notifyCanonicalData: ReturnType<typeof vi.fn>;
|
||||
destroy: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let saved: unknown;
|
||||
beforeEach(() => {
|
||||
saved = windowStub.CodemanKeyCode229Recovery;
|
||||
});
|
||||
afterEach(() => {
|
||||
windowStub.CodemanKeyCode229Recovery = saved;
|
||||
});
|
||||
|
||||
const fakeController = (overrides: Partial<FakeController> = {}): FakeController => ({
|
||||
handleKeyEvent: vi.fn(),
|
||||
notifyCanonicalData: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('a create() that throws leaves the tile connected and typing', async () => {
|
||||
windowStub.CodemanKeyCode229Recovery = {
|
||||
create: () => {
|
||||
throw new Error('broken');
|
||||
},
|
||||
};
|
||||
const { tile, ws, term } = await connectTile(makeApp());
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).toBeNull();
|
||||
ws.open();
|
||||
|
||||
term.type('a');
|
||||
|
||||
expect(ws.inputFrames().map((f) => f.d)).toEqual(['a']);
|
||||
});
|
||||
|
||||
it("a handleKeyEvent that throws leaves every one of the tile's key gates working", async () => {
|
||||
const controller = fakeController({
|
||||
handleKeyEvent: vi.fn(() => {
|
||||
throw new Error('broken');
|
||||
}),
|
||||
});
|
||||
windowStub.CodemanKeyCode229Recovery = { create: () => controller };
|
||||
const { term } = await connectTile(makeApp());
|
||||
|
||||
expect(term.keyHandler!({ type: 'keydown', key: '1', code: 'Digit1', altKey: true })).toBe(false);
|
||||
expect(term.keyHandler!({ type: 'keydown', key: 'z', code: 'KeyZ', ctrlKey: true })).toBe(false);
|
||||
expect(term.keyHandler!({ type: 'keydown', key: 'Unidentified', keyCode: 229 })).toBe(true);
|
||||
expect(term.keyHandler!({ type: 'keydown', key: 'a', code: 'KeyA', keyCode: 65 })).toBe(true);
|
||||
// It still ran first, for every one of them.
|
||||
expect(controller.handleKeyEvent).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('a destroy() that throws still lets the tile dispose its xterm', async () => {
|
||||
const controller = fakeController({
|
||||
destroy: vi.fn(() => {
|
||||
throw new Error('broken');
|
||||
}),
|
||||
});
|
||||
windowStub.CodemanKeyCode229Recovery = { create: () => controller };
|
||||
const { tile, term } = await connectTile(makeApp());
|
||||
const dispose = vi.spyOn(term, 'dispose');
|
||||
|
||||
tile.destroy();
|
||||
|
||||
expect(controller.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(dispose).toHaveBeenCalledTimes(1);
|
||||
expect((tile as ControllerTile)._keyCode229Recovery).toBeNull();
|
||||
});
|
||||
|
||||
it('each tile gets its own controller on its own textarea, and destroys only its own', async () => {
|
||||
const made: Array<{ options: { textarea: unknown }; controller: FakeController }> = [];
|
||||
windowStub.CodemanKeyCode229Recovery = {
|
||||
create: (options: { textarea: unknown }) => {
|
||||
const controller = fakeController();
|
||||
made.push({ options, controller });
|
||||
return controller;
|
||||
},
|
||||
};
|
||||
const app = makeApp();
|
||||
const a = await connectTile(app);
|
||||
const b = await connectTile(app);
|
||||
|
||||
expect(made).toHaveLength(2);
|
||||
expect(made[0].options.textarea).toBe(a.term.textarea);
|
||||
expect(made[1].options.textarea).toBe(b.term.textarea);
|
||||
|
||||
a.tile.destroy();
|
||||
|
||||
expect(made[0].controller.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(made[1].controller.destroy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('terminal-tile.js keeps the controller call where it works (source pin)', () => {
|
||||
const source = readFileSync(resolve(import.meta.dirname, '../src/web/public/terminal-tile.js'), 'utf8');
|
||||
|
||||
it('calls handleKeyEvent ABOVE the IME early return, so a 229 keydown reaches it', () => {
|
||||
const call = source.indexOf('this._keyCode229Recovery?.handleKeyEvent?.(ev)');
|
||||
const earlyReturn = source.indexOf("ev.key === 'Process' || ev.keyCode === 229) return true");
|
||||
expect(call).toBeGreaterThan(-1);
|
||||
expect(earlyReturn).toBeGreaterThan(-1);
|
||||
expect(call).toBeLessThan(earlyReturn);
|
||||
});
|
||||
|
||||
it("hands the controller this tile's own composition helper", () => {
|
||||
expect(source).toMatch(/getCompositionHelper:\s*\(\)\s*=>\s*this\.terminal\?\._core\?\._compositionHelper/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user