Use raw writes to stdin in test (#11871)

This commit is contained in:
Tommaso Sciortino
2025-10-23 16:55:30 -07:00
committed by GitHub
parent b16fe7b646
commit 0fe82a2f4e

View File

@@ -33,6 +33,9 @@ vi.mock('ink', async (importOriginal) => {
const PASTE_START = '\x1B[200~';
const PASTE_END = '\x1B[201~';
// readline will not emit most incomplete kitty sequences but it will give
// up on sequences like this where the modifier (135) has more than two digits.
const INCOMPLETE_KITTY_SEQUENCE = '\x1b[97;135';
class MockStdin extends EventEmitter {
isTTY = true;
@@ -45,15 +48,6 @@ class MockStdin extends EventEmitter {
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);
}
}
describe('KeypressContext - Kitty Protocol', () => {
@@ -171,9 +165,7 @@ describe('KeypressContext - Kitty Protocol', () => {
act(() => result.current.subscribe(keyHandler));
// Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
act(() => {
stdin.write(`\x1b[57414;5u`);
});
act(() => stdin.write(`\x1b[57414;5u`));
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
@@ -506,15 +498,14 @@ describe('KeypressContext - Kitty Protocol', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Send Ctrl+C
act(() => stdin.write('\x03'));
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
'\x1b[1',
INCOMPLETE_KITTY_SEQUENCE,
);
// Verify Ctrl+C was handled
@@ -543,19 +534,18 @@ describe('KeypressContext - Kitty Protocol', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
const sequence = '\x1b[12';
act(() => stdin.pressKey({ sequence }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Verify debug logging for accumulation
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer accumulating:',
JSON.stringify(sequence),
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
);
// Verify warning for char codes
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Kitty sequence buffer has content:',
JSON.stringify(sequence),
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
);
});
});
@@ -829,93 +819,75 @@ describe('Kitty Sequence Parsing', () => {
// Terminals to test
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
// Key mappings: letter -> [keycode, accented character, shouldHaveMeta]
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode
const keys: Record<string, [number, string, boolean]> = {
a: [97, 'å', true],
o: [111, 'ø', true],
m: [109, 'µ', false],
// Key mappings: letter -> [keycode, accented character]
const keys: Record<string, [number, string]> = {
a: [97, 'å'],
o: [111, 'ø'],
m: [109, 'µ'],
};
it.each(
terminals.flatMap((terminal) =>
Object.entries(keys).map(
([key, [keycode, accentedChar, shouldHaveMeta]]) => {
if (terminal === 'Ghostty') {
// Ghostty uses kitty protocol sequences
return {
terminal,
key,
kittySequence: `\x1b[${keycode};3u`,
expected: {
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
kittyProtocol: true,
},
};
} else if (terminal === 'MacTerminal') {
// Mac Terminal sends ESC + letter
return {
terminal,
key,
kitty: false,
input: {
sequence: `\x1b${key}`,
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
},
expected: {
sequence: `\x1b${key}`,
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
},
};
} else {
// iTerm2 and VSCode send accented characters (å, ø, µ)
// Note: µ comes with meta:false but gets converted to m with meta:true
return {
terminal,
key,
input: {
name: key,
ctrl: false,
meta: shouldHaveMeta,
shift: false,
paste: false,
sequence: accentedChar,
},
expected: {
name: key,
ctrl: false,
meta: true, // Always expect meta:true after conversion
shift: false,
paste: false,
sequence: accentedChar,
},
};
}
},
),
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
if (terminal === 'Ghostty') {
// Ghostty uses kitty protocol sequences
return {
terminal,
key,
chunk: `\x1b[${keycode};3u`,
expected: {
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
kittyProtocol: true,
},
};
} else if (terminal === 'MacTerminal') {
// Mac Terminal sends ESC + letter
return {
terminal,
key,
kitty: false,
chunk: `\x1b${key}`,
expected: {
sequence: `\x1b${key}`,
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
},
};
} else {
// iTerm2 and VSCode send accented characters (å, ø, µ)
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
// gets converted to m with meta:true
return {
terminal,
key,
chunk: accentedChar,
expected: {
name: key,
ctrl: false,
meta: true, // Always expect meta:true after conversion
shift: false,
paste: false,
sequence: accentedChar,
},
};
}
}),
),
)(
'should handle Alt+$key in $terminal',
({
kittySequence,
input,
chunk,
expected,
kitty = true,
}: {
kittySequence?: string;
input?: Partial<Key>;
chunk: string;
expected: Partial<Key>;
kitty?: boolean;
}) => {
@@ -930,11 +902,7 @@ describe('Kitty Sequence Parsing', () => {
});
act(() => result.current.subscribe(keyHandler));
if (kittySequence) {
act(() => stdin.write(kittySequence));
} else if (input) {
act(() => stdin.pressKey(input));
}
act(() => stdin.write(chunk));
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining(expected),
@@ -978,8 +946,7 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Should not broadcast immediately
expect(keyHandler).not.toHaveBeenCalled();
@@ -991,15 +958,13 @@ describe('Kitty Sequence Parsing', () => {
expect(keyHandler).not.toHaveBeenCalled();
// Advance past timeout
act(() => {
vi.advanceTimersByTime(10);
});
act(() => vi.advanceTimersByTime(10));
// Should now broadcast the incomplete sequence as regular input
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: '',
sequence: '\x1b[1;',
sequence: INCOMPLETE_KITTY_SEQUENCE,
paste: false,
}),
);
@@ -1079,8 +1044,7 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Press Ctrl+C
act(() => stdin.write('\x03'));
@@ -1189,13 +1153,13 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Start incomplete sequence
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
act(() => stdin.write('\x1b[97;13'));
// Advance time partway
act(() => vi.advanceTimersByTime(30));
// Add more to sequence
act(() => stdin.write('3'));
act(() => stdin.write('5'));
// Advance time from the first timeout point
act(() => vi.advanceTimersByTime(25));
@@ -1209,7 +1173,7 @@ describe('Kitty Sequence Parsing', () => {
// Should now parse as complete enter key
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'return',
name: 'a',
kittyProtocol: true,
}),
);
@@ -1221,8 +1185,7 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Incomplete sequence should be buffered, not broadcast
expect(keyHandler).not.toHaveBeenCalled();
@@ -1235,7 +1198,7 @@ describe('Kitty Sequence Parsing', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: '',
sequence: '\x1b[1;',
sequence: INCOMPLETE_KITTY_SEQUENCE,
paste: false,
}),
);
@@ -1247,8 +1210,7 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Incomplete sequence should be buffered, not broadcast
expect(keyHandler).not.toHaveBeenCalled();
@@ -1261,7 +1223,7 @@ describe('Kitty Sequence Parsing', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: '',
sequence: '\x1b[1;',
sequence: INCOMPLETE_KITTY_SEQUENCE,
paste: false,
}),
);
@@ -1274,8 +1236,7 @@ describe('Kitty Sequence Parsing', () => {
act(() => result.current.subscribe(keyHandler));
// Send incomplete kitty sequence
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
// Incomplete sequence should be buffered, not broadcast
expect(keyHandler).not.toHaveBeenCalled();
@@ -1288,7 +1249,7 @@ describe('Kitty Sequence Parsing', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: '',
sequence: '\x1b[1;',
sequence: INCOMPLETE_KITTY_SEQUENCE,
paste: false,
}),
);