mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Use raw writes to stdin where possible in tests (#11837)
This commit is contained in:
committed by
GitHub
parent
5e70a7dd46
commit
aa6ae954ef
@@ -31,33 +31,29 @@ vi.mock('ink', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const PASTE_START = '\x1B[200~';
|
||||
const PASTE_END = '\x1B[201~';
|
||||
|
||||
class MockStdin extends EventEmitter {
|
||||
isTTY = true;
|
||||
setRawMode = vi.fn();
|
||||
override on = this.addListener;
|
||||
override removeListener = super.removeListener;
|
||||
write = vi.fn();
|
||||
resume = vi.fn();
|
||||
pause = vi.fn();
|
||||
|
||||
// Helper to simulate a keypress event
|
||||
write(text: string) {
|
||||
this.emit('data', Buffer.from(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to directly simulate keyPress events. Certain keypress events might
|
||||
* be impossible to fire in certain versions of node. This allows us to
|
||||
* sidestep readline entirely and just emit the keypress we want.
|
||||
*/
|
||||
pressKey(key: Partial<Key>) {
|
||||
this.emit('keypress', null, key);
|
||||
}
|
||||
|
||||
// Helper to simulate a kitty protocol sequence
|
||||
sendKittySequence(sequence: string) {
|
||||
this.emit('data', Buffer.from(sequence));
|
||||
}
|
||||
|
||||
// Helper to simulate a paste event
|
||||
sendPaste(text: string) {
|
||||
const PASTE_MODE_PREFIX = `\x1b[200~`;
|
||||
const PASTE_MODE_SUFFIX = `\x1b[201~`;
|
||||
this.emit('data', Buffer.from(PASTE_MODE_PREFIX));
|
||||
this.emit('data', Buffer.from(text));
|
||||
this.emit('data', Buffer.from(PASTE_MODE_SUFFIX));
|
||||
}
|
||||
}
|
||||
|
||||
describe('KeypressContext - Kitty Protocol', () => {
|
||||
@@ -94,13 +90,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for regular enter: ESC[13u
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[13u`);
|
||||
stdin.write(`\x1b[13u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -122,13 +116,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter: ESC[57414u
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[57414u`);
|
||||
stdin.write(`\x1b[57414u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -150,13 +142,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter with Shift (modifier 2): ESC[57414;2u
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[57414;2u`);
|
||||
stdin.write(`\x1b[57414;2u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -178,13 +168,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[57414;5u`);
|
||||
stdin.write(`\x1b[57414;5u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -206,13 +194,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter with Alt (modifier 3): ESC[57414;3u
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[57414;3u`);
|
||||
stdin.write(`\x1b[57414;3u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -234,13 +220,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: false }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[57414u`);
|
||||
stdin.write(`\x1b[57414u`);
|
||||
});
|
||||
|
||||
// When kitty protocol is disabled, the sequence should be passed through
|
||||
@@ -263,13 +247,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for escape: ESC[27u
|
||||
act(() => {
|
||||
stdin.sendKittySequence('\x1b[27u');
|
||||
stdin.write('\x1b[27u');
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -288,7 +270,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[9u`);
|
||||
stdin.write(`\x1b[9u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -307,7 +289,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
// Modifier 2 is Shift
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[9;2u`);
|
||||
stdin.write(`\x1b[9;2u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -325,7 +307,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[127u`);
|
||||
stdin.write(`\x1b[127u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -344,7 +326,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
// Modifier 3 is Alt/Option
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[127;3u`);
|
||||
stdin.write(`\x1b[127;3u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -363,7 +345,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
// Modifier 5 is Ctrl
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[127;5u`);
|
||||
stdin.write(`\x1b[127;5u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -385,13 +367,13 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Simulate a bracketed paste event
|
||||
act(() => {
|
||||
stdin.sendPaste(pastedText);
|
||||
stdin.write(PASTE_START);
|
||||
stdin.write(pastedText);
|
||||
stdin.write(PASTE_END);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -437,13 +419,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send a kitty sequence
|
||||
act(() => {
|
||||
stdin.sendKittySequence('\x1b[27u');
|
||||
stdin.write('\x1b[27u');
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalled();
|
||||
@@ -466,14 +446,10 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send a complete kitty sequence for escape
|
||||
act(() => {
|
||||
stdin.sendKittySequence('\x1b[27u');
|
||||
});
|
||||
act(() => stdin.write('\x1b[27u'));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty buffer accumulating:',
|
||||
@@ -502,15 +478,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send a long sequence starting with a valid kitty prefix to trigger overflow
|
||||
const longSequence = '\x1b[1;' + '1'.repeat(100);
|
||||
act(() => {
|
||||
stdin.sendKittySequence(longSequence);
|
||||
});
|
||||
act(() => stdin.write(longSequence));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty buffer overflow, clearing:',
|
||||
@@ -532,31 +504,13 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[1',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
||||
|
||||
// Send Ctrl+C
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: 'c',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x03',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x03'));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
||||
@@ -586,21 +540,11 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
const sequence = '\x1b[12';
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence,
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence }));
|
||||
|
||||
// Verify debug logging for accumulation
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
@@ -629,6 +573,9 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
{ sequence: `\x1b[1~`, expected: { name: 'home' } },
|
||||
{ sequence: `\x1b[4~`, expected: { name: 'end' } },
|
||||
{ sequence: `\x1b[2~`, expected: { name: 'insert' } },
|
||||
// Reverse tabs
|
||||
{ sequence: `\x1b[Z`, expected: { name: 'tab', shift: true } },
|
||||
{ sequence: `\x1b[1;2Z`, expected: { name: 'tab', shift: true } },
|
||||
// Legacy Arrows
|
||||
{
|
||||
sequence: `\x1b[A`,
|
||||
@@ -662,7 +609,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
act(() => stdin.write(sequence));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expected),
|
||||
@@ -671,33 +618,14 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('Shift+Tab forms', () => {
|
||||
it.each([
|
||||
{ sequence: `\x1b[Z`, description: 'legacy reverse Tab' },
|
||||
{ sequence: `\x1b[1;2Z`, description: 'parameterized reverse Tab' },
|
||||
])(
|
||||
'should recognize $description "$sequence" as Shift+Tab',
|
||||
({ sequence }) => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'tab', shift: true }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Double-tap and batching', () => {
|
||||
it('should emit two delete events for double-tap CSI[3~', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
act(() => stdin.write(`\x1b[3~`));
|
||||
act(() => stdin.write(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
@@ -714,7 +642,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~\x1b[5~`));
|
||||
act(() => stdin.write(`\x1b[3~\x1b[5~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
@@ -732,15 +660,9 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
// Incomplete ESC sequence then a complete Delete
|
||||
act(() => {
|
||||
// Provide an incomplete ESC sequence chunk with a real ESC character
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
stdin.write('\x1b[1;');
|
||||
});
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
act(() => stdin.write(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -786,20 +708,9 @@ describe('Drag and Drop Handling', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write(SINGLE_QUOTE));
|
||||
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -809,20 +720,9 @@ describe('Drag and Drop Handling', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: DOUBLE_QUOTE,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write(DOUBLE_QUOTE));
|
||||
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -834,33 +734,13 @@ describe('Drag and Drop Handling', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Start by single quote
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write(SINGLE_QUOTE));
|
||||
|
||||
// Send single character
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'a',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('a'));
|
||||
|
||||
// Character should not be immediately broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -885,66 +765,16 @@ describe('Drag and Drop Handling', () => {
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Start by single quote
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write(SINGLE_QUOTE));
|
||||
|
||||
// Send multiple characters
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'a',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 't',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'h',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('p'));
|
||||
act(() => stdin.write('a'));
|
||||
act(() => stdin.write('t'));
|
||||
act(() => stdin.write('h'));
|
||||
|
||||
// Characters should not be immediately broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -1101,7 +931,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
if (kittySequence) {
|
||||
act(() => stdin.sendKittySequence(kittySequence));
|
||||
act(() => stdin.write(kittySequence));
|
||||
} else if (input) {
|
||||
act(() => stdin.pressKey(input));
|
||||
}
|
||||
@@ -1126,16 +956,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() =>
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\\',
|
||||
}),
|
||||
);
|
||||
act(() => stdin.write('\\'));
|
||||
|
||||
// Advance timers to trigger the backslash timeout
|
||||
act(() => {
|
||||
@@ -1155,29 +976,16 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
|
||||
// Should not broadcast immediately
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Advance time just before timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(KITTY_SEQUENCE_TIMEOUT_MS - 5);
|
||||
});
|
||||
act(() => vi.advanceTimersByTime(KITTY_SEQUENCE_TIMEOUT_MS - 5));
|
||||
|
||||
// Still shouldn't broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -1201,22 +1009,11 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send a CSI sequence that doesn't match kitty patterns
|
||||
// ESC[m is SGR reset, not a kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[m',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x1b[m'));
|
||||
|
||||
// Should broadcast immediately as it's not a valid kitty pattern
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -1232,21 +1029,10 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send complete kitty sequence for Ctrl+A
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[97;5u',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x1b[97;5u'));
|
||||
|
||||
// Should parse and broadcast immediately
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -1262,21 +1048,10 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send multiple kitty sequences at once
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[97;5u\x1b[98;5u', // Ctrl+a followed by Ctrl+b
|
||||
});
|
||||
});
|
||||
// Send Ctrl+a followed by Ctrl+b
|
||||
act(() => stdin.write('\x1b[97;5u\x1b[98;5u'));
|
||||
|
||||
// Should parse both sequences
|
||||
expect(keyHandler).toHaveBeenCalledTimes(2);
|
||||
@@ -1302,38 +1077,16 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
|
||||
// Press Ctrl+C
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: 'c',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x03',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x03'));
|
||||
|
||||
// Advance past timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(KITTY_SEQUENCE_TIMEOUT_MS + 10);
|
||||
});
|
||||
act(() => vi.advanceTimersByTime(KITTY_SEQUENCE_TIMEOUT_MS + 10));
|
||||
|
||||
// Should only have received Ctrl+C, not the incomplete sequence
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
@@ -1349,21 +1102,11 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send valid kitty sequence followed by invalid CSI
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[13u\x1b[!', // Valid enter, then invalid sequence
|
||||
});
|
||||
});
|
||||
// Valid enter, then invalid sequence
|
||||
act(() => stdin.write('\x1b[13u\x1b[!'));
|
||||
|
||||
// Should parse valid sequence and flush invalid immediately
|
||||
expect(keyHandler).toHaveBeenCalledTimes(2);
|
||||
@@ -1390,21 +1133,10 @@ describe('Kitty Sequence Parsing', () => {
|
||||
wrapper({ children, kittyProtocolEnabled: false }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send what would be a kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[13u',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x1b[13u'));
|
||||
|
||||
// Should pass through without parsing
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -1454,58 +1186,25 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Start incomplete sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[1',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
||||
|
||||
// Advance time partway
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(30);
|
||||
});
|
||||
act(() => vi.advanceTimersByTime(30));
|
||||
|
||||
// Add more to sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '3',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('3'));
|
||||
|
||||
// Advance time from the first timeout point
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(25);
|
||||
});
|
||||
act(() => vi.advanceTimersByTime(25));
|
||||
|
||||
// Should not have timed out yet (timeout restarted)
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Complete the sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'u',
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('u'));
|
||||
|
||||
// Should now parse as complete enter key
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
@@ -1520,27 +1219,16 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Send FOCUS_IN event
|
||||
const FOCUS_IN = '\x1b[I';
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
sequence: FOCUS_IN,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x1b[I'));
|
||||
|
||||
// The buffered sequence should be flushed
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
@@ -1557,27 +1245,16 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Send FOCUS_OUT event
|
||||
const FOCUS_OUT = '\x1b[O';
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
sequence: FOCUS_OUT,
|
||||
});
|
||||
});
|
||||
act(() => stdin.write('\x1b[O'));
|
||||
|
||||
// The buffered sequence should be flushed
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
@@ -1595,25 +1272,16 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Send paste start sequence
|
||||
const PASTE_MODE_PREFIX = `\x1b[200~`;
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from(PASTE_MODE_PREFIX));
|
||||
});
|
||||
act(() => stdin.write(`\x1b[200~`));
|
||||
|
||||
// The buffered sequence should be flushed
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
@@ -1629,13 +1297,11 @@ describe('Kitty Sequence Parsing', () => {
|
||||
const pastedText = 'hello';
|
||||
const PASTE_MODE_SUFFIX = `\x1b[201~`;
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from(pastedText));
|
||||
stdin.emit('data', Buffer.from(PASTE_MODE_SUFFIX));
|
||||
stdin.write(pastedText);
|
||||
stdin.write(PASTE_MODE_SUFFIX);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
act(() => vi.runAllTimers());
|
||||
|
||||
// The paste event should be broadcast
|
||||
expect(keyHandler).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import type { Key } from './useKeypress.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { useStdin } from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
// Mock the 'ink' module to control stdin
|
||||
@@ -23,35 +21,8 @@ vi.mock('ink', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the 'readline' module
|
||||
vi.mock('readline', () => {
|
||||
const mockedReadline = {
|
||||
createInterface: vi.fn().mockReturnValue({ close: vi.fn() }),
|
||||
// The paste workaround involves replacing stdin with a PassThrough stream.
|
||||
// This mock ensures that when emitKeypressEvents is called on that
|
||||
// stream, we simulate the 'keypress' events that the hook expects.
|
||||
emitKeypressEvents: vi.fn((stream: EventEmitter) => {
|
||||
if (stream instanceof PassThrough) {
|
||||
stream.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
for (const char of str) {
|
||||
stream.emit('keypress', null, {
|
||||
name: char,
|
||||
sequence: char,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
};
|
||||
return {
|
||||
...mockedReadline,
|
||||
default: mockedReadline,
|
||||
};
|
||||
});
|
||||
const PASTE_START = '\x1B[200~';
|
||||
const PASTE_END = '\x1B[201~';
|
||||
|
||||
class MockStdin extends EventEmitter {
|
||||
isTTY = true;
|
||||
@@ -59,45 +30,11 @@ class MockStdin extends EventEmitter {
|
||||
setRawMode = vi.fn();
|
||||
override on = this.addListener;
|
||||
override removeListener = super.removeListener;
|
||||
write = vi.fn();
|
||||
resume = vi.fn();
|
||||
pause = vi.fn();
|
||||
|
||||
private isLegacy = false;
|
||||
|
||||
setLegacy(isLegacy: boolean) {
|
||||
this.isLegacy = isLegacy;
|
||||
}
|
||||
|
||||
// Helper to simulate a full paste event.
|
||||
paste(text: string) {
|
||||
if (this.isLegacy) {
|
||||
const PASTE_START = '\x1B[200~';
|
||||
const PASTE_END = '\x1B[201~';
|
||||
this.emit('data', Buffer.from(`${PASTE_START}${text}${PASTE_END}`));
|
||||
} else {
|
||||
this.emit('keypress', null, { name: 'paste-start' });
|
||||
this.emit('keypress', null, { sequence: text });
|
||||
this.emit('keypress', null, { name: 'paste-end' });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to simulate the start of a paste, without the end.
|
||||
startPaste(text: string) {
|
||||
if (this.isLegacy) {
|
||||
this.emit('data', Buffer.from('\x1B[200~' + text));
|
||||
} else {
|
||||
this.emit('keypress', null, { name: 'paste-start' });
|
||||
this.emit('keypress', null, { sequence: text });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to simulate a single keypress event.
|
||||
pressKey(key: Partial<Key>) {
|
||||
if (this.isLegacy) {
|
||||
this.emit('data', Buffer.from(key.sequence ?? ''));
|
||||
} else {
|
||||
this.emit('keypress', null, key);
|
||||
}
|
||||
write(text: string) {
|
||||
this.emit('data', Buffer.from(text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +77,7 @@ describe('useKeypress', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: false }), {
|
||||
wrapper,
|
||||
});
|
||||
act(() => stdin.pressKey({ name: 'a' }));
|
||||
act(() => stdin.write('a'));
|
||||
expect(onKeypress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -152,7 +89,7 @@ describe('useKeypress', () => {
|
||||
{ key: { name: 'down', sequence: '\x1b[B' } },
|
||||
])('should listen for keypress when active for key $key.name', ({ key }) => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||
act(() => stdin.pressKey(key));
|
||||
act(() => stdin.write(key.sequence));
|
||||
expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
|
||||
});
|
||||
|
||||
@@ -172,14 +109,14 @@ describe('useKeypress', () => {
|
||||
{ wrapper },
|
||||
);
|
||||
unmount();
|
||||
act(() => stdin.pressKey({ name: 'a' }));
|
||||
act(() => stdin.write('a'));
|
||||
expect(onKeypress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly identify alt+enter (meta key)', () => {
|
||||
renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
|
||||
const key = { name: 'return', sequence: '\x1B\r' };
|
||||
act(() => stdin.pressKey(key));
|
||||
act(() => stdin.write(key.sequence));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...key, meta: true, paste: false }),
|
||||
);
|
||||
@@ -189,12 +126,10 @@ describe('useKeypress', () => {
|
||||
{
|
||||
description: 'Modern Node (>= v20)',
|
||||
setup: () => setNodeVersion('20.0.0'),
|
||||
isLegacy: false,
|
||||
},
|
||||
{
|
||||
description: 'Legacy Node (< v20)',
|
||||
setup: () => setNodeVersion('18.0.0'),
|
||||
isLegacy: true,
|
||||
},
|
||||
{
|
||||
description: 'Workaround Env Var',
|
||||
@@ -202,12 +137,10 @@ describe('useKeypress', () => {
|
||||
setNodeVersion('20.0.0');
|
||||
vi.stubEnv('PASTE_WORKAROUND', 'true');
|
||||
},
|
||||
isLegacy: true,
|
||||
},
|
||||
])('in $description', ({ setup, isLegacy }) => {
|
||||
])('in $description', ({ setup }) => {
|
||||
beforeEach(() => {
|
||||
setup();
|
||||
stdin.setLegacy(isLegacy);
|
||||
});
|
||||
|
||||
it('should process a paste as a single event', () => {
|
||||
@@ -215,7 +148,7 @@ describe('useKeypress', () => {
|
||||
wrapper,
|
||||
});
|
||||
const pasteText = 'hello world';
|
||||
act(() => stdin.paste(pasteText));
|
||||
act(() => stdin.write(PASTE_START + pasteText + PASTE_END));
|
||||
|
||||
expect(onKeypress).toHaveBeenCalledTimes(1);
|
||||
expect(onKeypress).toHaveBeenCalledWith({
|
||||
@@ -234,19 +167,19 @@ describe('useKeypress', () => {
|
||||
});
|
||||
|
||||
const keyA = { name: 'a', sequence: 'a' };
|
||||
act(() => stdin.pressKey(keyA));
|
||||
act(() => stdin.write('a'));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...keyA, paste: false }),
|
||||
);
|
||||
|
||||
const pasteText = 'pasted';
|
||||
act(() => stdin.paste(pasteText));
|
||||
act(() => stdin.write(PASTE_START + pasteText + PASTE_END));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ paste: true, sequence: pasteText }),
|
||||
);
|
||||
|
||||
const keyB = { name: 'b', sequence: 'b' };
|
||||
act(() => stdin.pressKey(keyB));
|
||||
act(() => stdin.write('b'));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...keyB, paste: false }),
|
||||
);
|
||||
@@ -261,7 +194,7 @@ describe('useKeypress', () => {
|
||||
);
|
||||
const pasteText = 'incomplete paste';
|
||||
|
||||
act(() => stdin.startPaste(pasteText));
|
||||
act(() => stdin.write(PASTE_START + pasteText));
|
||||
|
||||
// No event should be fired yet.
|
||||
expect(onKeypress).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user