mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
271 lines
8.2 KiB
TypeScript
271 lines
8.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { render, renderWithProviders } from '../../../test-utils/render.js';
|
|
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
|
import { MaxSizedBox } from './MaxSizedBox.js';
|
|
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
|
import { Box, Text } from 'ink';
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('<MaxSizedBox />', () => {
|
|
it('renders children without truncation when they fit', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
|
<Box>
|
|
<Text>Hello, World!</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain('Hello, World!');
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('hides lines when content exceeds maxHeight', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
<Text>Line 2</Text>
|
|
<Text>Line 3</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... first 2 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
<Text>Line 2</Text>
|
|
<Text>Line 3</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... last 2 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('shows plural "lines" when more than one line is hidden', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
<Text>Line 2</Text>
|
|
<Text>Line 3</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... first 2 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('shows singular "line" when exactly one line is hidden', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={1}>
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... first 1 line hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('accounts for additionalHiddenLinesCount', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
<Text>Line 2</Text>
|
|
<Text>Line 3</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... first 7 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('wraps text that exceeds maxWidth', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={10} maxHeight={5}>
|
|
<Box>
|
|
<Text wrap="wrap">This is a long line of text</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain('This is a');
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('does not truncate when maxHeight is undefined', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
|
|
<Box flexDirection="column">
|
|
<Text>Line 1</Text>
|
|
<Text>Line 2</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain('Line 1');
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('renders an empty box for empty children', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame({ allowEmpty: true })?.trim()).equals('');
|
|
unmount();
|
|
});
|
|
|
|
it('handles React.Fragment as a child', async () => {
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
|
<Box flexDirection="column">
|
|
<>
|
|
<Text>Line 1 from Fragment</Text>
|
|
<Text>Line 2 from Fragment</Text>
|
|
</>
|
|
<Text>Line 3 direct child</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain('Line 1 from Fragment');
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('clips a long single text child from the top', async () => {
|
|
const THIRTY_LINES = Array.from(
|
|
{ length: 30 },
|
|
(_, i) => `Line ${i + 1}`,
|
|
).join('\n');
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="top">
|
|
<Box>
|
|
<Text>{THIRTY_LINES}</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... first 21 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('clips a long single text child from the bottom', async () => {
|
|
const THIRTY_LINES = Array.from(
|
|
{ length: 30 },
|
|
(_, i) => `Line ${i + 1}`,
|
|
).join('\n');
|
|
const { lastFrame, waitUntilReady, unmount } = render(
|
|
<OverflowProvider>
|
|
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
|
|
<Box>
|
|
<Text>{THIRTY_LINES}</Text>
|
|
</Box>
|
|
</MaxSizedBox>
|
|
</OverflowProvider>,
|
|
);
|
|
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain(
|
|
'... last 21 lines hidden (Ctrl+O to show) ...',
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
|
|
it('does not leak content after hidden indicator with bottom overflow', async () => {
|
|
const markdownContent = Array.from(
|
|
{ length: 20 },
|
|
(_, i) => `- Step ${i + 1}: Do something important`,
|
|
).join('\n');
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
<MaxSizedBox maxWidth={80} maxHeight={5} overflowDirection="bottom">
|
|
<MarkdownDisplay
|
|
text={`## Plan\n\n${markdownContent}`}
|
|
isPending={false}
|
|
terminalWidth={76}
|
|
/>
|
|
</MaxSizedBox>,
|
|
{ width: 80 },
|
|
);
|
|
|
|
await waitUntilReady();
|
|
expect(lastFrame()).toContain('... last');
|
|
|
|
const frame = lastFrame();
|
|
const lines = frame.trim().split('\n');
|
|
const lastLine = lines[lines.length - 1];
|
|
|
|
// The last line should only contain the hidden indicator, no leaked content
|
|
expect(lastLine).toMatch(
|
|
/^\.\.\. last \d+ lines? hidden \(Ctrl\+O to show\) \.\.\.$/,
|
|
);
|
|
expect(lastFrame()).toMatchSnapshot();
|
|
unmount();
|
|
});
|
|
});
|