Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions lib/input-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ interface MockClipboardEvent {
preventDefault: () => void;
stopPropagation: () => void;
}
interface MockInputEvent {
type: string;
inputType: string;
data: string | null;
isComposing?: boolean;
preventDefault: () => void;
stopPropagation: () => void;
}

interface MockHTMLElement {
addEventListener: (event: string, handler: (e: any) => void) => void;
Expand Down Expand Up @@ -79,6 +87,18 @@ function createClipboardEvent(text: string | null): MockClipboardEvent {
stopPropagation: mock(() => {}),
};
}

// Helper to create mock beforeinput event
function createBeforeInputEvent(inputType: string, data: string | null): MockInputEvent {
return {
type: 'beforeinput',
inputType,
data,
isComposing: false,
preventDefault: mock(() => {}),
stopPropagation: mock(() => {}),
};
}
interface MockCompositionEvent {
type: string;
data: string | null;
Expand Down Expand Up @@ -399,6 +419,48 @@ describe('InputHandler', () => {
expect(container.childNodes[0]).toBe(elementNode);
expect(dataReceived).toEqual(['你好']);
});

test('avoids duplicate commit when compositionend fires before beforeinput', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
undefined,
inputElement as any
);

container.dispatchEvent(createCompositionEvent('compositionend', '你好'));
inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));

expect(dataReceived).toEqual(['你好']);
});

test('avoids duplicate commit when beforeinput fires before compositionend', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
undefined,
inputElement as any
);

inputElement.dispatchEvent(createBeforeInputEvent('insertText', '你好'));
container.dispatchEvent(createCompositionEvent('compositionend', '你好'));

expect(dataReceived).toEqual(['你好']);
});
});

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

test('handles beforeinput insertFromPaste with data', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
undefined,
inputElement as any
);

const pasteText = 'Hello, beforeinput!';
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);

inputElement.dispatchEvent(beforeInputEvent);

expect(dataReceived.length).toBe(1);
expect(dataReceived[0]).toBe(pasteText);
});

test('uses bracketed paste for beforeinput insertFromPaste', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
(mode) => mode === 2004,
inputElement as any
);

const pasteText = 'Bracketed paste';
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);

inputElement.dispatchEvent(beforeInputEvent);

expect(dataReceived.length).toBe(1);
expect(dataReceived[0]).toBe(`\x1b[200~${pasteText}\x1b[201~`);
});

test('handles multi-line paste', () => {
const handler = new InputHandler(
ghostty,
Expand All @@ -958,6 +1068,58 @@ describe('InputHandler', () => {
expect(dataReceived[0]).toBe(pasteText);
});

test('ignores beforeinput insertFromPaste when paste already handled', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
undefined,
inputElement as any
);

const pasteText = 'Hello, World!';
const pasteEvent = createClipboardEvent(pasteText);
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);

container.dispatchEvent(pasteEvent);
inputElement.dispatchEvent(beforeInputEvent);

expect(dataReceived.length).toBe(1);
expect(dataReceived[0]).toBe(pasteText);
});

test('ignores paste when beforeinput insertFromPaste already handled', () => {
const inputElement = createMockContainer();
const handler = new InputHandler(
ghostty,
container as any,
(data) => dataReceived.push(data),
() => {
bellCalled = true;
},
undefined,
undefined,
undefined,
inputElement as any
);

const pasteText = 'Hello, World!';
const beforeInputEvent = createBeforeInputEvent('insertFromPaste', pasteText);
const pasteEvent = createClipboardEvent(pasteText);

inputElement.dispatchEvent(beforeInputEvent);
container.dispatchEvent(pasteEvent);

expect(dataReceived.length).toBe(1);
expect(dataReceived[0]).toBe(pasteText);
});

test('ignores paste with no clipboard data', () => {
const handler = new InputHandler(
ghostty,
Expand Down
Loading