File size: 6,094 Bytes
9332ccf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { AnsiOutputText } from './AnsiOutput.js';
import type { AnsiOutput, AnsiToken } from '@google/gemini-cli-core';
// Helper to create a valid AnsiToken with default values
const createAnsiToken = (overrides: Partial<AnsiToken>): AnsiToken => ({
text: '',
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
isUninitialized: false,
fg: '#ffffff',
bg: '#000000',
...overrides,
});
describe('<AnsiOutputText />', () => {
it('renders a simple AnsiOutput object correctly', async () => {
const data: AnsiOutput = [
[
createAnsiToken({ text: 'Hello, ' }),
createAnsiToken({ text: 'world!' }),
],
];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} width={80} />,
);
expect(lastFrame().trim()).toBe('Hello, world!');
unmount();
});
// Note: ink-testing-library doesn't render styles, so we can only check the text.
// We are testing that it renders without crashing.
it.each([
{ style: { bold: true }, text: 'Bold' },
{ style: { italic: true }, text: 'Italic' },
{ style: { underline: true }, text: 'Underline' },
{ style: { dim: true }, text: 'Dim' },
{ style: { inverse: true }, text: 'Inverse' },
])('correctly applies style $text', async ({ style, text }) => {
const data: AnsiOutput = [[createAnsiToken({ text, ...style })]];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} width={80} />,
);
expect(lastFrame().trim()).toBe(text);
unmount();
});
it.each([
{ color: { fg: '#ff0000' }, text: 'Red FG' },
{ color: { bg: '#0000ff' }, text: 'Blue BG' },
{ color: { fg: '#00ff00', bg: '#ff00ff' }, text: 'Green FG Magenta BG' },
])('correctly applies color $text', async ({ color, text }) => {
const data: AnsiOutput = [[createAnsiToken({ text, ...color })]];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} width={80} />,
);
expect(lastFrame().trim()).toBe(text);
unmount();
});
it('handles empty lines and empty tokens', async () => {
const data: AnsiOutput = [
[createAnsiToken({ text: 'First line' })],
[],
[createAnsiToken({ text: 'Third line' })],
[createAnsiToken({ text: '' })],
];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} width={80} />,
);
const output = lastFrame();
expect(output).toBeDefined();
const lines = output.split('\n');
expect(lines[0].trim()).toBe('First line');
expect(lines[1].trim()).toBe('');
expect(lines[2].trim()).toBe('Third line');
unmount();
});
it('respects the availableTerminalHeight prop and slices the lines correctly', async () => {
const data: AnsiOutput = [
[createAnsiToken({ text: 'Line 1' })],
[createAnsiToken({ text: 'Line 2' })],
[createAnsiToken({ text: 'Line 3' })],
[createAnsiToken({ text: 'Line 4' })],
];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} availableTerminalHeight={2} width={80} />,
);
const output = lastFrame();
expect(output).not.toContain('Line 1');
expect(output).not.toContain('Line 2');
expect(output).toContain('Line 3');
expect(output).toContain('Line 4');
unmount();
});
it('respects the maxLines prop and slices the lines correctly', async () => {
const data: AnsiOutput = [
[createAnsiToken({ text: 'Line 1' })],
[createAnsiToken({ text: 'Line 2' })],
[createAnsiToken({ text: 'Line 3' })],
[createAnsiToken({ text: 'Line 4' })],
];
const { lastFrame, unmount } = await render(
<AnsiOutputText data={data} maxLines={2} width={80} />,
);
const output = lastFrame();
expect(output).not.toContain('Line 1');
expect(output).not.toContain('Line 2');
expect(output).toContain('Line 3');
expect(output).toContain('Line 4');
unmount();
});
it('prioritizes maxLines over availableTerminalHeight if maxLines is smaller', async () => {
const data: AnsiOutput = [
[createAnsiToken({ text: 'Line 1' })],
[createAnsiToken({ text: 'Line 2' })],
[createAnsiToken({ text: 'Line 3' })],
[createAnsiToken({ text: 'Line 4' })],
];
// availableTerminalHeight=3, maxLines=2 => show 2 lines
const { lastFrame, unmount } = await render(
<AnsiOutputText
data={data}
availableTerminalHeight={3}
maxLines={2}
width={80}
/>,
);
const output = lastFrame();
expect(output).not.toContain('Line 2');
expect(output).toContain('Line 3');
expect(output).toContain('Line 4');
unmount();
});
it('renders a large AnsiOutput object without crashing', async () => {
const largeData: AnsiOutput = [];
for (let i = 0; i < 1000; i++) {
largeData.push([createAnsiToken({ text: `Line ${i}` })]);
}
const { lastFrame, unmount } = await render(
<AnsiOutputText data={largeData} width={80} />,
);
// We are just checking that it renders something without crashing.
expect(lastFrame()).toBeDefined();
unmount();
});
describe('robustness', () => {
it('does NOT crash when data is undefined', async () => {
const { lastFrame, unmount } = await render(
<AnsiOutputText
data={undefined as unknown as AnsiOutput}
width={80}
disableTruncation={true}
/>,
);
expect(lastFrame({ allowEmpty: true }).trim()).toBe('');
unmount();
});
it('does NOT crash when data is an object but not an array', async () => {
const { lastFrame, unmount } = await render(
<AnsiOutputText
data={{ summary: 'test' } as unknown as AnsiOutput}
width={80}
disableTruncation={true}
/>,
);
expect(lastFrame({ allowEmpty: true }).trim()).toBe('');
unmount();
});
});
});
|