mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-19 18:40:57 -07:00
Use raw writes to stdin where possible in tests (#11837)
This commit is contained in:
committed by
GitHub
parent
5e70a7dd46
commit
aa6ae954ef
@@ -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