Fix tests to wrap all calls changing the UI with act. (#12268)

This commit is contained in:
Jacob Richman
2025-10-30 11:50:26 -07:00
committed by GitHub
parent cc081337b7
commit 54fa26ef0e
69 changed files with 2002 additions and 1291 deletions

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { AnsiOutputText } from './AnsiOutput.js';
import type { AnsiOutput, AnsiToken } from '@google/gemini-cli-core';

View File

@@ -5,7 +5,7 @@
*/
import { describe, it, expect, vi } from 'vitest';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { Text } from 'ink';
import { Composer } from './Composer.js';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';

View File

@@ -6,7 +6,8 @@
import { Text } from 'ink';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { act } from 'react';
import { ConsentPrompt } from './ConsentPrompt.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
@@ -32,7 +33,7 @@ describe('ConsentPrompt', () => {
it('renders a string prompt with MarkdownDisplay', () => {
const prompt = 'Are you sure?';
render(
const { unmount } = render(
<ConsentPrompt
prompt={prompt}
onConfirm={onConfirm}
@@ -48,11 +49,12 @@ describe('ConsentPrompt', () => {
},
undefined,
);
unmount();
});
it('renders a ReactNode prompt directly', () => {
const prompt = <Text>Are you sure?</Text>;
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<ConsentPrompt
prompt={prompt}
onConfirm={onConfirm}
@@ -62,11 +64,12 @@ describe('ConsentPrompt', () => {
expect(MockedMarkdownDisplay).not.toHaveBeenCalled();
expect(lastFrame()).toContain('Are you sure?');
unmount();
});
it('calls onConfirm with true when "Yes" is selected', () => {
const prompt = 'Are you sure?';
render(
const { unmount } = render(
<ConsentPrompt
prompt={prompt}
onConfirm={onConfirm}
@@ -75,14 +78,17 @@ describe('ConsentPrompt', () => {
);
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
onSelect(true);
act(() => {
onSelect(true);
});
expect(onConfirm).toHaveBeenCalledWith(true);
unmount();
});
it('calls onConfirm with false when "No" is selected', () => {
const prompt = 'Are you sure?';
render(
const { unmount } = render(
<ConsentPrompt
prompt={prompt}
onConfirm={onConfirm}
@@ -91,14 +97,17 @@ describe('ConsentPrompt', () => {
);
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
onSelect(false);
act(() => {
onSelect(false);
});
expect(onConfirm).toHaveBeenCalledWith(false);
unmount();
});
it('passes correct items to RadioButtonSelect', () => {
const prompt = 'Are you sure?';
render(
const { unmount } = render(
<ConsentPrompt
prompt={prompt}
onConfirm={onConfirm}
@@ -115,5 +124,6 @@ describe('ConsentPrompt', () => {
}),
undefined,
);
unmount();
});
});

View File

@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
@@ -37,17 +37,18 @@ describe('<ContextSummaryDisplay />', () => {
};
it('should render on a single line on a wide screen', () => {
const { lastFrame } = renderWithWidth(120, baseProps);
const { lastFrame, unmount } = renderWithWidth(120, baseProps);
const output = lastFrame()!;
expect(output).toContain(
'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file | 1 MCP server',
);
// Check for absence of newlines
expect(output.includes('\n')).toBe(false);
unmount();
});
it('should render on multiple lines on a narrow screen', () => {
const { lastFrame } = renderWithWidth(60, baseProps);
const { lastFrame, unmount } = renderWithWidth(60, baseProps);
const output = lastFrame()!;
const expectedLines = [
' Using:',
@@ -57,17 +58,26 @@ describe('<ContextSummaryDisplay />', () => {
];
const actualLines = output.split('\n');
expect(actualLines).toEqual(expectedLines);
unmount();
});
it('should switch layout at the 80-column breakpoint', () => {
// At 80 columns, should be on one line
const { lastFrame: wideFrame } = renderWithWidth(80, baseProps);
const { lastFrame: wideFrame, unmount: unmountWide } = renderWithWidth(
80,
baseProps,
);
expect(wideFrame()!.includes('\n')).toBe(false);
unmountWide();
// At 79 columns, should be on multiple lines
const { lastFrame: narrowFrame } = renderWithWidth(79, baseProps);
const { lastFrame: narrowFrame, unmount: unmountNarrow } = renderWithWidth(
79,
baseProps,
);
expect(narrowFrame()!.includes('\n')).toBe(true);
expect(narrowFrame()!.split('\n').length).toBe(4);
unmountNarrow();
});
it('should not render empty parts', () => {
@@ -77,9 +87,10 @@ describe('<ContextSummaryDisplay />', () => {
contextFileNames: [],
mcpServers: {},
};
const { lastFrame } = renderWithWidth(60, props);
const { lastFrame, unmount } = renderWithWidth(60, props);
const expectedLines = [' Using:', ' - 1 open file (ctrl+g to view)'];
const actualLines = lastFrame()!.split('\n');
expect(actualLines).toEqual(expectedLines);
unmount();
});
});

View File

@@ -5,6 +5,7 @@
*/
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
import { vi } from 'vitest';
import { FolderTrustDialog } from './FolderTrustDialog.js';
@@ -54,12 +55,12 @@ describe('FolderTrustDialog', () => {
stdin.write('\u001b[27u'); // Press kitty escape key
});
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain(
'A folder trust level must be selected to continue. Exiting since escape was pressed.',
);
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockedExit).toHaveBeenCalledWith(1);
});
expect(onSelect).not.toHaveBeenCalled();
@@ -93,7 +94,7 @@ describe('FolderTrustDialog', () => {
stdin.write('r');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockedExit).not.toHaveBeenCalled();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Header } from './Header.js';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
@@ -20,25 +20,35 @@ describe('<Header />', () => {
columns: 120,
rows: 20,
});
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
const { lastFrame, unmount } = render(
<Header version="1.0.0" nightly={false} />,
);
expect(lastFrame()).toContain(longAsciiLogo);
unmount();
});
it('renders custom ASCII art when provided', () => {
const customArt = 'CUSTOM ART';
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
);
expect(lastFrame()).toContain(customArt);
unmount();
});
it('displays the version number when nightly is true', () => {
const { lastFrame } = render(<Header version="1.0.0" nightly={true} />);
const { lastFrame, unmount } = render(
<Header version="1.0.0" nightly={true} />,
);
expect(lastFrame()).toContain('v1.0.0');
unmount();
});
it('does not display the version number when nightly is false', () => {
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
const { lastFrame, unmount } = render(
<Header version="1.0.0" nightly={false} />,
);
expect(lastFrame()).not.toContain('v1.0.0');
unmount();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect } from 'vitest';
import { Help } from './Help.js';
import type { SlashCommand } from '../commands/types.js';
@@ -44,18 +44,20 @@ const mockCommands: readonly SlashCommand[] = [
describe('Help Component', () => {
it('should not render hidden commands', () => {
const { lastFrame } = render(<Help commands={mockCommands} />);
const { lastFrame, unmount } = render(<Help commands={mockCommands} />);
const output = lastFrame();
expect(output).toContain('/test');
expect(output).not.toContain('/hidden');
unmount();
});
it('should not render hidden subcommands', () => {
const { lastFrame } = render(<Help commands={mockCommands} />);
const { lastFrame, unmount } = render(<Help commands={mockCommands} />);
const output = lastFrame();
expect(output).toContain('visible-child');
expect(output).not.toContain('hidden-child');
unmount();
});
});

View File

@@ -5,6 +5,7 @@
*/
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
import type { InputPromptProps } from './InputPrompt.js';
import { InputPrompt } from './InputPrompt.js';
@@ -240,7 +241,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled(),
);
unmount();
@@ -252,10 +253,10 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[B');
await waitFor(() =>
expect(mockShellHistory.getNextCommand).toHaveBeenCalled(),
);
});
await vi.waitFor(() =>
expect(mockShellHistory.getNextCommand).toHaveBeenCalled(),
);
unmount();
});
@@ -269,7 +270,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
});
@@ -284,7 +285,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith(
'ls -l',
);
@@ -300,21 +301,19 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A'); // Up arrow
});
await vi.waitFor(() =>
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
);
await waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockInputHistory.navigateDown).toHaveBeenCalled(),
);
await act(async () => {
stdin.write('\r'); // Enter
});
await vi.waitFor(() =>
await waitFor(() =>
expect(props.onSubmit).toHaveBeenCalledWith('some text'),
);
@@ -342,14 +341,14 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A'); // Up arrow
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(1),
);
await act(async () => {
stdin.write('\u0010'); // Ctrl+P
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2),
);
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
@@ -374,14 +373,14 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(1),
);
await act(async () => {
stdin.write('\u000E'); // Ctrl+N
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2),
);
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
@@ -395,32 +394,29 @@ describe('InputPrompt', () => {
showSuggestions: false,
});
props.buffer.setText('some text');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await act(async () => {
stdin.write('\u001B[A'); // Up arrow
});
await vi.waitFor(() =>
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
);
await waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockInputHistory.navigateDown).toHaveBeenCalled(),
);
await act(async () => {
stdin.write('\u0010'); // Ctrl+P
});
await vi.waitFor(() => {});
await act(async () => {
stdin.write('\u000E'); // Ctrl+N
});
await vi.waitFor(() => {});
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
await waitFor(() => {
expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
});
unmount();
});
@@ -447,7 +443,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x16'); // Ctrl+V
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(
props.config.getTargetDir(),
@@ -470,7 +466,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x16'); // Ctrl+V
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
});
expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled();
@@ -489,7 +485,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x16'); // Ctrl+V
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
});
expect(mockBuffer.setText).not.toHaveBeenCalled();
@@ -518,7 +514,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x16'); // Ctrl+V
});
await vi.waitFor(() => {
await waitFor(() => {
// Should insert at cursor position with spaces
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
});
@@ -549,7 +545,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x16'); // Ctrl+V
});
await vi.waitFor(() => {
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error handling clipboard image:',
expect.any(Error),
@@ -577,7 +573,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t'); // Press Tab
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
);
unmount();
@@ -601,7 +597,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t'); // Press Tab
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1),
);
unmount();
@@ -626,7 +622,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t'); // Press Tab
});
await vi.waitFor(() =>
await waitFor(() =>
// It should NOT become '/show'. It should correctly become '/memory show'.
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
);
@@ -648,7 +644,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t'); // Press Tab
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
);
unmount();
@@ -668,7 +664,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() => {
await waitFor(() => {
// The app should autocomplete the text, NOT submit.
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
});
@@ -700,7 +696,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t'); // Press Tab for autocomplete
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
);
unmount();
@@ -714,9 +710,10 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r'); // Press Enter
});
await vi.waitFor(() => {});
expect(props.onSubmit).not.toHaveBeenCalled();
await waitFor(() => {
expect(props.onSubmit).not.toHaveBeenCalled();
});
unmount();
});
@@ -733,9 +730,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() =>
expect(props.onSubmit).toHaveBeenCalledWith('/clear'),
);
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
unmount();
});
@@ -752,9 +747,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() =>
expect(props.onSubmit).toHaveBeenCalledWith('/clear'),
);
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
unmount();
});
@@ -772,7 +765,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
);
expect(props.onSubmit).not.toHaveBeenCalled();
@@ -790,7 +783,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.buffer.backspace).toHaveBeenCalled();
expect(props.buffer.newline).toHaveBeenCalled();
});
@@ -800,13 +793,15 @@ describe('InputPrompt', () => {
});
it('should clear the buffer on Ctrl+C if it has text', async () => {
props.buffer.setText('some text to clear');
await act(async () => {
props.buffer.setText('some text to clear');
});
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await act(async () => {
stdin.write('\x03'); // Ctrl+C character
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.buffer.setText).toHaveBeenCalledWith('');
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
});
@@ -821,9 +816,10 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x03'); // Ctrl+C character
});
await vi.waitFor(() => {});
expect(props.buffer.setText).not.toHaveBeenCalled();
await waitFor(() => {
expect(props.buffer.setText).not.toHaveBeenCalled();
});
unmount();
});
@@ -922,7 +918,7 @@ describe('InputPrompt', () => {
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
await vi.waitFor(() => {
await waitFor(() => {
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
mockBuffer,
['/test/project/src'],
@@ -949,7 +945,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('i');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.vimHandleInput).toHaveBeenCalled();
});
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
@@ -965,7 +961,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('i');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.vimHandleInput).toHaveBeenCalled();
expect(mockBuffer.handleInput).toHaveBeenCalled();
});
@@ -982,7 +978,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('i');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.vimHandleInput).toHaveBeenCalled();
expect(mockBuffer.handleInput).toHaveBeenCalled();
});
@@ -1000,7 +996,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B[200~pasted text\x1B[201~');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockBuffer.handleInput).toHaveBeenCalledWith(
expect.objectContaining({
paste: true,
@@ -1020,7 +1016,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('a');
});
await vi.waitFor(() => {});
await waitFor(() => {});
expect(mockBuffer.handleInput).not.toHaveBeenCalled();
unmount();
@@ -1090,7 +1086,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame();
expect(frame).toContain(expected);
});
@@ -1147,7 +1143,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame();
expect(frame).toContain(expected);
});
@@ -1171,7 +1167,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame();
const lines = frame!.split('\n');
// The line with the cursor should just be an inverted space inside the box border
@@ -1203,7 +1199,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame();
// Check that all lines, including the empty one, are rendered.
// This implicitly tests that the Box wrapper provides height for the empty line.
@@ -1241,7 +1237,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write(`\x1b[200~${pastedText}\x1b[201~`);
});
await vi.waitFor(() => {
await waitFor(() => {
// Verify that the buffer's handleInput was called once with the full text
expect(props.buffer.handleInput).toHaveBeenCalledTimes(1);
expect(props.buffer.handleInput).toHaveBeenCalledWith(
@@ -1277,7 +1273,9 @@ describe('InputPrompt', () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Simulate a paste operation (this should set the paste protection)
await act(async () => {
@@ -1288,7 +1286,9 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Verify that onSubmit was NOT called due to recent paste protection
expect(props.onSubmit).not.toHaveBeenCalled();
@@ -1304,13 +1304,17 @@ describe('InputPrompt', () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Simulate a paste operation (this sets the protection)
await act(async () => {
stdin.write('\x1b[200~pasted text\x1b[201~');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Advance timers past the protection timeout
await act(async () => {
@@ -1321,7 +1325,9 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
expect(props.onSubmit).toHaveBeenCalledWith('pasted text');
expect(props.buffer.newline).not.toHaveBeenCalled();
@@ -1349,19 +1355,25 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
{ kittyProtocolEnabled: true },
);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Simulate a paste operation
await act(async () => {
stdin.write('\x1b[200~some pasted stuff\x1b[201~');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Simulate an Enter key press immediately after paste
await act(async () => {
stdin.write('\r');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Verify that onSubmit was called
expect(props.onSubmit).toHaveBeenCalledWith('pasted command');
@@ -1376,13 +1388,17 @@ describe('InputPrompt', () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Press Enter without any recent paste
await act(async () => {
stdin.write('\r');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
// Verify that onSubmit was called normally
expect(props.onSubmit).toHaveBeenCalledWith('normal command');
@@ -1404,17 +1420,17 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B');
});
await vi.waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(true);
await waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
});
});
await act(async () => {
stdin.write('\x1B');
});
await vi.waitFor(() => {
expect(props.buffer.setText).toHaveBeenCalledWith('');
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
await waitFor(() => {
expect(props.buffer.setText).toHaveBeenCalledWith('');
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
});
});
unmount();
});
@@ -1431,18 +1447,16 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B');
});
await vi.waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(true);
await waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
});
});
await act(async () => {
stdin.write('a');
});
await vi.waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
await waitFor(() => {
expect(onEscapePromptChange).toHaveBeenCalledWith(false);
});
});
unmount();
});
@@ -1457,10 +1471,10 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B');
await waitFor(() =>
expect(props.setShellModeActive).toHaveBeenCalledWith(false),
);
});
await vi.waitFor(() =>
expect(props.setShellModeActive).toHaveBeenCalledWith(false),
);
unmount();
});
@@ -1479,7 +1493,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(),
);
unmount();
@@ -1494,12 +1508,16 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
{ kittyProtocolEnabled: false },
);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
await act(async () => {
stdin.write('\x1B');
});
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
vi.useRealTimers();
unmount();
@@ -1514,12 +1532,12 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x0C');
});
await vi.waitFor(() => expect(props.onClearScreen).toHaveBeenCalled());
await waitFor(() => expect(props.onClearScreen).toHaveBeenCalled());
await act(async () => {
stdin.write('\x01');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(props.buffer.move).toHaveBeenCalledWith('home'),
);
unmount();
@@ -1561,7 +1579,7 @@ describe('InputPrompt', () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame();
expect(frame).toContain('(r:)');
expect(frame).toContain('echo hello');
@@ -1580,7 +1598,6 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x12');
});
await vi.waitFor(() => {});
await act(async () => {
stdin.write('\x1B');
});
@@ -1588,12 +1605,11 @@ describe('InputPrompt', () => {
stdin.write('\u001b[27u'); // Press kitty escape key
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).not.toContain('(r:)');
expect(stdout.lastFrame()).not.toContain('echo hello');
});
expect(stdout.lastFrame()).not.toContain('echo hello');
unmount();
});
@@ -1629,7 +1645,7 @@ describe('InputPrompt', () => {
});
// Verify reverse search is active
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).toContain('(r:)');
});
@@ -1637,7 +1653,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\t');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockHandleAutocomplete).toHaveBeenCalledWith(0);
expect(props.buffer.setText).toHaveBeenCalledWith('echo hello');
});
@@ -1665,7 +1681,7 @@ describe('InputPrompt', () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).toContain('(r:)');
});
@@ -1673,7 +1689,7 @@ describe('InputPrompt', () => {
stdin.write('\r');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).not.toContain('(r:)');
});
@@ -1708,7 +1724,7 @@ describe('InputPrompt', () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).toContain('(r:)');
});
@@ -1717,7 +1733,7 @@ describe('InputPrompt', () => {
stdin.write('\u001b[27u');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).not.toContain('(r:)');
expect(props.buffer.text).toBe(initialText);
expect(props.buffer.cursor).toEqual(initialCursor);
@@ -1740,7 +1756,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x05'); // Ctrl+E
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.buffer.move).toHaveBeenCalledWith('end');
});
expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
@@ -1759,7 +1775,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x05'); // Ctrl+E
});
await vi.waitFor(() => {
await waitFor(() => {
expect(props.buffer.move).toHaveBeenCalledWith('end');
});
expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
@@ -1793,7 +1809,7 @@ describe('InputPrompt', () => {
stdin.write('\x12'); // Ctrl+R
});
await vi.waitFor(() => {
await waitFor(() => {
const frame = stdout.lastFrame() ?? '';
expect(frame).toContain('(r:)');
expect(frame).toContain('git commit');
@@ -1822,14 +1838,14 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clean(stdout.lastFrame())).toContain('→');
});
await act(async () => {
stdin.write('\u001B[C');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clean(stdout.lastFrame())).toContain('←');
});
expect(stdout.lastFrame()).toMatchSnapshot(
@@ -1839,7 +1855,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[D');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(clean(stdout.lastFrame())).toContain('→');
});
expect(stdout.lastFrame()).toMatchSnapshot(
@@ -1871,7 +1887,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).toMatchSnapshot(
'command-search-render-collapsed-match',
);
@@ -1880,7 +1896,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[C');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).toMatchSnapshot(
'command-search-render-expanded-match',
);
@@ -1909,7 +1925,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x12');
});
await vi.waitFor(() => {
await waitFor(() => {
const frame = clean(stdout.lastFrame());
// Ensure it rendered the search mode
expect(frame).toContain('(r:)');
@@ -1933,7 +1949,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
const callback = mockPopAllMessages.mock.calls[0][0];
await act(async () => {
@@ -1957,7 +1973,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
);
expect(mockPopAllMessages).not.toHaveBeenCalled();
@@ -1976,7 +1992,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
const callback = mockPopAllMessages.mock.calls[0][0];
await act(async () => {
callback(undefined);
@@ -2002,7 +2018,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
unmount();
});
@@ -2018,7 +2034,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
const callback = mockPopAllMessages.mock.calls[0][0];
await act(async () => {
@@ -2041,7 +2057,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
unmount();
});
@@ -2056,7 +2072,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() =>
await waitFor(() =>
expect(mockInputHistory.navigateUp).toHaveBeenCalled(),
);
unmount();
@@ -2074,7 +2090,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\u001B[A');
});
await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
await waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
const callback = mockPopAllMessages.mock.calls[0][0];
await act(async () => {
@@ -2094,7 +2110,7 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
unmount();
});
@@ -2103,7 +2119,7 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
unmount();
});
@@ -2112,7 +2128,7 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
await waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
unmount();
});
@@ -2122,7 +2138,7 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await vi.waitFor(() => {
await waitFor(() => {
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
// This snapshot is good to make sure there was an input prompt but does
// not show the inverted cursor because snapshots do not show colors.
@@ -2140,7 +2156,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('a');
});
await vi.waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled());
await waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled());
unmount();
});
describe('command queuing while streaming', () => {
@@ -2184,7 +2200,7 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\r');
});
await vi.waitFor(() => {
await waitFor(() => {
if (shouldSubmit) {
expect(props.onSubmit).toHaveBeenCalledWith(bufferText);
expect(props.setQueueErrorMessage).not.toHaveBeenCalled();

View File

@@ -5,7 +5,7 @@
*/
import React from 'react';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { Text } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StreamingContext } from '../contexts/StreamingContext.js';
@@ -96,11 +96,12 @@ describe('<LoadingIndicator />', () => {
currentLoadingPhrase: 'Processing data...',
elapsedTime: 3,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
expect(lastFrame()).toContain('Processing data...');
unmount();
});
it('should display the elapsedTime correctly when Responding', () => {
@@ -108,11 +109,12 @@ describe('<LoadingIndicator />', () => {
currentLoadingPhrase: 'Working...',
elapsedTime: 60,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
expect(lastFrame()).toContain('(esc to cancel, 1m)');
unmount();
});
it('should display the elapsedTime correctly in human-readable format', () => {
@@ -120,24 +122,26 @@ describe('<LoadingIndicator />', () => {
currentLoadingPhrase: 'Working...',
elapsedTime: 125,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
expect(lastFrame()).toContain('(esc to cancel, 2m 5s)');
unmount();
});
it('should render rightContent when provided', () => {
const rightContent = <Text>Extra Info</Text>;
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...defaultProps} rightContent={rightContent} />,
StreamingState.Responding,
);
expect(lastFrame()).toContain('Extra Info');
unmount();
});
it('should transition correctly between states using rerender', () => {
const { lastFrame, rerender } = renderWithContext(
const { lastFrame, rerender, unmount } = renderWithContext(
<LoadingIndicator {...defaultProps} />,
StreamingState.Idle,
);
@@ -179,6 +183,7 @@ describe('<LoadingIndicator />', () => {
</StreamingContext.Provider>,
);
expect(lastFrame()).toBe('');
unmount();
});
it('should display fallback phrase if thought is empty', () => {
@@ -187,12 +192,13 @@ describe('<LoadingIndicator />', () => {
currentLoadingPhrase: 'Loading...',
elapsedTime: 5,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
const output = lastFrame();
expect(output).toContain('Loading...');
unmount();
});
it('should display the subject of a thought', () => {
@@ -203,7 +209,7 @@ describe('<LoadingIndicator />', () => {
},
elapsedTime: 5,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
@@ -213,6 +219,7 @@ describe('<LoadingIndicator />', () => {
expect(output).toContain('Thinking about something...');
expect(output).not.toContain('and other stuff.');
}
unmount();
});
it('should prioritize thought.subject over currentLoadingPhrase', () => {
@@ -224,17 +231,18 @@ describe('<LoadingIndicator />', () => {
currentLoadingPhrase: 'This should not be displayed',
elapsedTime: 5,
};
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
const output = lastFrame();
expect(output).toContain('This should be displayed');
expect(output).not.toContain('This should not be displayed');
unmount();
});
it('should truncate long primary text instead of wrapping', () => {
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator
{...defaultProps}
currentLoadingPhrase={
@@ -246,11 +254,12 @@ describe('<LoadingIndicator />', () => {
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
describe('responsive layout', () => {
it('should render on a single line on a wide terminal', () => {
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator
{...defaultProps}
rightContent={<Text>Right</Text>}
@@ -264,10 +273,11 @@ describe('<LoadingIndicator />', () => {
expect(output).toContain('Loading...');
expect(output).toContain('(esc to cancel, 5s)');
expect(output).toContain('Right');
unmount();
});
it('should render on multiple lines on a narrow terminal', () => {
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator
{...defaultProps}
rightContent={<Text>Right</Text>}
@@ -288,24 +298,27 @@ describe('<LoadingIndicator />', () => {
expect(lines[1]).toContain('(esc to cancel, 5s)');
expect(lines[2]).toContain('Right');
}
unmount();
});
it('should use wide layout at 80 columns', () => {
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...defaultProps} />,
StreamingState.Responding,
80,
);
expect(lastFrame()?.includes('\n')).toBe(false);
unmount();
});
it('should use narrow layout at 79 columns', () => {
const { lastFrame } = renderWithContext(
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...defaultProps} />,
StreamingState.Responding,
79,
);
expect(lastFrame()?.includes('\n')).toBe(true);
unmount();
});
});
});

View File

@@ -4,7 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render, cleanup } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { cleanup } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -80,16 +81,17 @@ describe('<ModelDialog />', () => {
});
it('renders the title and help text', () => {
const { lastFrame } = renderComponent();
const { lastFrame, unmount } = renderComponent();
expect(lastFrame()).toContain('Select Model');
expect(lastFrame()).toContain('(Press Esc to close)');
expect(lastFrame()).toContain(
'> To use a specific Gemini model, use the --model flag.',
);
unmount();
});
it('passes all model options to DescriptiveRadioButtonSelect', () => {
renderComponent();
const { unmount } = renderComponent();
expect(mockedSelect).toHaveBeenCalledTimes(1);
const props = mockedSelect.mock.calls[0][0];
@@ -99,11 +101,12 @@ describe('<ModelDialog />', () => {
expect(props.items[2].value).toBe(DEFAULT_GEMINI_FLASH_MODEL);
expect(props.items[3].value).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
expect(props.showNumbers).toBe(true);
unmount();
});
it('initializes with the model from ConfigContext', () => {
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_FLASH_MODEL);
renderComponent({}, { getModel: mockGetModel });
const { unmount } = renderComponent({}, { getModel: mockGetModel });
expect(mockGetModel).toHaveBeenCalled();
expect(mockedSelect).toHaveBeenCalledWith(
@@ -112,10 +115,11 @@ describe('<ModelDialog />', () => {
}),
undefined,
);
unmount();
});
it('initializes with "auto" model if context is not provided', () => {
renderComponent({}, undefined);
const { unmount } = renderComponent({}, undefined);
expect(mockedSelect).toHaveBeenCalledWith(
expect.objectContaining({
@@ -123,13 +127,14 @@ describe('<ModelDialog />', () => {
}),
undefined,
);
unmount();
});
it('initializes with "auto" model if getModel returns undefined', () => {
const mockGetModel = vi.fn(() => undefined);
// @ts-expect-error This test validates component robustness when getModel
// returns an unexpected undefined value.
renderComponent({}, { getModel: mockGetModel });
const { unmount } = renderComponent({}, { getModel: mockGetModel });
expect(mockGetModel).toHaveBeenCalled();
@@ -142,10 +147,11 @@ describe('<ModelDialog />', () => {
undefined,
);
expect(mockedSelect).toHaveBeenCalledTimes(1);
unmount();
});
it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => {
const { props, mockConfig } = renderComponent({}, {}); // Pass empty object for contextValue
const { props, mockConfig, unmount } = renderComponent({}, {}); // Pass empty object for contextValue
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
expect(childOnSelect).toBeDefined();
@@ -155,17 +161,19 @@ describe('<ModelDialog />', () => {
// Assert against the default mock provided by renderComponent
expect(mockConfig?.setModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
expect(props.onClose).toHaveBeenCalledTimes(1);
unmount();
});
it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => {
renderComponent();
const { unmount } = renderComponent();
const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight;
expect(childOnHighlight).toBeUndefined();
unmount();
});
it('calls onClose prop when "escape" key is pressed', () => {
const { props } = renderComponent();
const { props, unmount } = renderComponent();
expect(mockedUseKeypress).toHaveBeenCalled();
@@ -193,11 +201,12 @@ describe('<ModelDialog />', () => {
sequence: '',
});
expect(props.onClose).toHaveBeenCalledTimes(1);
unmount();
});
it('updates initialIndex when config context changes', () => {
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO);
const { rerender } = render(
const { rerender, unmount } = render(
<ConfigContext.Provider
value={{ getModel: mockGetModel } as unknown as Config}
>
@@ -219,5 +228,6 @@ describe('<ModelDialog />', () => {
// Should be called at least twice: initial render + re-render after context change
expect(mockedSelect).toHaveBeenCalledTimes(2);
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(3);
unmount();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';

View File

@@ -7,6 +7,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
import { TrustLevel } from '../../config/trustedFolders.js';
import { act } from 'react';
@@ -68,7 +69,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Modify Trust Level');
expect(lastFrame()).toContain('Folder: /test/dir');
expect(lastFrame()).toContain('Current Level: DO_NOT_TRUST');
@@ -90,7 +91,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain(
'Note: This folder behaves as a trusted folder because one of the parent folders is trusted.',
);
@@ -112,7 +113,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain(
'Note: This folder behaves as a trusted folder because the connected IDE workspace is trusted.',
);
@@ -124,7 +125,7 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
);
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Trust this folder (dir)');
expect(lastFrame()).toContain('Trust parent folder (test)');
});
@@ -136,13 +137,13 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => {
stdin.write('\u001b[27u'); // Kitty escape key
});
await vi.waitFor(() => {
await waitFor(() => {
expect(onExit).toHaveBeenCalled();
});
});
@@ -167,11 +168,11 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => stdin.write('r')); // Press 'r' to restart
await vi.waitFor(() => {
await waitFor(() => {
expect(mockCommitTrustLevelChange).toHaveBeenCalled();
expect(mockRelaunchApp).toHaveBeenCalled();
expect(onExit).toHaveBeenCalled();
@@ -197,11 +198,11 @@ describe('PermissionsModifyTrustDialog', () => {
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
);
await vi.waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
act(() => stdin.write('\u001b[27u')); // Press kitty escape key
await vi.waitFor(() => {
await waitFor(() => {
expect(mockCommitTrustLevelChange).not.toHaveBeenCalled();
expect(onExit).toHaveBeenCalled();
});

View File

@@ -5,7 +5,7 @@
*/
import { describe, it, expect } from 'vitest';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { PrepareLabel, MAX_WIDTH } from './PrepareLabel.js';
describe('PrepareLabel', () => {
@@ -13,7 +13,7 @@ describe('PrepareLabel', () => {
const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, '');
it('renders plain label when no match (short label)', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label="simple command"
userInput=""
@@ -23,11 +23,12 @@ describe('PrepareLabel', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('truncates long label when collapsed and no match', () => {
const long = 'x'.repeat(MAX_WIDTH + 25);
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label={long}
userInput=""
@@ -40,11 +41,12 @@ describe('PrepareLabel', () => {
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(MAX_WIDTH + 3);
expect(out).toMatchSnapshot();
unmount();
});
it('shows full long label when expanded and no match', () => {
const long = 'y'.repeat(MAX_WIDTH + 25);
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label={long}
userInput=""
@@ -56,13 +58,14 @@ describe('PrepareLabel', () => {
const f = flat(out);
expect(f.length).toBe(long.length);
expect(out).toMatchSnapshot();
unmount();
});
it('highlights matched substring when expanded (text only visible)', () => {
const label = 'run: git commit -m "feat: add search"';
const userInput = 'commit';
const matchedIndex = label.indexOf(userInput);
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label={label}
userInput={userInput}
@@ -72,6 +75,7 @@ describe('PrepareLabel', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('creates centered window around match when collapsed', () => {
@@ -80,7 +84,7 @@ describe('PrepareLabel', () => {
const suffix = '/and/then/some/more/components/'.repeat(3);
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label={label}
userInput={core}
@@ -95,6 +99,7 @@ describe('PrepareLabel', () => {
expect(f.startsWith('...')).toBe(true);
expect(f.endsWith('...')).toBe(true);
expect(out).toMatchSnapshot();
unmount();
});
it('truncates match itself when match is very long', () => {
@@ -103,7 +108,7 @@ describe('PrepareLabel', () => {
const suffix = ' in this text';
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<PrepareLabel
label={label}
userInput={core}
@@ -119,5 +124,6 @@ describe('PrepareLabel', () => {
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(MAX_WIDTH + 2);
expect(out).toMatchSnapshot();
unmount();
});
});

View File

@@ -4,7 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { act } from 'react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { ProQuotaDialog } from './ProQuotaDialog.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
@@ -20,7 +21,7 @@ describe('ProQuotaDialog', () => {
});
it('should render with correct title and options', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<ProQuotaDialog
failedModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
@@ -49,11 +50,12 @@ describe('ProQuotaDialog', () => {
}),
undefined,
);
unmount();
});
it('should call onChoice with "auth" when "Change auth" is selected', () => {
const mockOnChoice = vi.fn();
render(
const { unmount } = render(
<ProQuotaDialog
failedModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
@@ -65,14 +67,17 @@ describe('ProQuotaDialog', () => {
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
// Simulate the selection
onSelect('auth');
act(() => {
onSelect('auth');
});
expect(mockOnChoice).toHaveBeenCalledWith('auth');
unmount();
});
it('should call onChoice with "continue" when "Continue with flash" is selected', () => {
const mockOnChoice = vi.fn();
render(
const { unmount } = render(
<ProQuotaDialog
failedModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
@@ -84,8 +89,11 @@ describe('ProQuotaDialog', () => {
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
// Simulate the selection
onSelect('continue');
act(() => {
onSelect('continue');
});
expect(mockOnChoice).toHaveBeenCalledWith('continue');
unmount();
});
});

View File

@@ -5,24 +5,28 @@
*/
import { describe, it, expect } from 'vitest';
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
describe('QueuedMessageDisplay', () => {
it('renders nothing when message queue is empty', () => {
const { lastFrame } = render(<QueuedMessageDisplay messageQueue={[]} />);
const { lastFrame, unmount } = render(
<QueuedMessageDisplay messageQueue={[]} />,
);
expect(lastFrame()).toBe('');
unmount();
});
it('displays single queued message', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<QueuedMessageDisplay messageQueue={['First message']} />,
);
const output = lastFrame();
expect(output).toContain('Queued (press ↑ to edit):');
expect(output).toContain('First message');
unmount();
});
it('displays multiple queued messages', () => {
@@ -32,7 +36,7 @@ describe('QueuedMessageDisplay', () => {
'Third queued message',
];
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<QueuedMessageDisplay messageQueue={messageQueue} />,
);
@@ -41,6 +45,7 @@ describe('QueuedMessageDisplay', () => {
expect(output).toContain('First queued message');
expect(output).toContain('Second queued message');
expect(output).toContain('Third queued message');
unmount();
});
it('shows overflow indicator when more than 3 messages are queued', () => {
@@ -52,7 +57,7 @@ describe('QueuedMessageDisplay', () => {
'Message 5',
];
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<QueuedMessageDisplay messageQueue={messageQueue} />,
);
@@ -64,17 +69,19 @@ describe('QueuedMessageDisplay', () => {
expect(output).toContain('... (+2 more)');
expect(output).not.toContain('Message 4');
expect(output).not.toContain('Message 5');
unmount();
});
it('normalizes whitespace in messages', () => {
const messageQueue = ['Message with\tmultiple\n whitespace'];
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<QueuedMessageDisplay messageQueue={messageQueue} />,
);
const output = lastFrame();
expect(output).toContain('Queued (press ↑ to edit):');
expect(output).toContain('Message with multiple whitespace');
unmount();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';

View File

@@ -21,7 +21,8 @@
*
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SettingsDialog } from './SettingsDialog.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
@@ -321,7 +322,7 @@ describe('SettingsDialog', () => {
stdin.write(down as string);
});
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Disable Auto Update');
});
@@ -330,7 +331,7 @@ describe('SettingsDialog', () => {
stdin.write(up as string);
});
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -348,7 +349,7 @@ describe('SettingsDialog', () => {
stdin.write(TerminalKeys.UP_ARROW);
});
await vi.waitFor(() => {
await waitFor(() => {
// Should wrap to last setting (without relying on exact bullet character)
expect(lastFrame()).toContain('Codebase Investigator Max Num Turns');
});
@@ -367,7 +368,7 @@ describe('SettingsDialog', () => {
const { stdin, unmount, lastFrame } = renderDialog(settings, onSelect);
// Wait for initial render and verify we're on Vim Mode (first setting)
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -375,7 +376,7 @@ describe('SettingsDialog', () => {
act(() => {
stdin.write(TerminalKeys.DOWN_ARROW as string);
});
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Disable Auto Update');
});
@@ -384,14 +385,14 @@ describe('SettingsDialog', () => {
stdin.write(TerminalKeys.ENTER as string);
});
// Wait for the setting change to be processed
await vi.waitFor(() => {
await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
});
// Wait for the mock to be called
await vi.waitFor(() => {
await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
@@ -439,7 +440,7 @@ describe('SettingsDialog', () => {
stdin.write(TerminalKeys.ENTER as string);
});
await vi.waitFor(() => {
await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
@@ -513,7 +514,7 @@ describe('SettingsDialog', () => {
const { lastFrame, unmount } = renderDialog(settings, onSelect);
// Wait for initial render
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -569,7 +570,7 @@ describe('SettingsDialog', () => {
const { lastFrame, unmount } = renderDialog(settings, onSelect);
// Wait for initial render
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Hide Window Title');
});
@@ -735,7 +736,7 @@ describe('SettingsDialog', () => {
// Since we can't easily target specific settings, we test the general behavior
// Should not show restart prompt initially
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).not.toContain(
'To see changes, Gemini CLI must be restarted',
);
@@ -836,7 +837,7 @@ describe('SettingsDialog', () => {
});
}
await vi.waitFor(() => {
await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
@@ -928,7 +929,7 @@ describe('SettingsDialog', () => {
const { lastFrame, unmount } = renderDialog(settings, onSelect);
// Wait for initial render
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -978,7 +979,7 @@ describe('SettingsDialog', () => {
const { lastFrame, unmount } = renderDialog(settings, onSelect);
// Wait for initial render
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toContain('Vim Mode');
});
@@ -1096,7 +1097,7 @@ describe('SettingsDialog', () => {
stdin.write('\u001B');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
});
@@ -1198,8 +1199,11 @@ describe('SettingsDialog', () => {
userSettings: {},
systemSettings: {},
workspaceSettings: {},
stdinActions: (stdin: { write: (data: string) => void }) =>
stdin.write('\t'),
stdinActions: (stdin: { write: (data: string) => void }) => {
act(() => {
stdin.write('\t');
});
},
},
{
name: 'accessibility settings enabled',

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';

View File

@@ -4,7 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ThemeDialog } from './ThemeDialog.js';
import { LoadedSettings } from '../../config/settings.js';
@@ -126,7 +127,7 @@ describe('ThemeDialog Snapshots', () => {
stdin.write('\x1b');
});
await vi.waitFor(() => {
await waitFor(() => {
expect(mockOnCancel).toHaveBeenCalled();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';

View File

@@ -178,10 +178,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
> Settings │
Settings │
│ │
│ ▲ │
Vim Mode false │
Vim Mode false │
│ │
│ Disable Auto Update false │
│ │
@@ -200,10 +200,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
│ ▼ │
│ │
│ │
Apply To │
│ ● User Settings
│ Workspace Settings
│ System Settings
> Apply To │
│ ● 1. User Settings │
2. Workspace Settings │
3. System Settings │
│ │
│ (Use Enter to select, Tab to change focus, Esc to close) │
│ │

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import type { CompressionDisplayProps } from './CompressionMessage.js';
import { CompressionMessage } from './CompressionMessage.js';
import { CompressionStatus } from '@google/gemini-cli-core';
@@ -27,10 +27,11 @@ describe('<CompressionMessage />', () => {
describe('pending state', () => {
it('renders pending message when compression is in progress', () => {
const props = createCompressionProps({ isPending: true });
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('Compressing chat history');
unmount();
});
});
@@ -42,13 +43,14 @@ describe('<CompressionMessage />', () => {
newTokenCount: 50,
compressionStatus: CompressionStatus.COMPRESSED,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('✦');
expect(output).toContain(
'Chat history compressed from 100 to 50 tokens.',
);
unmount();
});
it('renders success message for large successful compressions', () => {
@@ -57,14 +59,16 @@ describe('<CompressionMessage />', () => {
{ original: 700000, new: 350000 }, // Very large compression
];
testCases.forEach(({ original, new: newTokens }) => {
for (const { original, new: newTokens } of testCases) {
const props = createCompressionProps({
isPending: false,
originalTokenCount: original,
newTokenCount: newTokens,
compressionStatus: CompressionStatus.COMPRESSED,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(
<CompressionMessage {...props} />,
);
const output = lastFrame();
expect(output).toContain('✦');
@@ -73,7 +77,8 @@ describe('<CompressionMessage />', () => {
);
expect(output).not.toContain('Skipping compression');
expect(output).not.toContain('did not reduce size');
});
unmount();
}
});
});
@@ -86,13 +91,14 @@ describe('<CompressionMessage />', () => {
compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain('✦');
expect(output).toContain(
'Compression was not beneficial for this history size.',
);
unmount();
});
it('renders skip message when token counts are equal', () => {
@@ -103,12 +109,13 @@ describe('<CompressionMessage />', () => {
compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(<CompressionMessage {...props} />);
const output = lastFrame();
expect(output).toContain(
'Compression was not beneficial for this history size.',
);
unmount();
});
});
@@ -132,18 +139,21 @@ describe('<CompressionMessage />', () => {
},
];
testCases.forEach(({ original, new: newTokens, expected }) => {
for (const { original, new: newTokens, expected } of testCases) {
const props = createCompressionProps({
isPending: false,
originalTokenCount: original,
newTokenCount: newTokens,
compressionStatus: CompressionStatus.COMPRESSED,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(
<CompressionMessage {...props} />,
);
const output = lastFrame();
expect(output).toContain(expected);
});
unmount();
}
});
it('shows skip message for small histories when new tokens >= original tokens', () => {
@@ -153,7 +163,7 @@ describe('<CompressionMessage />', () => {
{ original: 49999, new: 50000 }, // Just under 50k threshold
];
testCases.forEach(({ original, new: newTokens }) => {
for (const { original, new: newTokens } of testCases) {
const props = createCompressionProps({
isPending: false,
originalTokenCount: original,
@@ -161,14 +171,17 @@ describe('<CompressionMessage />', () => {
compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(
<CompressionMessage {...props} />,
);
const output = lastFrame();
expect(output).toContain(
'Compression was not beneficial for this history size.',
);
expect(output).not.toContain('compressed from');
});
unmount();
}
});
it('shows compression failure message for large histories when new tokens >= original tokens', () => {
@@ -178,7 +191,7 @@ describe('<CompressionMessage />', () => {
{ original: 100000, new: 100000 }, // Large history, same count
];
testCases.forEach(({ original, new: newTokens }) => {
for (const { original, new: newTokens } of testCases) {
const props = createCompressionProps({
isPending: false,
originalTokenCount: original,
@@ -186,13 +199,16 @@ describe('<CompressionMessage />', () => {
compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
});
const { lastFrame } = render(<CompressionMessage {...props} />);
const { lastFrame, unmount } = render(
<CompressionMessage {...props} />,
);
const output = lastFrame();
expect(output).toContain('compression did not reduce size');
expect(output).not.toContain('compressed from');
expect(output).not.toContain('Compression was not beneficial');
});
unmount();
}
});
});
});

View File

@@ -5,7 +5,7 @@
*/
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
import { vi } from 'vitest';

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { describe, it, expect } from 'vitest';
import { Box } from 'ink';
import { TodoTray } from './Todo.js';

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
import type React from 'react';
@@ -98,10 +98,11 @@ describe('<ToolGroupMessage />', () => {
describe('Golden Snapshots', () => {
it('renders single successful tool call', () => {
const toolCalls = [createToolCall()];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders multiple tool calls with different statuses', () => {
@@ -125,10 +126,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Error,
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders tool call awaiting confirmation', () => {
@@ -146,10 +148,11 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders shell command with yellow border', () => {
@@ -161,10 +164,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders mixed tool calls including shell command', () => {
@@ -188,10 +192,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Pending,
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with limited terminal height', () => {
@@ -210,7 +215,7 @@ describe('<ToolGroupMessage />', () => {
resultDisplay: 'More output here',
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -218,11 +223,12 @@ describe('<ToolGroupMessage />', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders when not focused', () => {
const toolCalls = [createToolCall()];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -230,6 +236,7 @@ describe('<ToolGroupMessage />', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with narrow terminal width', () => {
@@ -240,7 +247,7 @@ describe('<ToolGroupMessage />', () => {
'This is a very long description that might cause wrapping issues',
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -248,24 +255,27 @@ describe('<ToolGroupMessage />', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders empty tool calls array', () => {
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={[]} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
describe('Border Color Logic', () => {
it('uses yellow border when tools are pending', () => {
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
// The snapshot will capture the visual appearance including border color
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('uses yellow border for shell commands even when successful', () => {
@@ -275,10 +285,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('uses gray border when all tools are successful and no shell commands', () => {
@@ -290,10 +301,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
@@ -313,7 +325,7 @@ describe('<ToolGroupMessage />', () => {
resultDisplay: '', // No result
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -321,6 +333,7 @@ describe('<ToolGroupMessage />', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
@@ -350,11 +363,12 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const { lastFrame } = renderWithProviders(
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
// Should only show confirmation for the first tool
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});

View File

@@ -6,6 +6,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import {
BaseSelectionList,
type BaseSelectionListProps,
@@ -298,7 +299,7 @@ describe('BaseSelectionList', () => {
rerender(<BaseSelectionList {...componentProps} />);
await vi.waitFor(() => {
await waitFor(() => {
expect(lastFrame()).toBeTruthy();
});
};
@@ -322,7 +323,7 @@ describe('BaseSelectionList', () => {
// New visible window should be Items 2, 3, 4 (scroll offset 1).
await updateActiveIndex(3);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).not.toContain('Item 1');
expect(output).toContain('Item 2');
@@ -336,7 +337,7 @@ describe('BaseSelectionList', () => {
await updateActiveIndex(4);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
expect(output).toContain('Item 5');
@@ -347,7 +348,7 @@ describe('BaseSelectionList', () => {
// This should trigger scroll up to show items 2, 3, 4
await updateActiveIndex(1);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
@@ -361,7 +362,7 @@ describe('BaseSelectionList', () => {
// Visible items: 8, 9, 10.
const { lastFrame } = renderScrollableList(9);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 10');
expect(output).toContain('Item 8');
@@ -380,14 +381,14 @@ describe('BaseSelectionList', () => {
expect(lastFrame()).toContain('Item 1');
await updateActiveIndex(3); // Should trigger scroll
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 1');
});
await updateActiveIndex(5); // Scroll further
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 4');
expect(output).toContain('Item 6');
@@ -414,7 +415,7 @@ describe('BaseSelectionList', () => {
it('should correctly identify the selected item when scrolled (high index)', async () => {
renderScrollableList(5);
await vi.waitFor(() => {
await waitFor(() => {
// Item 6 (index 5) should be selected
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ value: 'Item 6' }),
@@ -472,7 +473,7 @@ describe('BaseSelectionList', () => {
0,
);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
// At the top, should show first 3 items
expect(output).toContain('Item 1');
@@ -490,7 +491,7 @@ describe('BaseSelectionList', () => {
5,
);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
// After scrolling to middle, should see items around index 5
expect(output).toContain('Item 4');
@@ -509,7 +510,7 @@ describe('BaseSelectionList', () => {
9,
);
await vi.waitFor(() => {
await waitFor(() => {
const output = lastFrame();
// At the end, should show last 3 items
expect(output).toContain('Item 8');

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
import { Box, Text } from 'ink';
@@ -18,7 +18,7 @@ describe('<MaxSizedBox />', () => {
setMaxSizedBoxDebugging(true);
it('renders children without truncation when they fit', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}>
<Box>
@@ -28,10 +28,11 @@ describe('<MaxSizedBox />', () => {
</OverflowProvider>,
);
expect(lastFrame()).equals('Hello, World!');
unmount();
});
it('hides lines when content exceeds maxHeight', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2}>
<Box>
@@ -48,10 +49,11 @@ describe('<MaxSizedBox />', () => {
);
expect(lastFrame()).equals(`... first 2 lines hidden ...
Line 3`);
unmount();
});
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
<Box>
@@ -68,10 +70,11 @@ Line 3`);
);
expect(lastFrame()).equals(`Line 1
... last 2 lines hidden ...`);
unmount();
});
it('wraps text that exceeds maxWidth', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={10} maxHeight={5}>
<Box>
@@ -84,13 +87,14 @@ Line 3`);
expect(lastFrame()).equals(`This is a
long line
of text`);
unmount();
});
it('handles mixed wrapping and non-wrapping segments', () => {
const multilineText = `This part will wrap around.
And has a line break.
Leading spaces preserved.`;
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={20} maxHeight={20}>
<Box>
@@ -125,10 +129,11 @@ Longer No Wrap: This
arou
nd.`,
);
unmount();
});
it('handles words longer than maxWidth by splitting them', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={5} maxHeight={5}>
<Box>
@@ -143,10 +148,11 @@ istic
expia
lidoc
ious`);
unmount();
});
it('does not truncate when maxHeight is undefined', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
<Box>
@@ -160,10 +166,11 @@ ious`);
);
expect(lastFrame()).equals(`Line 1
Line 2`);
unmount();
});
it('shows plural "lines" when more than one line is hidden', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2}>
<Box>
@@ -180,10 +187,11 @@ Line 2`);
);
expect(lastFrame()).equals(`... first 2 lines hidden ...
Line 3`);
unmount();
});
it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
<Box>
@@ -200,10 +208,11 @@ Line 3`);
);
expect(lastFrame()).equals(`Line 1
... last 2 lines hidden ...`);
unmount();
});
it('renders an empty box for empty children', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
</OverflowProvider>,
@@ -211,10 +220,11 @@ Line 3`);
// Expect an empty string or a box with nothing in it.
// Ink renders an empty box as an empty string.
expect(lastFrame()).equals('');
unmount();
});
it('wraps text with multi-byte unicode characters correctly', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={5} maxHeight={5}>
<Box>
@@ -228,10 +238,11 @@ Line 3`);
// With maxWidth=5, it should wrap after the second character.
expect(lastFrame()).equals(`你好
世界`);
unmount();
});
it('wraps text with multi-byte emoji characters correctly', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={5} maxHeight={5}>
<Box>
@@ -246,10 +257,11 @@ Line 3`);
expect(lastFrame()).equals(`🐶🐶
🐶🐶
🐶`);
unmount();
});
it('falls back to an ellipsis when width is extremely small', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={2} maxHeight={2}>
<Box>
@@ -261,10 +273,11 @@ Line 3`);
);
expect(lastFrame()).equals('N…');
unmount();
});
it('truncates long non-wrapping text with ellipsis', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={3} maxHeight={2}>
<Box>
@@ -276,10 +289,11 @@ Line 3`);
);
expect(lastFrame()).equals('AB…');
unmount();
});
it('truncates non-wrapping text containing line breaks', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={3} maxHeight={2}>
<Box>
@@ -291,10 +305,11 @@ Line 3`);
);
expect(lastFrame()).equals(`A\n…`);
unmount();
});
it('truncates emoji characters correctly with ellipsis', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={3} maxHeight={2}>
<Box>
@@ -306,10 +321,11 @@ Line 3`);
);
expect(lastFrame()).equals(`🐶…`);
unmount();
});
it('shows ellipsis for multiple rows with long non-wrapping text', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={3} maxHeight={3}>
<Box>
@@ -329,10 +345,11 @@ Line 3`);
);
expect(lastFrame()).equals(`AA…\nBB…\nCC…`);
unmount();
});
it('accounts for additionalHiddenLinesCount', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
<Box>
@@ -350,10 +367,11 @@ Line 3`);
// 1 line is hidden by overflow, 5 are additionally hidden.
expect(lastFrame()).equals(`... first 7 lines hidden ...
Line 3`);
unmount();
});
it('handles React.Fragment as a child', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}>
<>
@@ -373,6 +391,7 @@ Line 3`);
expect(lastFrame()).equals(`Line 1 from Fragment
Line 2 from Fragment
Line 3 direct child`);
unmount();
});
it('clips a long single text child from the top', () => {
@@ -381,7 +400,7 @@ Line 3 direct child`);
(_, i) => `Line ${i + 1}`,
).join('\n');
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}>
<Box>
@@ -397,6 +416,7 @@ Line 3 direct child`);
].join('\n');
expect(lastFrame()).equals(expected);
unmount();
});
it('clips a long single text child from the bottom', () => {
@@ -405,7 +425,7 @@ Line 3 direct child`);
(_, i) => `Line ${i + 1}`,
).join('\n');
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
<Box>
@@ -421,5 +441,6 @@ Line 3 direct child`);
].join('\n');
expect(lastFrame()).equals(expected);
unmount();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { TextInput } from './TextInput.js';
import { useKeypress } from '../../hooks/useKeypress.js';

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { describe, it, expect } from 'vitest';
import { ChatList } from './ChatList.js';
import type { ChatDetail } from '../../types.js';
@@ -22,14 +22,16 @@ const mockChats: ChatDetail[] = [
describe('<ChatList />', () => {
it('renders correctly with a list of chats', () => {
const { lastFrame } = render(<ChatList chats={mockChats} />);
const { lastFrame, unmount } = render(<ChatList chats={mockChats} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with no chats', () => {
const { lastFrame } = render(<ChatList chats={[]} />);
const { lastFrame, unmount } = render(<ChatList chats={[]} />);
expect(lastFrame()).toContain('No saved conversation checkpoints found.');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('handles invalid date formats gracefully', () => {
@@ -39,8 +41,11 @@ describe('<ChatList />', () => {
mtime: 'an-invalid-date-string',
},
];
const { lastFrame } = render(<ChatList chats={mockChatsWithInvalidDate} />);
const { lastFrame, unmount } = render(
<ChatList chats={mockChatsWithInvalidDate} />,
);
expect(lastFrame()).toContain('(Invalid Date)');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { vi, describe, beforeEach, it, expect } from 'vitest';
import { useUIState } from '../../contexts/UIStateContext.js';
import { ExtensionUpdateState } from '../../state/extensions.js';
@@ -57,27 +57,30 @@ describe('<ExtensionsList />', () => {
it('should render "No extensions installed." if there are no extensions', () => {
mockUIState(new Map());
const { lastFrame } = render(<ExtensionsList extensions={[]} />);
const { lastFrame, unmount } = render(<ExtensionsList extensions={[]} />);
expect(lastFrame()).toContain('No extensions installed.');
unmount();
});
it('should render a list of extensions with their version and status', () => {
mockUIState(new Map());
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<ExtensionsList extensions={mockExtensions} />,
);
const output = lastFrame();
expect(output).toContain('ext-one (v1.0.0) - active');
expect(output).toContain('ext-two (v2.1.0) - active');
expect(output).toContain('ext-disabled (v3.0.0) - disabled');
unmount();
});
it('should display "unknown state" if an extension has no update state', () => {
mockUIState(new Map());
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<ExtensionsList extensions={[mockExtensions[0]]} />,
);
expect(lastFrame()).toContain('(unknown state)');
unmount();
});
const stateTestCases = [
@@ -115,10 +118,11 @@ describe('<ExtensionsList />', () => {
it(`should correctly display the state: ${state}`, () => {
const updateState = new Map([[mockExtensions[0].name, state]]);
mockUIState(updateState);
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<ExtensionsList extensions={[mockExtensions[0]]} />,
);
expect(lastFrame()).toContain(expectedText);
unmount();
});
}
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { render } from '../../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { McpStatus } from './McpStatus.js';
import { MCPServerStatus } from '@google/gemini-cli-core';
@@ -46,32 +46,36 @@ describe('McpStatus', () => {
};
it('renders correctly with a connected server', () => {
const { lastFrame } = render(<McpStatus {...baseProps} />);
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with authenticated OAuth status', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus {...baseProps} authStatus={{ 'server-1': 'authenticated' }} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with expired OAuth status', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus {...baseProps} authStatus={{ 'server-1': 'expired' }} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with unauthenticated OAuth status', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus
{...baseProps}
authStatus={{ 'server-1': 'unauthenticated' }}
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with a disconnected server', async () => {
@@ -79,26 +83,29 @@ describe('McpStatus', () => {
await import('@google/gemini-cli-core'),
'getMCPServerStatus',
).mockReturnValue(MCPServerStatus.DISCONNECTED);
const { lastFrame } = render(<McpStatus {...baseProps} />);
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly when discovery is in progress', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus {...baseProps} discoveryInProgress={true} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with schema enabled', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus {...baseProps} showSchema={true} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with parametersJsonSchema', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus
{...baseProps}
tools={[
@@ -120,10 +127,11 @@ describe('McpStatus', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with prompts', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus
{...baseProps}
prompts={[
@@ -136,22 +144,25 @@ describe('McpStatus', () => {
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with a blocked server', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus
{...baseProps}
blockedServers={[{ name: 'server-1', extensionName: 'test-extension' }]}
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with a connecting server', () => {
const { lastFrame } = render(
const { lastFrame, unmount } = render(
<McpStatus {...baseProps} connectingServers={['server-1']} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});