Skip to content

Commit 8485af0

Browse files
committed
Fix mobile paste and IME dedupe
1 parent c7e37fb commit 8485af0

File tree

3 files changed

+416
-22
lines changed

3 files changed

+416
-22
lines changed

lib/input-handler.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ interface MockClipboardEvent {
2929
preventDefault: () => void;
3030
stopPropagation: () => void;
3131
}
32+
interface MockInputEvent {
33+
type: string;
34+
inputType: string;
35+
data: string | null;
36+
isComposing?: boolean;
37+
preventDefault: () => void;
38+
stopPropagation: () => void;
39+
}
3240

3341
interface MockHTMLElement {
3442
addEventListener: (event: string, handler: (e: any) => void) => void;
@@ -79,6 +87,18 @@ function createClipboardEvent(text: string | null): MockClipboardEvent {
7987
stopPropagation: mock(() => {}),
8088
};
8189
}
90+
91+
// Helper to create mock beforeinput event
92+
function createBeforeInputEvent(inputType: string, data: string | null): MockInputEvent {
93+
return {
94+
type: 'beforeinput',
95+
inputType,
96+
data,
97+
isComposing: false,
98+
preventDefault: mock(() => {}),
99+
stopPropagation: mock(() => {}),
100+
};
101+
}
82102
interface MockCompositionEvent {
83103
type: string;
84104
data: string | null;
@@ -399,6 +419,48 @@ describe('InputHandler', () => {
399419
expect(container.childNodes[0]).toBe(elementNode);
400420
expect(dataReceived).toEqual(['你好']);
401421
});
422+
423+
test('avoids duplicate commit when compositionend fires before beforeinput', () => {
424+
const inputElement = createMockContainer();
425+
const handler = new InputHandler(
426+
ghostty,
427+
container as any,
428+
(data) => dataReceived.push(data),
429+
() => {
430+
bellCalled = true;
431+
},
432+
undefined,
433+
undefined,
434+
undefined,
435+
inputElement as any
436+
);
437+
438+
container.dispatchEvent(createCompositionEvent('compositionend', '你好'));
439+
inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));
440+
441+
expect(dataReceived).toEqual(['你好']);
442+
});
443+
444+
test('avoids duplicate commit when beforeinput fires before compositionend', () => {
445+
const inputElement = createMockContainer();
446+
const handler = new InputHandler(
447+
ghostty,
448+
container as any,
449+
(data) => dataReceived.push(data),
450+
() => {
451+
bellCalled = true;
452+
},
453+
undefined,
454+
undefined,
455+
undefined,
456+
inputElement as any
457+
);
458+
459+
inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));
460+
container.dispatchEvent(createCompositionEvent('compositionend', '你好'));
461+
462+
expect(dataReceived).toEqual(['你好']);
463+
});
402464
});
403465

404466
describe('Control Characters', () => {
@@ -939,6 +1001,54 @@ describe('InputHandler', () => {
9391001
expect(dataReceived[0]).toBe(pasteText);
9401002
});
9411003

1004+
test('handles beforeinput insertFromPaste with data', () => {
1005+
const inputElement = createMockContainer();
1006+
const handler = new InputHandler(
1007+
ghostty,
1008+
container as any,
1009+
(data) => dataReceived.push(data),
1010+
() => {
1011+
bellCalled = true;
1012+
},
1013+
undefined,
1014+
undefined,
1015+
undefined,
1016+
inputElement as any
1017+
);
1018+
1019+
const pasteText = 'Hello, beforeinput!';
1020+
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);
1021+
1022+
inputElement.dispatchEvent(beforeInputEvent);
1023+
1024+
expect(dataReceived.length).toBe(1);
1025+
expect(dataReceived[0]).toBe(pasteText);
1026+
});
1027+
1028+
test('uses bracketed paste for beforeinput insertFromPaste', () => {
1029+
const inputElement = createMockContainer();
1030+
const handler = new InputHandler(
1031+
ghostty,
1032+
container as any,
1033+
(data) => dataReceived.push(data),
1034+
() => {
1035+
bellCalled = true;
1036+
},
1037+
undefined,
1038+
undefined,
1039+
(mode) => mode === 2004,
1040+
inputElement as any
1041+
);
1042+
1043+
const pasteText = 'Bracketed paste';
1044+
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);
1045+
1046+
inputElement.dispatchEvent(beforeInputEvent);
1047+
1048+
expect(dataReceived.length).toBe(1);
1049+
expect(dataReceived[0]).toBe(`\x1b[200~${pasteText}\x1b[201~`);
1050+
});
1051+
9421052
test('handles multi-line paste', () => {
9431053
const handler = new InputHandler(
9441054
ghostty,
@@ -958,6 +1068,58 @@ describe('InputHandler', () => {
9581068
expect(dataReceived[0]).toBe(pasteText);
9591069
});
9601070

1071+
test('ignores beforeinput insertFromPaste when paste already handled', () => {
1072+
const inputElement = createMockContainer();
1073+
const handler = new InputHandler(
1074+
ghostty,
1075+
container as any,
1076+
(data) => dataReceived.push(data),
1077+
() => {
1078+
bellCalled = true;
1079+
},
1080+
undefined,
1081+
undefined,
1082+
undefined,
1083+
inputElement as any
1084+
);
1085+
1086+
const pasteText = 'Hello, World!';
1087+
const pasteEvent = createClipboardEvent(pasteText);
1088+
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);
1089+
1090+
container.dispatchEvent(pasteEvent);
1091+
inputElement.dispatchEvent(beforeInputEvent);
1092+
1093+
expect(dataReceived.length).toBe(1);
1094+
expect(dataReceived[0]).toBe(pasteText);
1095+
});
1096+
1097+
test('ignores paste when beforeinput insertFromPaste already handled', () => {
1098+
const inputElement = createMockContainer();
1099+
const handler = new InputHandler(
1100+
ghostty,
1101+
container as any,
1102+
(data) => dataReceived.push(data),
1103+
() => {
1104+
bellCalled = true;
1105+
},
1106+
undefined,
1107+
undefined,
1108+
undefined,
1109+
inputElement as any
1110+
);
1111+
1112+
const pasteText = 'Hello, World!';
1113+
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);
1114+
const pasteEvent = createClipboardEvent(pasteText);
1115+
1116+
inputElement.dispatchEvent(beforeInputEvent);
1117+
container.dispatchEvent(pasteEvent);
1118+
1119+
expect(dataReceived.length).toBe(1);
1120+
expect(dataReceived[0]).toBe(pasteText);
1121+
});
1122+
9611123
test('ignores paste with no clipboard data', () => {
9621124
const handler = new InputHandler(
9631125
ghostty,

0 commit comments

Comments
 (0)