mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 00:51:25 -07:00
Fix tests to wrap all calls changing the UI with act. (#12268)
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) │
|
||||
│ │
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user