Skip to content

Commit 242e38f

Browse files
tiannblink-so[bot]
andauthored
Android support (#110)
* Fix mobile paste and IME dedupe * chore: fix formatting * chore: correctly pass parameters in test --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
1 parent e3ec592 commit 242e38f

File tree

3 files changed

+421
-22
lines changed

3 files changed

+421
-22
lines changed

lib/input-handler.test.ts

Lines changed: 168 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,50 @@ 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+
undefined,
436+
inputElement as any
437+
);
438+
439+
container.dispatchEvent(createCompositionEvent('compositionend', '你好'));
440+
inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));
441+
442+
expect(dataReceived).toEqual(['你好']);
443+
});
444+
445+
test('avoids duplicate commit when beforeinput fires before compositionend', () => {
446+
const inputElement = createMockContainer();
447+
const handler = new InputHandler(
448+
ghostty,
449+
container as any,
450+
(data) => dataReceived.push(data),
451+
() => {
452+
bellCalled = true;
453+
},
454+
undefined,
455+
undefined,
456+
undefined,
457+
undefined,
458+
inputElement as any
459+
);
460+
461+
inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));
462+
container.dispatchEvent(createCompositionEvent('compositionend', '你好'));
463+
464+
expect(dataReceived).toEqual(['你好']);
465+
});
402466
});
403467

404468
describe('Control Characters', () => {
@@ -939,6 +1003,56 @@ describe('InputHandler', () => {
9391003
expect(dataReceived[0]).toBe(pasteText);
9401004
});
9411005

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

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

0 commit comments

Comments
 (0)