summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
diff options
context:
space:
mode:
authorJacob Richman <[email protected]>2025-06-19 20:17:23 +0000
committerGitHub <[email protected]>2025-06-19 13:17:23 -0700
commitb0bc7c3d996d25c9fefdfbcba3ca19fa46ad199f (patch)
treec2d89d14b8dade1daf51f835969d9b0e79d4df30 /packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
parent10a83a6395b70f21b01da99d0992c78d0354a8dd (diff)
Fix flicker issues by ensuring all actively changing content fits in the viewport (#1217)
Diffstat (limited to 'packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx')
-rw-r--r--packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx303
1 files changed, 303 insertions, 0 deletions
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
new file mode 100644
index 00000000..23ef98cd
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
@@ -0,0 +1,303 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { MaxSizedBox } from './MaxSizedBox.js';
+import { Box, Text } from 'ink';
+import { describe, it, expect } from 'vitest';
+
+describe('<MaxSizedBox />', () => {
+ it('renders children without truncation when they fit', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={10}>
+ <Box>
+ <Text>Hello, World!</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals('Hello, World!');
+ });
+
+ it('hides lines when content exceeds maxHeight', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={2}>
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ <Box>
+ <Text>Line 3</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`... first 2 lines hidden ...
+Line 3`);
+ });
+
+ it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ <Box>
+ <Text>Line 3</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`Line 1
+... last 2 lines hidden ...`);
+ });
+
+ it('wraps text that exceeds maxWidth', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={10} maxHeight={5}>
+ <Box>
+ <Text wrap="wrap">This is a long line of text</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ expect(lastFrame()).equals(`This is a
+long line
+of text`);
+ });
+
+ it('handles mixed wrapping and non-wrapping segments', () => {
+ const multilineText = `This part will wrap around.
+And has a line break.
+ Leading spaces preserved.`;
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={20} maxHeight={20}>
+ <Box>
+ <Text>Example</Text>
+ </Box>
+ <Box>
+ <Text>No Wrap: </Text>
+ <Text wrap="wrap">{multilineText}</Text>
+ </Box>
+ <Box>
+ <Text>Longer No Wrap: </Text>
+ <Text wrap="wrap">This part will wrap around.</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ expect(lastFrame()).equals(
+ `Example
+No Wrap: This part
+ will wrap
+ around.
+ And has a
+ line break.
+ Leading
+ spaces
+ preserved.
+Longer No Wrap: This
+ part
+ will
+ wrap
+ arou
+ nd.`,
+ );
+ });
+
+ it('handles words longer than maxWidth by splitting them', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={5} maxHeight={5}>
+ <Box>
+ <Text wrap="wrap">Supercalifragilisticexpialidocious</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ expect(lastFrame()).equals(`... …
+istic
+expia
+lidoc
+ious`);
+ });
+
+ it('does not truncate when maxHeight is undefined', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={undefined}>
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`Line 1
+Line 2`);
+ });
+
+ it('shows plural "lines" when more than one line is hidden', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={2}>
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ <Box>
+ <Text>Line 3</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`... first 2 lines hidden ...
+Line 3`);
+ });
+
+ it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ <Box>
+ <Text>Line 3</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`Line 1
+... last 2 lines hidden ...`);
+ });
+
+ it('renders an empty box for empty children', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>,
+ );
+ // Expect an empty string or a box with nothing in it.
+ // Ink renders an empty box as an empty string.
+ expect(lastFrame()).equals('');
+ });
+
+ it('wraps text with multi-byte unicode characters correctly', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={5} maxHeight={5}>
+ <Box>
+ <Text wrap="wrap">你好世界</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ // "你好" has a visual width of 4. "世界" has a visual width of 4.
+ // With maxWidth=5, it should wrap after the second character.
+ expect(lastFrame()).equals(`你好
+世界`);
+ });
+
+ it('wraps text with multi-byte emoji characters correctly', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={5} maxHeight={5}>
+ <Box>
+ <Text wrap="wrap">🐶🐶🐶🐶🐶</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ // Each "🐶" has a visual width of 2.
+ // With maxWidth=5, it should wrap every 2 emojis.
+ expect(lastFrame()).equals(`🐶🐶
+🐶🐶
+🐶`);
+ });
+
+ it('accounts for additionalHiddenLinesCount', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
+ <Box>
+ <Text>Line 1</Text>
+ </Box>
+ <Box>
+ <Text>Line 2</Text>
+ </Box>
+ <Box>
+ <Text>Line 3</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ // 1 line is hidden by overflow, 5 are additionally hidden.
+ expect(lastFrame()).equals(`... first 7 lines hidden ...
+Line 3`);
+ });
+
+ it('handles React.Fragment as a child', () => {
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={10}>
+ <>
+ <Box>
+ <Text>Line 1 from Fragment</Text>
+ </Box>
+ <Box>
+ <Text>Line 2 from Fragment</Text>
+ </Box>
+ </>
+ <Box>
+ <Text>Line 3 direct child</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+ expect(lastFrame()).equals(`Line 1 from Fragment
+Line 2 from Fragment
+Line 3 direct child`);
+ });
+
+ it('clips a long single text child from the top', () => {
+ const THIRTY_LINES = Array.from(
+ { length: 30 },
+ (_, i) => `Line ${i + 1}`,
+ ).join('\n');
+
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={10}>
+ <Box>
+ <Text>{THIRTY_LINES}</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ const expected = [
+ '... first 21 lines hidden ...',
+ ...Array.from({ length: 9 }, (_, i) => `Line ${22 + i}`),
+ ].join('\n');
+
+ expect(lastFrame()).equals(expected);
+ });
+
+ it('clips a long single text child from the bottom', () => {
+ const THIRTY_LINES = Array.from(
+ { length: 30 },
+ (_, i) => `Line ${i + 1}`,
+ ).join('\n');
+
+ const { lastFrame } = render(
+ <MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
+ <Box>
+ <Text>{THIRTY_LINES}</Text>
+ </Box>
+ </MaxSizedBox>,
+ );
+
+ const expected = [
+ ...Array.from({ length: 9 }, (_, i) => `Line ${i + 1}`),
+ '... last 21 lines hidden ...',
+ ].join('\n');
+
+ expect(lastFrame()).equals(expected);
+ });
+});