mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-11 02:20:48 -07:00
Migrate core render util to use xterm.js as part of the rendering loop. (#19044)
This commit is contained in:
@@ -25,11 +25,12 @@ describe('<CompressionMessage />', () => {
|
||||
});
|
||||
|
||||
describe('pending state', () => {
|
||||
it('renders pending message when compression is in progress', () => {
|
||||
it('renders pending message when compression is in progress', async () => {
|
||||
const props = createCompressionProps({ isPending: true });
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Compressing chat history');
|
||||
@@ -38,16 +39,17 @@ describe('<CompressionMessage />', () => {
|
||||
});
|
||||
|
||||
describe('normal compression (successful token reduction)', () => {
|
||||
it('renders success message when tokens are reduced', () => {
|
||||
it('renders success message when tokens are reduced', async () => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: 100,
|
||||
newTokenCount: 50,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
@@ -57,22 +59,22 @@ describe('<CompressionMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders success message for large successful compressions', () => {
|
||||
const testCases = [
|
||||
{ original: 50000, new: 25000 }, // Large compression
|
||||
{ original: 700000, new: 350000 }, // Very large compression
|
||||
];
|
||||
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
it.each([
|
||||
{ original: 50000, newTokens: 25000 }, // Large compression
|
||||
{ original: 700000, newTokens: 350000 }, // Very large compression
|
||||
])(
|
||||
'renders success message for large successful compression (from $original to $newTokens)',
|
||||
async ({ original, newTokens }) => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
newTokenCount: newTokens,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
@@ -82,12 +84,12 @@ describe('<CompressionMessage />', () => {
|
||||
expect(output).not.toContain('Skipping compression');
|
||||
expect(output).not.toContain('did not reduce size');
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('skipped compression (tokens increased or same)', () => {
|
||||
it('renders skip message when compression would increase token count', () => {
|
||||
it('renders skip message when compression would increase token count', async () => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: 50,
|
||||
@@ -95,9 +97,10 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
@@ -107,7 +110,7 @@ describe('<CompressionMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders skip message when token counts are equal', () => {
|
||||
it('renders skip message when token counts are equal', async () => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: 50,
|
||||
@@ -115,9 +118,10 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(
|
||||
@@ -128,50 +132,49 @@ describe('<CompressionMessage />', () => {
|
||||
});
|
||||
|
||||
describe('message content validation', () => {
|
||||
it('displays correct compression statistics', () => {
|
||||
const testCases = [
|
||||
{
|
||||
original: 200,
|
||||
new: 80,
|
||||
expected: 'compressed from 200 to 80 tokens',
|
||||
},
|
||||
{
|
||||
original: 500,
|
||||
new: 150,
|
||||
expected: 'compressed from 500 to 150 tokens',
|
||||
},
|
||||
{
|
||||
original: 1500,
|
||||
new: 400,
|
||||
expected: 'compressed from 1500 to 400 tokens',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { original, new: newTokens, expected } of testCases) {
|
||||
it.each([
|
||||
{
|
||||
original: 200,
|
||||
newTokens: 80,
|
||||
expected: 'compressed from 200 to 80 tokens',
|
||||
},
|
||||
{
|
||||
original: 500,
|
||||
newTokens: 150,
|
||||
expected: 'compressed from 500 to 150 tokens',
|
||||
},
|
||||
{
|
||||
original: 1500,
|
||||
newTokens: 400,
|
||||
expected: 'compressed from 1500 to 400 tokens',
|
||||
},
|
||||
])(
|
||||
'displays correct compression statistics (from $original to $newTokens)',
|
||||
async ({ original, newTokens, expected }) => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
newTokenCount: newTokens,
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(expected);
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('shows skip message for small histories when new tokens >= original tokens', () => {
|
||||
const testCases = [
|
||||
{ original: 50, new: 60 }, // Increased
|
||||
{ original: 100, new: 100 }, // Same
|
||||
{ original: 49999, new: 50000 }, // Just under 50k threshold
|
||||
];
|
||||
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
it.each([
|
||||
{ original: 50, newTokens: 60 }, // Increased
|
||||
{ original: 100, newTokens: 100 }, // Same
|
||||
{ original: 49999, newTokens: 50000 }, // Just under 50k threshold
|
||||
])(
|
||||
'shows skip message for small histories when new tokens >= original tokens ($original -> $newTokens)',
|
||||
async ({ original, newTokens }) => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
@@ -179,9 +182,10 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(
|
||||
@@ -189,17 +193,16 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
expect(output).not.toContain('compressed from');
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('shows compression failure message for large histories when new tokens >= original tokens', () => {
|
||||
const testCases = [
|
||||
{ original: 50000, new: 50100 }, // At 50k threshold
|
||||
{ original: 700000, new: 710000 }, // Large history case
|
||||
{ original: 100000, new: 100000 }, // Large history, same count
|
||||
];
|
||||
|
||||
for (const { original, new: newTokens } of testCases) {
|
||||
it.each([
|
||||
{ original: 50000, newTokens: 50100 }, // At 50k threshold
|
||||
{ original: 700000, newTokens: 710000 }, // Large history case
|
||||
{ original: 100000, newTokens: 100000 }, // Large history, same count
|
||||
])(
|
||||
'shows compression failure message for large histories when new tokens >= original tokens ($original -> $newTokens)',
|
||||
async ({ original, newTokens }) => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
originalTokenCount: original,
|
||||
@@ -207,28 +210,30 @@ describe('<CompressionMessage />', () => {
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('failure states', () => {
|
||||
it('renders failure message when model returns an empty summary', () => {
|
||||
it('renders failure message when model returns an empty summary', async () => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
compressionStatus: CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
@@ -238,15 +243,16 @@ describe('<CompressionMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders failure message for token count errors', () => {
|
||||
it('renders failure message for token count errors', async () => {
|
||||
const props = createCompressionProps({
|
||||
isPending: false,
|
||||
compressionStatus:
|
||||
CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
|
||||
});
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CompressionMessage {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(
|
||||
|
||||
@@ -9,18 +9,26 @@ import { ErrorMessage } from './ErrorMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ErrorMessage', () => {
|
||||
it('renders with the correct prefix and text', () => {
|
||||
const { lastFrame } = render(<ErrorMessage text="Something went wrong" />);
|
||||
it('renders with the correct prefix and text', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ErrorMessage text="Something went wrong" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiline error messages', () => {
|
||||
it('renders multiline error messages', async () => {
|
||||
const message = 'Error line 1\nError line 2';
|
||||
const { lastFrame } = render(<ErrorMessage text={message} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ErrorMessage text={message} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,35 +23,39 @@ describe('<GeminiMessage /> - Raw Markdown Display Snapshots', () => {
|
||||
},
|
||||
])(
|
||||
'renders with renderMarkdown=$renderMarkdown $description',
|
||||
({ renderMarkdown }) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
async ({ renderMarkdown }) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<GeminiMessage {...baseProps} />,
|
||||
{
|
||||
uiState: { renderMarkdown, streamingState: StreamingState.Idle },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([{ renderMarkdown: true }, { renderMarkdown: false }])(
|
||||
'renders pending state with renderMarkdown=$renderMarkdown',
|
||||
({ renderMarkdown }) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
async ({ renderMarkdown }) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<GeminiMessage {...baseProps} isPending={true} />,
|
||||
{
|
||||
uiState: { renderMarkdown, streamingState: StreamingState.Idle },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it('wraps long lines correctly in raw markdown mode', () => {
|
||||
it('wraps long lines correctly in raw markdown mode', async () => {
|
||||
const terminalWidth = 20;
|
||||
const text =
|
||||
'This is a long line that should wrap correctly without truncation';
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<GeminiMessage
|
||||
text={text}
|
||||
isPending={false}
|
||||
@@ -61,6 +65,8 @@ describe('<GeminiMessage /> - Raw Markdown Display Snapshots', () => {
|
||||
uiState: { renderMarkdown: false, streamingState: StreamingState.Idle },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,27 +9,37 @@ import { InfoMessage } from './InfoMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('InfoMessage', () => {
|
||||
it('renders with the correct default prefix and text', () => {
|
||||
const { lastFrame } = render(<InfoMessage text="Just so you know" />);
|
||||
it('renders with the correct default prefix and text', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<InfoMessage text="Just so you know" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with a custom icon', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders with a custom icon', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<InfoMessage text="Custom icon test" icon="★" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiline info messages', () => {
|
||||
it('renders multiline info messages', async () => {
|
||||
const message = 'Info line 1\nInfo line 2';
|
||||
const { lastFrame } = render(<InfoMessage text={message} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<InfoMessage text={message} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('ToolConfirmationMessage Redirection', () => {
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should display redirection warning and tip for redirected commands', () => {
|
||||
it('should display redirection warning and tip for redirected commands', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Shell Command',
|
||||
@@ -32,7 +32,7 @@ describe('ToolConfirmationMessage Redirection', () => {
|
||||
rootCommands: ['echo'],
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={confirmationDetails}
|
||||
@@ -41,8 +41,10 @@ describe('ToolConfirmationMessage Redirection', () => {
|
||||
terminalWidth={100}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('<ShellToolMessage />', () => {
|
||||
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
|
||||
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
|
||||
])('clicks inside the shell area sets focus for %s', async (_, name) => {
|
||||
const { stdin, lastFrame, simulateClick } = renderShell(
|
||||
const { lastFrame, simulateClick } = renderShell(
|
||||
{ name },
|
||||
{ mouseEventsEnabled: true },
|
||||
);
|
||||
@@ -74,7 +74,7 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(lastFrame()).toContain('A shell command');
|
||||
});
|
||||
|
||||
await simulateClick(stdin, 2, 2);
|
||||
await simulateClick(2, 2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||
@@ -164,10 +164,9 @@ describe('<ShellToolMessage />', () => {
|
||||
},
|
||||
],
|
||||
])('%s', async (_, props, options) => {
|
||||
const { lastFrame } = renderShell(props, options);
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
const { lastFrame, waitUntilReady } = renderShell(props, options);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,7 +197,7 @@ describe('<ShellToolMessage />', () => {
|
||||
false,
|
||||
],
|
||||
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
|
||||
const { lastFrame } = renderShell(
|
||||
const { lastFrame, waitUntilReady } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
@@ -215,11 +214,10 @@ describe('<ShellToolMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame!.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame).toMatchSnapshot();
|
||||
});
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,28 +9,32 @@ import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { ThinkingMessage } from './ThinkingMessage.js';
|
||||
|
||||
describe('ThinkingMessage', () => {
|
||||
it('renders subject line', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders subject line', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{ subject: 'Planning', description: 'test' }}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses description when subject is empty', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('uses description when subject is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{ subject: '', description: 'Processing details' }}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders full mode with left border and full text', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders full mode with left border and full text', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Planning',
|
||||
@@ -38,12 +42,14 @@ describe('ThinkingMessage', () => {
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('indents summary line correctly', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('indents summary line correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Summary line',
|
||||
@@ -51,12 +57,14 @@ describe('ThinkingMessage', () => {
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('normalizes escaped newline tokens', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('normalizes escaped newline tokens', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Matching the Blocks',
|
||||
@@ -64,15 +72,19 @@ describe('ThinkingMessage', () => {
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders empty state gracefully', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders empty state gracefully', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage thought={{ subject: '', description: '' }} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,29 +32,37 @@ const createTodoHistoryItem = (todos: Todo[]): HistoryItem =>
|
||||
|
||||
describe.each([true, false])(
|
||||
'<TodoTray /> (showFullTodos: %s)',
|
||||
(showFullTodos: boolean) => {
|
||||
const renderWithUiState = (uiState: Partial<UIState>) =>
|
||||
render(
|
||||
async (showFullTodos: boolean) => {
|
||||
const renderWithUiState = async (uiState: Partial<UIState>) => {
|
||||
const result = render(
|
||||
<UIStateContext.Provider value={uiState as UIState}>
|
||||
<TodoTray />
|
||||
</UIStateContext.Provider>,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
it('renders null when no todos are in the history', () => {
|
||||
const { lastFrame } = renderWithUiState({ history: [], showFullTodos });
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
it('renders null when no todos are in the history', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders null when todo list is empty', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
it('renders null when todo list is empty', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [createTodoHistoryItem([])],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders when todos exist but none are in progress', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
it('renders when todos exist but none are in progress', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
@@ -65,10 +73,11 @@ describe.each([true, false])(
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders when todos exist and one is in progress', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
it('renders when todos exist and one is in progress', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
@@ -80,10 +89,11 @@ describe.each([true, false])(
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders a todo list with long descriptions that wrap when full view is on', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders a todo list with long descriptions that wrap when full view is on', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<Box width="50">
|
||||
<UIStateContext.Provider
|
||||
value={
|
||||
@@ -110,11 +120,13 @@ describe.each([true, false])(
|
||||
</UIStateContext.Provider>
|
||||
</Box>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders the most recent todo list when multiple write_todos calls are in history', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
it('renders the most recent todo list when multiple write_todos calls are in history', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Older Task 1', status: 'completed' },
|
||||
@@ -128,10 +140,11 @@ describe.each([true, false])(
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders full list when all todos are inactive', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
it('renders full list when all todos are inactive', async () => {
|
||||
const { lastFrame, unmount } = await renderWithUiState({
|
||||
history: [
|
||||
createTodoHistoryItem([
|
||||
{ description: 'Task 1', status: 'completed' },
|
||||
@@ -140,7 +153,8 @@ describe.each([true, false])(
|
||||
],
|
||||
showFullTodos,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should not display urls if prompt and url are the same', () => {
|
||||
it('should not display urls if prompt and url are the same', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: 'Confirm Web Fetch',
|
||||
@@ -46,7 +46,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
urls: ['https://example.com'],
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={confirmationDetails}
|
||||
@@ -55,11 +55,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display urls if prompt and url are different', () => {
|
||||
it('should display urls if prompt and url are different', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: 'Confirm Web Fetch',
|
||||
@@ -70,7 +72,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={confirmationDetails}
|
||||
@@ -79,11 +81,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display multiple commands for exec type when provided', () => {
|
||||
it('should display multiple commands for exec type when provided', async () => {
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'exec',
|
||||
title: 'Confirm Multiple Commands',
|
||||
@@ -93,7 +97,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={confirmationDetails}
|
||||
@@ -102,12 +106,14 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('echo "hello"');
|
||||
expect(output).toContain('ls -la');
|
||||
expect(output).toContain('whoami');
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('with folder trust', () => {
|
||||
@@ -166,13 +172,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
alwaysAllowText: 'always allow',
|
||||
},
|
||||
])('$description', ({ details }) => {
|
||||
it('should show "allow always" when folder is trusted', () => {
|
||||
it('should show "allow always" when folder is trusted', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={details}
|
||||
@@ -181,17 +187,19 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT show "allow always" when folder is untrusted', () => {
|
||||
it('should NOT show "allow always" when folder is untrusted', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => false,
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={details}
|
||||
@@ -200,8 +208,10 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -217,13 +227,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
newContent: 'b',
|
||||
};
|
||||
|
||||
it('should NOT show "Allow for all future sessions" when setting is false (default)', () => {
|
||||
it('should NOT show "Allow for all future sessions" when setting is false (default)', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
@@ -237,17 +247,19 @@ describe('ToolConfirmationMessage', () => {
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Allow for all future sessions');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show "Allow for all future sessions" when setting is true', () => {
|
||||
it('should show "Allow for all future sessions" when setting is true', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
@@ -261,8 +273,10 @@ describe('ToolConfirmationMessage', () => {
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Allow for all future sessions');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -277,7 +291,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
newContent: 'b',
|
||||
};
|
||||
|
||||
it('should show "Modify with external editor" when NOT in IDE mode', () => {
|
||||
it('should show "Modify with external editor" when NOT in IDE mode', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
@@ -289,7 +303,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isDiffingEnabled: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
@@ -298,11 +312,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Modify with external editor');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', () => {
|
||||
it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
@@ -314,7 +330,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isDiffingEnabled: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
@@ -323,11 +339,13 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Modify with external editor');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', () => {
|
||||
it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', async () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
@@ -339,7 +357,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
isDiffingEnabled: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
@@ -348,8 +366,10 @@ describe('ToolConfirmationMessage', () => {
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Modify with external editor');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,10 +65,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
});
|
||||
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
it('renders single successful tool call', async () => {
|
||||
const toolCalls = [createToolCall()];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -82,11 +82,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides confirming tools (standard behavior)', () => {
|
||||
it('hides confirming tools (standard behavior)', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'confirm-tool',
|
||||
@@ -100,17 +101,18 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
// Should render nothing because all tools in the group are confirming
|
||||
expect(lastFrame()).toBe('');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiple tool calls with different statuses (only visible ones)', () => {
|
||||
it('renders multiple tool calls with different statuses (only visible ones)', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-1',
|
||||
@@ -133,7 +135,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -148,6 +150,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
// pending-tool should be hidden
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('successful-tool');
|
||||
expect(output).not.toContain('pending-tool');
|
||||
@@ -156,7 +159,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders mixed tool calls including shell command', () => {
|
||||
it('renders mixed tool calls including shell command', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-1',
|
||||
@@ -179,7 +182,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -194,6 +197,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
// write_file (Pending) should be hidden
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('read_file');
|
||||
expect(output).toContain('run_shell_command');
|
||||
@@ -202,7 +206,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with limited terminal height', () => {
|
||||
it('renders with limited terminal height', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-1',
|
||||
@@ -219,7 +223,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
@@ -238,11 +242,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with narrow terminal width', () => {
|
||||
it('renders with narrow terminal width', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
name: 'very-long-tool-name-that-might-wrap',
|
||||
@@ -251,7 +256,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
@@ -270,14 +275,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders empty tool calls array', () => {
|
||||
it('renders empty tool calls array', async () => {
|
||||
const toolCalls: IndividualToolCallDisplay[] = [];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -291,11 +297,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders header when scrolled', () => {
|
||||
it('renders header when scrolled', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: '1',
|
||||
@@ -312,7 +319,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<Scrollable height={10} hasFocus={true} scrollToBottom={true}>
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />
|
||||
</Scrollable>,
|
||||
@@ -328,11 +335,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders tool call with outputFile', () => {
|
||||
it('renders tool call with outputFile', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-output-file',
|
||||
@@ -343,7 +351,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -357,11 +365,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders two tool groups where only the last line of the previous group is visible', () => {
|
||||
it('renders two tool groups where only the last line of the previous group is visible', async () => {
|
||||
const toolCalls1 = [
|
||||
createToolCall({
|
||||
callId: '1',
|
||||
@@ -381,7 +390,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item2 = createItem(toolCalls2);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<Scrollable height={6} hasFocus={true} scrollToBottom={true}>
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
@@ -410,13 +419,14 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border for shell commands even when successful', () => {
|
||||
it('uses yellow border for shell commands even when successful', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
name: 'run_shell_command',
|
||||
@@ -424,7 +434,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -438,11 +448,12 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses gray border when all tools are successful and no shell commands', () => {
|
||||
it('uses gray border when all tools are successful and no shell commands', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({ status: CoreToolCallStatus.Success }),
|
||||
createToolCall({
|
||||
@@ -452,7 +463,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
@@ -466,13 +477,14 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Height Calculation', () => {
|
||||
it('calculates available height correctly with multiple tools with results', () => {
|
||||
it('calculates available height correctly with multiple tools with results', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-1',
|
||||
@@ -488,7 +500,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
@@ -507,7 +519,8 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -542,7 +555,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
])(
|
||||
'filtering logic for status=$status and hasResult=$resultDisplay',
|
||||
({ status, resultDisplay, shouldHide }) => {
|
||||
async ({ status, resultDisplay, shouldHide }) => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: `ask-user-${status}`,
|
||||
@@ -553,13 +566,14 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
if (shouldHide) {
|
||||
expect(lastFrame()).toBe('');
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
} else {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
}
|
||||
@@ -567,7 +581,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('shows other tools when ask_user is filtered out', () => {
|
||||
it('shows other tools when ask_user is filtered out', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'other-tool',
|
||||
@@ -582,16 +596,17 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders nothing when only tool is in-progress AskUser with borderBottom=false', () => {
|
||||
it('renders nothing when only tool is in-progress AskUser with borderBottom=false', async () => {
|
||||
// AskUser tools in progress are rendered by AskUserDialog, not ToolGroupMessage.
|
||||
// When AskUser is the only tool and borderBottom=false (no border to close),
|
||||
// the component should render nothing.
|
||||
@@ -604,7 +619,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
@@ -614,7 +629,8 @@ describe('<ToolGroupMessage />', () => {
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
|
||||
expect(lastFrame()).toBe('');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -634,27 +650,32 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
{ name: READ_FILE_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: true },
|
||||
{ name: GLOB_DISPLAY_NAME, mode: ApprovalMode.PLAN, visible: true },
|
||||
])('filtering logic for $name in $mode mode', ({ name, mode, visible }) => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'test-call',
|
||||
name,
|
||||
approvalMode: mode,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
])(
|
||||
'filtering logic for $name in $mode mode',
|
||||
async ({ name, mode, visible }) => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'test-call',
|
||||
name,
|
||||
approvalMode: mode,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
if (visible) {
|
||||
expect(lastFrame()).toContain(name);
|
||||
} else {
|
||||
expect(lastFrame()).toBe('');
|
||||
}
|
||||
unmount();
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
if (visible) {
|
||||
expect(lastFrame()).toContain(name);
|
||||
} else {
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
}
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,25 +59,33 @@ describe('<ToolMessage />', () => {
|
||||
renderWithProviders(ui, {
|
||||
uiActions,
|
||||
uiState: { streamingState },
|
||||
width: 80,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it('renders basic tool information', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders basic tool information', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('JSON rendering', () => {
|
||||
it('pretty prints valid JSON', () => {
|
||||
it('pretty prints valid JSON', async () => {
|
||||
const testJSONstring = '{"a": 1, "b": [2, 3]}';
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={testJSONstring}
|
||||
@@ -85,6 +93,7 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -94,22 +103,25 @@ describe('<ToolMessage />', () => {
|
||||
expect(output).toContain('"a": 1');
|
||||
expect(output).toContain('"b": [');
|
||||
// Should not use markdown renderer for JSON
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders pretty JSON in ink frame', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('renders pretty JSON in ink frame', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} resultDisplay='{"a":1,"b":2}' />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses JSON renderer even when renderOutputAsMarkdown=true is true', () => {
|
||||
it('uses JSON renderer even when renderOutputAsMarkdown=true is true', async () => {
|
||||
const testJSONstring = '{"a": 1, "b": [2, 3]}';
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={testJSONstring}
|
||||
@@ -117,6 +129,7 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -126,10 +139,11 @@ describe('<ToolMessage />', () => {
|
||||
expect(output).toContain('"a": 1');
|
||||
expect(output).toContain('"b": [');
|
||||
// Should not use markdown renderer for JSON even when renderOutputAsMarkdown=true
|
||||
unmount();
|
||||
});
|
||||
it('falls back to plain text for malformed JSON', () => {
|
||||
it('falls back to plain text for malformed JSON', async () => {
|
||||
const testJSONstring = 'a": 1, "b": [2, 3]}';
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={testJSONstring}
|
||||
@@ -137,16 +151,18 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
expect(tryParseJSON(testJSONstring)).toBeFalsy();
|
||||
expect(typeof output === 'string').toBeTruthy();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('rejects mixed text + JSON renders as plain text', () => {
|
||||
it('rejects mixed text + JSON renders as plain text', async () => {
|
||||
const testJSONstring = `{"result": "count": 42,"items": ["apple", "banana"]},"meta": {"timestamp": "2025-09-28T12:34:56Z"}}End.`;
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={testJSONstring}
|
||||
@@ -154,17 +170,19 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
expect(tryParseJSON(testJSONstring)).toBeFalsy();
|
||||
expect(typeof output === 'string').toBeTruthy();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('rejects ANSI-tained JSON renders as plain text', () => {
|
||||
it('rejects ANSI-tained JSON renders as plain text', async () => {
|
||||
const testJSONstring =
|
||||
'\u001b[32mOK\u001b[0m {"status": "success", "data": {"id": 123, "values": [10, 20, 30]}}';
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={testJSONstring}
|
||||
@@ -172,16 +190,18 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
expect(tryParseJSON(testJSONstring)).toBeFalsy();
|
||||
expect(typeof output === 'string').toBeTruthy();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('pretty printing 10kb JSON completes in <50ms', () => {
|
||||
it('pretty printing 10kb JSON completes in <50ms', async () => {
|
||||
const large = '{"key": "' + 'x'.repeat(10000) + '"}';
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
resultDisplay={large}
|
||||
@@ -189,83 +209,101 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const start = performance.now();
|
||||
lastFrame();
|
||||
expect(performance.now() - start).toBeLessThan(50);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToolStatusIndicator rendering', () => {
|
||||
it('shows ✓ for Success status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows ✓ for Success status', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Success} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows o for Pending status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows o for Pending status', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Scheduled} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows ? for Confirming status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows ? for Confirming status', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
status={CoreToolCallStatus.AwaitingApproval}
|
||||
/>,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows - for Canceled status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows - for Canceled status', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Cancelled} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows x for Error status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows x for Error status', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Error} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows paused spinner for Executing status when streamingState is Idle', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows paused spinner for Executing status when streamingState is Idle', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Executing} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Executing} />,
|
||||
StreamingState.WaitingForConfirmation,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('shows MockRespondingSpinner for Executing status when streamingState is Responding', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} status={CoreToolCallStatus.Executing} />,
|
||||
StreamingState.Responding, // Simulate app still responding
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders DiffRenderer for diff results', () => {
|
||||
it('renders DiffRenderer for diff results', async () => {
|
||||
const diffResult = {
|
||||
fileDiff: '--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new',
|
||||
fileName: 'file.txt',
|
||||
@@ -273,33 +311,47 @@ describe('<ToolMessage />', () => {
|
||||
newContent: 'new',
|
||||
filePath: 'file.txt',
|
||||
};
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} resultDisplay={diffResult} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// Check that the output contains the MockDiff content as part of the whole message
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders emphasis correctly', () => {
|
||||
const { lastFrame: highEmphasisFrame } = renderWithContext(
|
||||
it('renders emphasis correctly', async () => {
|
||||
const {
|
||||
lastFrame: highEmphasisFrame,
|
||||
waitUntilReady: waitUntilReadyHigh,
|
||||
unmount: unmountHigh,
|
||||
} = renderWithContext(
|
||||
<ToolMessage {...baseProps} emphasis="high" />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReadyHigh();
|
||||
// Check for trailing indicator or specific color if applicable (Colors are not easily testable here)
|
||||
expect(highEmphasisFrame()).toMatchSnapshot();
|
||||
unmountHigh();
|
||||
|
||||
const { lastFrame: lowEmphasisFrame } = renderWithContext(
|
||||
const {
|
||||
lastFrame: lowEmphasisFrame,
|
||||
waitUntilReady: waitUntilReadyLow,
|
||||
unmount: unmountLow,
|
||||
} = renderWithContext(
|
||||
<ToolMessage {...baseProps} emphasis="low" />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReadyLow();
|
||||
// For low emphasis, the name and description might be dimmed (check for dimColor if possible)
|
||||
// This is harder to assert directly in text output without color checks.
|
||||
// We can at least ensure it doesn't have the high emphasis indicator.
|
||||
expect(lowEmphasisFrame()).toMatchSnapshot();
|
||||
unmountLow();
|
||||
});
|
||||
|
||||
it('renders AnsiOutputText for AnsiOutput results', () => {
|
||||
it('renders AnsiOutputText for AnsiOutput results', async () => {
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
@@ -314,15 +366,17 @@ describe('<ToolMessage />', () => {
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = renderWithContext(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage {...baseProps} resultDisplay={ansiResult} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders progress information appended to description for executing tools', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('renders progress information appended to description for executing tools', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
status={CoreToolCallStatus.Executing}
|
||||
@@ -331,13 +385,15 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'A tool for testing (Working on it... - 42%)',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders only percentage when progressMessage is missing', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
it('renders only percentage when progressMessage is missing', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithContext(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
status={CoreToolCallStatus.Executing}
|
||||
@@ -345,6 +401,8 @@ describe('<ToolMessage />', () => {
|
||||
/>,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('A tool for testing (75%)');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,46 +66,52 @@ describe('Focus Hint', () => {
|
||||
|
||||
describe.each(testCases)('$componentName', ({ Component }) => {
|
||||
it('shows focus hint after delay even with NO output', async () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Component {...baseProps} resultDisplay={undefined} />,
|
||||
{ uiState: { streamingState: StreamingState.Idle } },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initially, no focus hint
|
||||
expect(lastFrame()).toMatchSnapshot('initial-no-output');
|
||||
|
||||
// Advance timers by the delay
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Now it SHOULD contain the focus hint
|
||||
expect(lastFrame()).toMatchSnapshot('after-delay-no-output');
|
||||
expect(lastFrame()).toContain('(Tab to focus)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows focus hint after delay with output', async () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Component {...baseProps} resultDisplay="Some output" />,
|
||||
{ uiState: { streamingState: StreamingState.Idle } },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initially, no focus hint
|
||||
expect(lastFrame()).toMatchSnapshot('initial-with-output');
|
||||
|
||||
// Advance timers
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot('after-delay-with-output');
|
||||
expect(lastFrame()).toContain('(Tab to focus)');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles long descriptions by shrinking them to show the focus hint', async () => {
|
||||
const longDescription = 'A'.repeat(100);
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
description={longDescription}
|
||||
@@ -113,15 +119,18 @@ describe('Focus Hint', () => {
|
||||
/>,
|
||||
{ uiState: { streamingState: StreamingState.Idle } },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// The focus hint should be visible
|
||||
expect(lastFrame()).toMatchSnapshot('long-description');
|
||||
expect(lastFrame()).toContain('(Tab to focus)');
|
||||
// The name should still be visible
|
||||
expect(lastFrame()).toContain(SHELL_COMMAND_NAME);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,8 +63,8 @@ describe('<ToolMessage /> - Raw Markdown Display Snapshots', () => {
|
||||
},
|
||||
])(
|
||||
'renders with renderMarkdown=$renderMarkdown, useAlternateBuffer=$useAlternateBuffer $description',
|
||||
({ renderMarkdown, useAlternateBuffer, availableTerminalHeight }) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
async ({ renderMarkdown, useAlternateBuffer, availableTerminalHeight }) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<StreamingContext.Provider value={StreamingState.Idle}>
|
||||
<ToolMessage
|
||||
{...baseProps}
|
||||
@@ -76,7 +76,9 @@ describe('<ToolMessage /> - Raw Markdown Display Snapshots', () => {
|
||||
useAlternateBuffer,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,10 +33,7 @@ describe('ToolResultDisplay', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
});
|
||||
|
||||
// Helper to use renderWithProviders
|
||||
const render = (ui: React.ReactElement) => renderWithProviders(ui);
|
||||
|
||||
it('uses ScrollableList for ANSI output in alternate buffer mode', () => {
|
||||
it('uses ScrollableList for ANSI output in alternate buffer mode', async () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const content = 'ansi content';
|
||||
const ansiResult: AnsiOutput = [
|
||||
@@ -53,57 +50,65 @@ describe('ToolResultDisplay', () => {
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
maxLines={10}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain(content);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses Scrollable for non-ANSI output in alternate buffer mode', () => {
|
||||
it('uses Scrollable for non-ANSI output in alternate buffer mode', async () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="**Markdown content**"
|
||||
terminalWidth={80}
|
||||
maxLines={10}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// With real components, we check for the content itself
|
||||
expect(output).toContain('Markdown content');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes hasFocus prop to scrollable components', () => {
|
||||
it('passes hasFocus prop to scrollable components', async () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
terminalWidth={80}
|
||||
hasFocus={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Some result');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders string result as markdown by default', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders string result as markdown by default', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders string result as plain text when renderOutputAsMarkdown is false', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders string result as plain text when renderOutputAsMarkdown is false', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
@@ -111,43 +116,49 @@ describe('ToolResultDisplay', () => {
|
||||
renderOutputAsMarkdown={false}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates very long string results', { timeout: 20000 }, () => {
|
||||
it('truncates very long string results', { timeout: 20000 }, async () => {
|
||||
const longString = 'a'.repeat(1000005);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={longString}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders file diff result', () => {
|
||||
it('renders file diff result', async () => {
|
||||
const diffResult = {
|
||||
fileDiff: 'diff content',
|
||||
fileName: 'test.ts',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={diffResult}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders ANSI output result', () => {
|
||||
it('renders ANSI output result', async () => {
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
@@ -162,38 +173,42 @@ describe('ToolResultDisplay', () => {
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult as unknown as AnsiOutput}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders nothing for todos result', () => {
|
||||
it('renders nothing for todos result', async () => {
|
||||
const todoResult = {
|
||||
todos: [],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={todoResult}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
await waitUntilReady();
|
||||
const output = lastFrame({ allowEmpty: true });
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not fall back to plain text if availableHeight is set and not in alternate buffer', () => {
|
||||
it('does not fall back to plain text if availableHeight is set and not in alternate buffer', async () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
@@ -201,13 +216,15 @@ describe('ToolResultDisplay', () => {
|
||||
renderOutputAsMarkdown={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('keeps markdown if in alternate buffer even with availableHeight', () => {
|
||||
it('keeps markdown if in alternate buffer even with availableHeight', async () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
@@ -215,12 +232,14 @@ describe('ToolResultDisplay', () => {
|
||||
renderOutputAsMarkdown={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates ANSI output when maxLines is provided', () => {
|
||||
it('truncates ANSI output when maxLines is provided', async () => {
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
@@ -259,7 +278,7 @@ describe('ToolResultDisplay', () => {
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
@@ -267,14 +286,16 @@ describe('ToolResultDisplay', () => {
|
||||
maxLines={2}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined', () => {
|
||||
it('truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined', async () => {
|
||||
const ansiResult: AnsiOutput = Array.from({ length: 50 }, (_, i) => [
|
||||
{
|
||||
text: `Line ${i + 1}`,
|
||||
@@ -287,7 +308,7 @@ describe('ToolResultDisplay', () => {
|
||||
inverse: false,
|
||||
},
|
||||
]);
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult}
|
||||
terminalWidth={80}
|
||||
@@ -295,10 +316,12 @@ describe('ToolResultDisplay', () => {
|
||||
availableTerminalHeight={undefined}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// It SHOULD truncate to 25 lines because maxLines is provided
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).toContain('Line 50');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Box height={terminalHeight}>
|
||||
<TestComponent />
|
||||
</Box>,
|
||||
@@ -100,6 +100,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
uiState: { terminalWidth },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial state: tool-1 should be visible
|
||||
await waitFor(() => {
|
||||
@@ -112,6 +113,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
await act(async () => {
|
||||
listRef?.scrollBy(5);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// tool-1 header should still be visible because it is sticky
|
||||
await waitFor(() => {
|
||||
@@ -130,6 +132,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
await act(async () => {
|
||||
listRef?.scrollBy(17);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('tool-2');
|
||||
@@ -138,6 +141,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
// tool-1 should be gone now (both header and content)
|
||||
expect(lastFrame()).not.toContain('tool-1');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers', async () => {
|
||||
@@ -177,7 +181,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
);
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Box height={terminalHeight}>
|
||||
<TestComponent />
|
||||
</Box>,
|
||||
@@ -186,6 +190,7 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
uiState: { terminalWidth },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(SHELL_COMMAND_NAME);
|
||||
@@ -196,11 +201,13 @@ describe('ToolMessage Sticky Header Regression', () => {
|
||||
await act(async () => {
|
||||
listRef?.scrollBy(5);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(SHELL_COMMAND_NAME);
|
||||
});
|
||||
expect(lastFrame()).toContain('shell-06');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,46 +14,54 @@ vi.mock('../../utils/commandUtils.js', () => ({
|
||||
}));
|
||||
|
||||
describe('UserMessage', () => {
|
||||
it('renders normal user message with correct prefix', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders normal user message with correct prefix', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<UserMessage text="Hello Gemini" width={80} />,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders slash command message', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders slash command message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<UserMessage text="/help" width={80} />,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiline user message', () => {
|
||||
it('renders multiline user message', async () => {
|
||||
const message = 'Line 1\nLine 2';
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<UserMessage text={message} width={80} />,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('transforms image paths in user message', () => {
|
||||
it('transforms image paths in user message', async () => {
|
||||
const message = 'Check out this image: @/path/to/my-image.png';
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<UserMessage text={message} width={80} />,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('[Image my-image.png]');
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,18 +9,26 @@ import { WarningMessage } from './WarningMessage.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('WarningMessage', () => {
|
||||
it('renders with the correct prefix and text', () => {
|
||||
const { lastFrame } = render(<WarningMessage text="Watch out!" />);
|
||||
it('renders with the correct prefix and text', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<WarningMessage text="Watch out!" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiline warning messages', () => {
|
||||
it('renders multiline warning messages', async () => {
|
||||
const message = 'Warning line 1\nWarning line 2';
|
||||
const { lastFrame } = render(<WarningMessage text={message} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<WarningMessage text={message} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
1 + const newVar = 1;
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 - const anotherOld = 'test';
|
||||
20 + const anotherNew = 'test';"
|
||||
20 + const anotherNew = 'test';
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
@@ -14,7 +15,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
22 console.log('end of second
|
||||
hunk');"
|
||||
hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
|
||||
@@ -23,7 +25,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
22 console.log('end of second hunk');"
|
||||
22 console.log('end of second hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height undefined 1`] = `
|
||||
@@ -35,24 +38,30 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
22 console.log('end of second hunk');"
|
||||
22 console.log('end of second hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a new file with no file extension correctly 1`] = `
|
||||
"1 FROM node:14
|
||||
2 RUN npm install
|
||||
3 RUN npm run build"
|
||||
3 RUN npm run build
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should handle diff with only header and no changes 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should handle empty diff content 1`] = `"No diff content."`;
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should handle empty diff content 1`] = `
|
||||
"No diff content.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP) 1`] = `
|
||||
" 1 context line 1
|
||||
@@ -64,7 +73,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
12 context line 12
|
||||
13 context line 13
|
||||
14 context line 14
|
||||
15 context line 15"
|
||||
15 context line 15
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should render a gap indicator for skipped lines 1`] = `
|
||||
@@ -73,12 +83,14 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
2 + added line
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
10 context line 10
|
||||
11 context line 11"
|
||||
11 context line 11
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should render diff content for existing file (not calling colorizeCode directly for the whole block) 1`] = `
|
||||
"1 - old line
|
||||
1 + new line"
|
||||
1 + new line
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with a SVN diff format 1`] = `
|
||||
@@ -86,7 +98,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
1 + const newVar = 1;
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 - const anotherOld = 'test';
|
||||
20 + const anotherNew = 'test';"
|
||||
20 + const anotherNew = 'test';
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
@@ -95,7 +108,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
22 console.log('end of second
|
||||
hunk');"
|
||||
hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
|
||||
@@ -104,7 +118,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
22 console.log('end of second hunk');"
|
||||
22 console.log('end of second hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height undefined 1`] = `
|
||||
@@ -116,24 +131,30 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
22 console.log('end of second hunk');"
|
||||
22 console.log('end of second hunk');
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a new file with no file extension correctly 1`] = `
|
||||
"1 FROM node:14
|
||||
2 RUN npm install
|
||||
3 RUN npm run build"
|
||||
3 RUN npm run build
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should handle diff with only header and no changes 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should handle empty diff content 1`] = `"No diff content."`;
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should handle empty diff content 1`] = `
|
||||
"No diff content.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP) 1`] = `
|
||||
" 1 context line 1
|
||||
@@ -145,7 +166,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
12 context line 12
|
||||
13 context line 13
|
||||
14 context line 14
|
||||
15 context line 15"
|
||||
15 context line 15
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should render a gap indicator for skipped lines 1`] = `
|
||||
@@ -154,10 +176,12 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
2 + added line
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
10 context line 10
|
||||
11 context line 11"
|
||||
11 context line 11
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should render diff content for existing file (not calling colorizeCode directly for the whole block) 1`] = `
|
||||
"1 - old line
|
||||
1 + new line"
|
||||
1 + new line
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
exports[`InfoMessage > renders multiline info messages 1`] = `
|
||||
"
|
||||
ℹ Info line 1
|
||||
Info line 2"
|
||||
Info line 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InfoMessage > renders with a custom icon 1`] = `
|
||||
"
|
||||
★Custom icon test"
|
||||
★Custom icon test
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InfoMessage > renders with the correct default prefix and text 1`] = `
|
||||
"
|
||||
ℹ Just so you know"
|
||||
ℹ Just so you know
|
||||
"
|
||||
`;
|
||||
|
||||
+18
-9
@@ -18,7 +18,8 @@ exports[`<ShellToolMessage /> > Height Constraints > defaults to ACTIVE_SHELL_MA
|
||||
│ Line 97 │
|
||||
│ Line 98 ▄ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │"
|
||||
│ Line 100 █ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES 1`] = `
|
||||
@@ -32,7 +33,8 @@ exports[`<ShellToolMessage /> > Height Constraints > respects availableTerminalH
|
||||
│ Line 97 │
|
||||
│ Line 98 │
|
||||
│ Line 99 │
|
||||
│ Line 100 █ │"
|
||||
│ Line 100 █ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large 1`] = `
|
||||
@@ -53,7 +55,8 @@ exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES
|
||||
│ Line 97 │
|
||||
│ Line 98 ▄ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │"
|
||||
│ Line 100 █ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminalHeight when focused in alternate buffer mode 1`] = `
|
||||
@@ -158,7 +161,8 @@ exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminal
|
||||
│ Line 98 █ │
|
||||
│ Line 99 █ │
|
||||
│ Line 100 █ │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while focused 1`] = `
|
||||
@@ -166,33 +170,38 @@ exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode whi
|
||||
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
|
||||
│ │
|
||||
│ Test result │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while unfocused 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Error state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x Shell Command A shell command │
|
||||
│ │
|
||||
│ Error output │"
|
||||
│ Error output │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Executing state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ShellToolMessage /> > Snapshots > renders in Success state (history mode) 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Shell Command A shell command │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: false) > renders a todo list with long descriptions that wrap when full view is on 1`] = `
|
||||
"──────────────────────────────────────────────────
|
||||
Todo 1/2 completed (ctrl+t to toggle) » This i…"
|
||||
Todo 1/2 completed (ctrl+t to toggle) » This i…
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: false) > renders full list when all todos are inactive 1`] = `""`;
|
||||
@@ -13,17 +14,20 @@ exports[`<TodoTray /> (showFullTodos: false) > renders null when todo list is em
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: false) > renders the most recent todo list when multiple write_todos calls are in history 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Todo 0/2 completed (ctrl+t to toggle) » Newer Task 2"
|
||||
Todo 0/2 completed (ctrl+t to toggle) » Newer Task 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: false) > renders when todos exist and one is in progress 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Todo 1/3 completed (ctrl+t to toggle) » Task 2"
|
||||
Todo 1/3 completed (ctrl+t to toggle) » Task 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: false) > renders when todos exist but none are in progress 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Todo 1/2 completed (ctrl+t to toggle)"
|
||||
Todo 1/2 completed (ctrl+t to toggle)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: true) > renders a todo list with long descriptions that wrap when full view is on 1`] = `
|
||||
@@ -34,7 +38,8 @@ exports[`<TodoTray /> (showFullTodos: true) > renders a todo list with long desc
|
||||
task that should wrap around multiple lines
|
||||
when the terminal width is constrained.
|
||||
✓ Another completed task with an equally verbose
|
||||
description to test wrapping behavior."
|
||||
description to test wrapping behavior.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: true) > renders full list when all todos are inactive 1`] = `
|
||||
@@ -42,7 +47,8 @@ exports[`<TodoTray /> (showFullTodos: true) > renders full list when all todos a
|
||||
Todo 1/1 completed (ctrl+t to toggle)
|
||||
|
||||
✓ Task 1
|
||||
✗ Task 2"
|
||||
✗ Task 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: true) > renders null when no todos are in the history 1`] = `""`;
|
||||
@@ -54,7 +60,8 @@ exports[`<TodoTray /> (showFullTodos: true) > renders the most recent todo list
|
||||
Todo 0/2 completed (ctrl+t to toggle)
|
||||
|
||||
☐ Newer Task 1
|
||||
» Newer Task 2"
|
||||
» Newer Task 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: true) > renders when todos exist and one is in progress 1`] = `
|
||||
@@ -64,7 +71,8 @@ exports[`<TodoTray /> (showFullTodos: true) > renders when todos exist and one i
|
||||
☐ Pending Task
|
||||
» Task 2
|
||||
✗ In Progress Task
|
||||
✓ Completed Task"
|
||||
✓ Completed Task
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<TodoTray /> (showFullTodos: true) > renders when todos exist but none are in progress 1`] = `
|
||||
@@ -73,5 +81,6 @@ exports[`<TodoTray /> (showFullTodos: true) > renders when todos exist but none
|
||||
|
||||
☐ Pending Task
|
||||
✗ In Progress Task
|
||||
✓ Completed Task"
|
||||
✓ Completed Task
|
||||
"
|
||||
`;
|
||||
|
||||
+12
-10
@@ -55,13 +55,14 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to b… │
|
||||
│──────────────────────────────────────────────────────────────────────────│
|
||||
│ │ ▄
|
||||
│ ✓ tool-2 Description 2 │ █
|
||||
│ │ █
|
||||
│ line1 │ █
|
||||
│ line2 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █
|
||||
█"
|
||||
│ │ ▄
|
||||
│ ✓ tool-2 Description 2 │ █
|
||||
│ │ █
|
||||
│ line1 │ █
|
||||
│ line2 │ █
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █
|
||||
█
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
||||
@@ -113,9 +114,10 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-2 Description 2 │
|
||||
│ │
|
||||
│ line1 │ ▄
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █
|
||||
█"
|
||||
│ line1 │ ▄
|
||||
╰──────────────────────────────────────────────────────────────────────────╯ █
|
||||
█
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
||||
|
||||
@@ -7,70 +7,80 @@ exports[`<ToolMessage /> > JSON rendering > renders pretty JSON in ink frame 1`]
|
||||
│ { │
|
||||
│ "a": 1, │
|
||||
│ "b": 2 │
|
||||
│ } │"
|
||||
│ } │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ? for Confirming status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ? test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ - test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ hello │"
|
||||
│ hello │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
||||
@@ -78,26 +88,30 @@ exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ 1 - old │
|
||||
│ 1 + new │"
|
||||
│ 1 + new │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders basic tool information 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing ← │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │"
|
||||
│ Test result │
|
||||
"
|
||||
`;
|
||||
|
||||
+18
-9
@@ -3,53 +3,62 @@
|
||||
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing (Tab to focus) │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing (Tab to focus) │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing (Tab to focus) │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing (Tab to focus) │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command A tool for testing │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │
|
||||
│ │"
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
+12
-6
@@ -4,40 +4,46 @@ exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderM
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test **bold** and \`code\` markdown │"
|
||||
│ Test **bold** and \`code\` markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=true '(raw markdown, alternate buffer)' 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test **bold** and \`code\` markdown │"
|
||||
│ Test **bold** and \`code\` markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(constrained height, regular buffer -…' 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test bold and code markdown │"
|
||||
│ Test bold and code markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(default, regular buffer)' 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test bold and code markdown │"
|
||||
│ Test bold and code markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(constrained height, alternate buffer…' 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test bold and code markdown │"
|
||||
│ Test bold and code markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(default, alternate buffer)' 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test bold and code markdown │"
|
||||
│ Test bold and code markdown │
|
||||
"
|
||||
`;
|
||||
|
||||
+24
-7
@@ -1,24 +1,40 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolResultDisplay > does not fall back to plain text if availableHeight is set and not in alternate buffer 1`] = `"Some result"`;
|
||||
exports[`ToolResultDisplay > does not fall back to plain text if availableHeight is set and not in alternate buffer 1`] = `
|
||||
"Some result
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"Some result"`;
|
||||
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `
|
||||
"Some result
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `
|
||||
"ansi content
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > renders file diff result 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ No changes detected. │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"Some result"`;
|
||||
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `
|
||||
"Some result
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"**Some result**"`;
|
||||
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `
|
||||
"**Some result**
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > truncates very long string results 1`] = `
|
||||
"... first 252 lines hidden ...
|
||||
@@ -33,5 +49,6 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaa"
|
||||
aaaaaaaaaaaaaaa
|
||||
"
|
||||
`;
|
||||
|
||||
+10
-5
@@ -5,7 +5,8 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage i
|
||||
│ ✓ Shell Command Description for Shell Command │ ▀
|
||||
│ │
|
||||
│ shell-01 │
|
||||
│ shell-02 │"
|
||||
│ shell-02 │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = `
|
||||
@@ -13,7 +14,8 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage i
|
||||
│ ✓ Shell Command Description for Shell Command │
|
||||
│────────────────────────────────────────────────────────────────────────│ █
|
||||
│ shell-06 │ ▀
|
||||
│ shell-07 │"
|
||||
│ shell-07 │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 1`] = `
|
||||
@@ -21,7 +23,8 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa
|
||||
│ ✓ tool-1 Description for tool-1 │
|
||||
│ │
|
||||
│ c1-01 │
|
||||
│ c1-02 │"
|
||||
│ c1-02 │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 2`] = `
|
||||
@@ -29,7 +32,8 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa
|
||||
│ ✓ tool-1 Description for tool-1 │ █
|
||||
│────────────────────────────────────────────────────────────────────────│
|
||||
│ c1-06 │
|
||||
│ c1-07 │"
|
||||
│ c1-07 │
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 3`] = `
|
||||
@@ -37,5 +41,6 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa
|
||||
│ ✓ tool-2 Description for tool-2 │
|
||||
│────────────────────────────────────────────────────────────────────────│
|
||||
│ c2-10 │
|
||||
╰────────────────────────────────────────────────────────────────────────╯ █"
|
||||
╰────────────────────────────────────────────────────────────────────────╯ █
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -4,23 +4,27 @@ exports[`UserMessage > renders multiline user message 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Line 1
|
||||
Line 2
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > renders normal user message with correct prefix 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Hello Gemini
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > renders slash command message 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> /help
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`UserMessage > transforms image paths in user message 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Check out this image: [Image my-image.png]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
exports[`WarningMessage > renders multiline warning messages 1`] = `
|
||||
"
|
||||
⚠ Warning line 1
|
||||
Warning line 2"
|
||||
Warning line 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`WarningMessage > renders with the correct prefix and text 1`] = `
|
||||
"
|
||||
⚠ Watch out!"
|
||||
⚠ Watch out!
|
||||
"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user