fix: add test code again

This commit is contained in:
tanzhenxin
2025-09-16 14:04:07 +08:00
parent b9fd4737c9
commit c093bed38c
3 changed files with 99 additions and 85 deletions

View File

@@ -736,16 +736,16 @@ describe('KeypressContext - Kitty Protocol', () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(keyHandler).toHaveBeenCalledTimes(1); // With the current implementation, fragmented data gets processed differently
// The first fragment '\x1b[20' gets processed as individual characters
// The second fragment '0~content\x1b[2' gets processed as paste + individual chars
// The third fragment '01~' gets processed as individual characters
expect(keyHandler).toHaveBeenCalled();
}); });
// Should properly reconstruct and handle the paste sequence // The current implementation processes fragmented paste markers as separate events
expect(keyHandler).toHaveBeenCalledWith( // rather than reconstructing them into a single paste event
expect.objectContaining({ expect(keyHandler.mock.calls.length).toBeGreaterThan(1);
paste: true,
sequence: 'content',
}),
);
}); });
}); });
@@ -812,15 +812,7 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.emit('data', Buffer.from('a')); stdin.emit('data', Buffer.from('a'));
}); });
// Should not emit immediately // With the current implementation, single characters are processed immediately
expect(keyHandler).not.toHaveBeenCalled();
// Advance timer to trigger timeout
act(() => {
vi.advanceTimersByTime(8);
});
// Should emit after timeout
expect(keyHandler).toHaveBeenCalledWith( expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
name: 'a', name: 'a',
@@ -860,22 +852,36 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.emit('data', Buffer.from('lo')); stdin.emit('data', Buffer.from('lo'));
}); });
// Should not have emitted yet // With the current implementation, data is processed as it arrives
expect(keyHandler).not.toHaveBeenCalled(); // First chunk 'hel' is treated as paste (multi-character)
expect(keyHandler).toHaveBeenNthCalledWith(
// Complete the timeout 1,
act(() => {
vi.advanceTimersByTime(8);
});
// Should emit as single paste event (multi-character data treated as paste)
expect(keyHandler).toHaveBeenCalledTimes(1);
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
paste: true, paste: true,
sequence: 'hello', sequence: 'hel',
}), }),
); );
// Second chunk 'lo' is processed as individual characters
expect(keyHandler).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
name: 'l',
sequence: 'l',
paste: false,
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
name: 'o',
sequence: 'o',
paste: false,
}),
);
expect(keyHandler).toHaveBeenCalledTimes(3);
} finally { } finally {
vi.useRealTimers(); vi.useRealTimers();
} }
@@ -951,20 +957,7 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.emit('data', Buffer.from('b')); stdin.emit('data', Buffer.from('b'));
}); });
// Advance by the original remaining time (1ms) // With the current implementation, both characters are processed immediately
act(() => {
vi.advanceTimersByTime(1);
});
// Should not have emitted yet because timeout was reset
expect(keyHandler).not.toHaveBeenCalled();
// Complete the new timeout period
act(() => {
vi.advanceTimersByTime(7);
});
// The current implementation emits 2 individual keypress events for 'a' and 'b'
expect(keyHandler).toHaveBeenCalledTimes(2); expect(keyHandler).toHaveBeenCalledTimes(2);
// First event should be 'a', second should be 'b' // First event should be 'a', second should be 'b'
@@ -1060,20 +1053,48 @@ describe('KeypressContext - Kitty Protocol', () => {
stdin.emit('data', Buffer.from('o')); stdin.emit('data', Buffer.from('o'));
}); });
// Should not have emitted yet // With the current implementation, each character is processed immediately
expect(keyHandler).not.toHaveBeenCalled(); expect(keyHandler).toHaveBeenCalledTimes(5);
// Complete timeout // Each character should be processed as individual keypress events
act(() => { expect(keyHandler).toHaveBeenNthCalledWith(
vi.advanceTimersByTime(8); 1,
});
// Should emit as single paste event (multi-character data treated as paste)
expect(keyHandler).toHaveBeenCalledTimes(1);
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
paste: true, name: 'h',
sequence: 'hello', sequence: 'h',
paste: false,
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
name: 'e',
sequence: 'e',
paste: false,
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
name: 'l',
sequence: 'l',
paste: false,
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
4,
expect.objectContaining({
name: 'l',
sequence: 'l',
paste: false,
}),
);
expect(keyHandler).toHaveBeenNthCalledWith(
5,
expect.objectContaining({
name: 'o',
sequence: 'o',
paste: false,
}), }),
); );
} finally { } finally {

View File

@@ -34,8 +34,6 @@ import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
const ESC = '\u001B'; const ESC = '\u001B';
export const PASTE_MODE_PREFIX = `${ESC}[200~`; export const PASTE_MODE_PREFIX = `${ESC}[200~`;
export const PASTE_MODE_SUFFIX = `${ESC}[201~`; export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
const RAW_PASTE_DEBOUNCE_MS = 8; // Debounce window to coalesce fragmented paste chunks
const RAW_PASTE_BUFFER_LIMIT = 32;
export interface Key { export interface Key {
name: string; name: string;
@@ -437,16 +435,18 @@ export function KeypressProvider({
// Buffer the incoming data // Buffer the incoming data
rawDataBuffer = Buffer.concat([rawDataBuffer, data]); rawDataBuffer = Buffer.concat([rawDataBuffer, data]);
// If buffered data exceeds limit, flush immediately
if (rawDataBuffer.length > RAW_PASTE_BUFFER_LIMIT) {
clearRawFlushTimeout();
flushRawBuffer();
return;
}
clearRawFlushTimeout(); clearRawFlushTimeout();
rawFlushTimeout = setTimeout(flushRawBuffer, RAW_PASTE_DEBOUNCE_MS); // On some Windows terminals, during a paste, the terminal might send a
// single return character chunk. In this case, we need to wait a time period
// to know if it is part of a paste or just a return character.
const isReturnChar =
rawDataBuffer.length <= 2 && rawDataBuffer.includes(0x0d);
if (isReturnChar) {
rawFlushTimeout = setTimeout(flushRawBuffer, 100);
} else {
flushRawBuffer();
}
}; };
let rl: readline.Interface; let rl: readline.Interface;

View File

@@ -54,8 +54,8 @@ vi.mock('readline', () => {
class MockStdin extends EventEmitter { class MockStdin extends EventEmitter {
isTTY = true; isTTY = true;
setRawMode = vi.fn(); setRawMode = vi.fn();
on = this.addListener; override on = this.addListener;
removeListener = this.removeListener; override removeListener = super.removeListener;
write = vi.fn(); write = vi.fn();
resume = vi.fn(); resume = vi.fn();
@@ -110,7 +110,7 @@ describe('useKeypress', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
stdin = new MockStdin(); stdin = new MockStdin();
(useStdin as vi.Mock).mockReturnValue({ (useStdin as ReturnType<typeof vi.fn>).mockReturnValue({
stdin, stdin,
setRawMode: mockSetRawMode, setRawMode: mockSetRawMode,
}); });
@@ -278,23 +278,16 @@ describe('useKeypress', () => {
// Unmounting should trigger the flush // Unmounting should trigger the flush
unmount(); unmount();
if (isLegacy) { // Both legacy and modern modes now flush partial paste content on unmount
// In legacy/passthrough mode, partial paste content may not be flushed on unmount expect(onKeypress).toHaveBeenCalledTimes(1);
// due to the asynchronous nature of the raw data processing pipeline. expect(onKeypress).toHaveBeenCalledWith({
// The current implementation doesn't reliably flush partial pastes in legacy mode name: '',
expect(onKeypress).not.toHaveBeenCalled(); ctrl: false,
} else { meta: false,
// In modern Node mode, partial paste content should be flushed on unmount shift: false,
expect(onKeypress).toHaveBeenCalledTimes(1); paste: true,
expect(onKeypress).toHaveBeenCalledWith({ sequence: pasteText,
name: '', });
ctrl: false,
meta: false,
shift: false,
paste: true,
sequence: pasteText,
});
}
}); });
}); });
}); });