test(cli): refactor tests for async render utilities (#23252)

This commit is contained in:
Tommaso Sciortino
2026-03-20 20:08:29 +00:00
committed by GitHub
parent 86a3a913b5
commit 6c78eb7a39
198 changed files with 3592 additions and 4802 deletions
@@ -78,7 +78,6 @@ describe('BaseSelectionList', () => {
const result = await renderWithProviders(
<BaseSelectionList {...defaultProps} />,
);
await result.waitUntilReady();
return result;
};
@@ -313,7 +312,6 @@ describe('BaseSelectionList', () => {
const { rerender, lastFrame, waitUntilReady, unmount } =
await renderWithProviders(<BaseSelectionList {...componentProps} />);
await waitUntilReady();
// Function to simulate the activeIndex changing over time
const updateActiveIndex = async (newIndex: number) => {
@@ -25,7 +25,7 @@ const NUMERIC_OPTIONS: readonly SettingEnumOption[] = [
describe('<EnumSelector />', () => {
it('renders with string options and matches snapshot', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={LANGUAGE_OPTIONS}
currentValue="en"
@@ -33,13 +33,12 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with numeric options and matches snapshot', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={NUMERIC_OPTIONS}
currentValue={2}
@@ -47,13 +46,12 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders inactive state and matches snapshot', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={LANGUAGE_OPTIONS}
currentValue="zh"
@@ -61,7 +59,6 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -70,7 +67,7 @@ describe('<EnumSelector />', () => {
const singleOption: readonly SettingEnumOption[] = [
{ label: 'Only Option', value: 'only' },
];
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={singleOption}
currentValue="only"
@@ -78,13 +75,12 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders nothing when no options are provided', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={[]}
currentValue=""
@@ -92,13 +88,12 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toBe('');
unmount();
});
it('handles currentValue not found in options', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={LANGUAGE_OPTIONS}
currentValue="invalid"
@@ -106,7 +101,6 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
// Should default to first option
expect(lastFrame()).toContain('English');
unmount();
@@ -122,7 +116,6 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('English');
await act(async () => {
@@ -141,7 +134,7 @@ describe('<EnumSelector />', () => {
});
it('shows navigation arrows when multiple options available', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={LANGUAGE_OPTIONS}
currentValue="en"
@@ -149,7 +142,6 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('←');
expect(lastFrame()).toContain('→');
unmount();
@@ -159,7 +151,7 @@ describe('<EnumSelector />', () => {
const singleOption: readonly SettingEnumOption[] = [
{ label: 'Only Option', value: 'only' },
];
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<EnumSelector
options={singleOption}
currentValue="only"
@@ -167,7 +159,6 @@ describe('<EnumSelector />', () => {
onValueChange={async () => {}}
/>,
);
await waitUntilReady();
expect(lastFrame()).not.toContain('←');
expect(lastFrame()).not.toContain('→');
unmount();
@@ -13,7 +13,7 @@ describe('ExpandableText', () => {
const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, '');
it('renders plain label when no match (short label)', async () => {
const renderResult = render(
const renderResult = await render(
<ExpandableText
label="simple command"
userInput=""
@@ -22,15 +22,14 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { unmount } = renderResult;
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
it('truncates long label when collapsed and no match', async () => {
const long = 'x'.repeat(MAX_WIDTH + 25);
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={long}
userInput=""
@@ -38,8 +37,7 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { lastFrame, unmount } = renderResult;
const out = lastFrame();
const f = flat(out);
expect(f.endsWith('...')).toBe(true);
@@ -50,7 +48,7 @@ describe('ExpandableText', () => {
it('shows full long label when expanded and no match', async () => {
const long = 'y'.repeat(MAX_WIDTH + 25);
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={long}
userInput=""
@@ -58,8 +56,7 @@ describe('ExpandableText', () => {
isExpanded={true}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { lastFrame, unmount } = renderResult;
const out = lastFrame();
const f = flat(out);
expect(f.length).toBe(long.length);
@@ -71,7 +68,7 @@ describe('ExpandableText', () => {
const label = 'run: git commit -m "feat: add search"';
const userInput = 'commit';
const matchedIndex = label.indexOf(userInput);
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={label}
userInput={userInput}
@@ -81,8 +78,7 @@ describe('ExpandableText', () => {
/>,
100,
);
const { waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { unmount } = renderResult;
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
@@ -93,7 +89,7 @@ describe('ExpandableText', () => {
const suffix = '/and/then/some/more/components/'.repeat(3);
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={label}
userInput={core}
@@ -103,8 +99,7 @@ describe('ExpandableText', () => {
/>,
100,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { lastFrame, unmount } = renderResult;
const out = lastFrame();
const f = flat(out);
expect(f.includes(core)).toBe(true);
@@ -120,7 +115,7 @@ describe('ExpandableText', () => {
const suffix = ' in this text';
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={label}
userInput={core}
@@ -129,8 +124,7 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { lastFrame, unmount } = renderResult;
const out = lastFrame();
const f = flat(out);
expect(f.includes('...')).toBe(true);
@@ -144,7 +138,7 @@ describe('ExpandableText', () => {
it('respects custom maxWidth', async () => {
const customWidth = 50;
const long = 'z'.repeat(100);
const renderResult = render(
const renderResult = await render(
<ExpandableText
label={long}
userInput=""
@@ -153,8 +147,7 @@ describe('ExpandableText', () => {
maxWidth={customWidth}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const { lastFrame, unmount } = renderResult;
const out = lastFrame();
const f = flat(out);
expect(f.endsWith('...')).toBe(true);
@@ -28,13 +28,12 @@ describe('<HalfLinePaddedBox />', () => {
it('renders standard background and blocks when not iTerm2', async () => {
vi.mocked(isITerm2).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -44,13 +43,12 @@ describe('<HalfLinePaddedBox />', () => {
it('renders iTerm2-specific blocks when iTerm2 is detected', async () => {
vi.mocked(isITerm2).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -58,7 +56,7 @@ describe('<HalfLinePaddedBox />', () => {
});
it('renders nothing when useBackgroundColor is false', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<HalfLinePaddedBox
backgroundBaseColor="blue"
backgroundOpacity={0.5}
@@ -68,7 +66,6 @@ describe('<HalfLinePaddedBox />', () => {
</HalfLinePaddedBox>,
{ width: 10 },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -78,13 +75,12 @@ describe('<HalfLinePaddedBox />', () => {
it('renders nothing when screen reader is enabled', async () => {
mockUseIsScreenReaderEnabled.mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
<Text>Content</Text>
</HalfLinePaddedBox>,
{ width: 10 },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -23,7 +23,7 @@ describe('<MaxSizedBox />', () => {
});
it('renders children without truncation when they fit', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}>
<Box>
@@ -42,7 +42,7 @@ describe('<MaxSizedBox />', () => {
});
it('hides lines when content exceeds maxHeight', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2}>
<Box flexDirection="column">
@@ -65,7 +65,7 @@ describe('<MaxSizedBox />', () => {
});
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
<Box flexDirection="column">
@@ -88,7 +88,7 @@ describe('<MaxSizedBox />', () => {
});
it('shows plural "lines" when more than one line is hidden', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2}>
<Box flexDirection="column">
@@ -111,7 +111,7 @@ describe('<MaxSizedBox />', () => {
});
it('shows singular "line" when exactly one line is hidden', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={1}>
<Box flexDirection="column">
@@ -132,7 +132,7 @@ describe('<MaxSizedBox />', () => {
});
it('accounts for additionalHiddenLinesCount', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
<Box flexDirection="column">
@@ -155,7 +155,7 @@ describe('<MaxSizedBox />', () => {
});
it('wraps text that exceeds maxWidth', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={10} maxHeight={5}>
<Box>
@@ -175,7 +175,7 @@ describe('<MaxSizedBox />', () => {
});
it('does not truncate when maxHeight is undefined', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
<Box flexDirection="column">
@@ -195,7 +195,7 @@ describe('<MaxSizedBox />', () => {
});
it('renders an empty box for empty children', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
</OverflowProvider>,
@@ -209,7 +209,7 @@ describe('<MaxSizedBox />', () => {
});
it('handles React.Fragment as a child', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10}>
<Box flexDirection="column">
@@ -236,7 +236,7 @@ describe('<MaxSizedBox />', () => {
{ length: 30 },
(_, i) => `Line ${i + 1}`,
).join('\n');
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="top">
<Box>
@@ -262,7 +262,7 @@ describe('<MaxSizedBox />', () => {
{ length: 30 },
(_, i) => `Line ${i + 1}`,
).join('\n');
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<OverflowProvider>
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
<Box>
@@ -29,25 +29,23 @@ describe('<Scrollable />', () => {
});
it('renders children', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<Scrollable hasFocus={false} height={5}>
<Text>Hello World</Text>
</Scrollable>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Hello World');
unmount();
});
it('renders multiple children', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<Scrollable hasFocus={false} height={5}>
<Text>Line 1</Text>
<Text>Line 2</Text>
<Text>Line 3</Text>
</Scrollable>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Line 1');
expect(lastFrame()).toContain('Line 2');
expect(lastFrame()).toContain('Line 3');
@@ -55,14 +53,13 @@ describe('<Scrollable />', () => {
});
it('matches snapshot', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<Scrollable hasFocus={false} height={5}>
<Text>Line 1</Text>
<Text>Line 2</Text>
<Text>Line 3</Text>
</Scrollable>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -77,7 +74,7 @@ describe('<Scrollable />', () => {
},
);
const { waitUntilReady, unmount } = await renderWithProviders(
const { unmount } = await renderWithProviders(
<Scrollable hasFocus={true} height={5}>
<Text>Line 1</Text>
<Text>Line 2</Text>
@@ -91,7 +88,6 @@ describe('<Scrollable />', () => {
<Text>Line 10</Text>
</Scrollable>,
);
await waitUntilReady();
expect(capturedEntry).toBeDefined();
@@ -104,22 +100,20 @@ describe('<Scrollable />', () => {
// Initial state with scrollToBottom={true}
unmount();
const { waitUntilReady: waitUntilReady2, unmount: unmount2 } =
await renderWithProviders(
<Scrollable hasFocus={true} height={5} scrollToBottom={true}>
<Text>Line 1</Text>
<Text>Line 2</Text>
<Text>Line 3</Text>
<Text>Line 4</Text>
<Text>Line 5</Text>
<Text>Line 6</Text>
<Text>Line 7</Text>
<Text>Line 8</Text>
<Text>Line 9</Text>
<Text>Line 10</Text>
</Scrollable>,
);
await waitUntilReady2();
const { unmount: unmount2 } = await renderWithProviders(
<Scrollable hasFocus={true} height={5} scrollToBottom={true}>
<Text>Line 1</Text>
<Text>Line 2</Text>
<Text>Line 3</Text>
<Text>Line 4</Text>
<Text>Line 5</Text>
<Text>Line 6</Text>
<Text>Line 7</Text>
<Text>Line 8</Text>
<Text>Line 9</Text>
<Text>Line 10</Text>
</Scrollable>,
);
await waitFor(() => {
expect(capturedEntry?.getScrollState().scrollTop).toBe(5);
});
@@ -197,14 +191,13 @@ describe('<Scrollable />', () => {
},
);
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
const { stdin, unmount, waitUntilReady } = await renderWithProviders(
<Scrollable hasFocus={true} height={5}>
<Box height={scrollHeight}>
<Text>Content</Text>
</Box>
</Scrollable>,
);
await waitUntilReady();
// Ensure initial state using existing scrollBy method
await act(async () => {
@@ -95,8 +95,7 @@ describe('SearchableList', () => {
};
it('should render all items initially', async () => {
const { lastFrame, waitUntilReady } = await renderList();
await waitUntilReady();
const { lastFrame } = await renderList();
const frame = lastFrame();
expect(frame).toContain('Test List');
@@ -109,10 +108,9 @@ describe('SearchableList', () => {
});
it('should reset selection to top when items change if resetSelectionOnItemsChange is true', async () => {
const { lastFrame, stdin, waitUntilReady } = await renderList({
const { lastFrame, stdin } = await renderList({
resetSelectionOnItemsChange: true,
});
await waitUntilReady();
await React.act(async () => {
stdin.write('\u001B[B'); // Down arrow
@@ -218,8 +216,7 @@ describe('SearchableList', () => {
});
it('should match snapshot', async () => {
const { lastFrame, waitUntilReady } = await renderList();
await waitUntilReady();
const { lastFrame } = await renderList();
expect(lastFrame()).toMatchSnapshot();
});
});
@@ -37,11 +37,10 @@ describe('<SectionHeader />', () => {
width: 40,
},
])('$description', async ({ title, subtitle, width }) => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<SectionHeader title={title} subtitle={subtitle} />,
{ width },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
@@ -12,21 +12,20 @@ import { describe, it, expect } from 'vitest';
describe('<SlicingMaxSizedBox />', () => {
it('renders string data without slicing when it fits', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<OverflowProvider>
<SlicingMaxSizedBox data="Hello World" maxWidth={80}>
{(truncatedData) => <Text>{truncatedData}</Text>}
</SlicingMaxSizedBox>
</OverflowProvider>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Hello World');
unmount();
});
it('slices string data by characters when very long', async () => {
const veryLongString = 'A'.repeat(25000);
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<OverflowProvider>
<SlicingMaxSizedBox
data={veryLongString}
@@ -37,7 +36,6 @@ describe('<SlicingMaxSizedBox />', () => {
</SlicingMaxSizedBox>
</OverflowProvider>,
);
await waitUntilReady();
// 20000 characters + 3 for '...'
expect(lastFrame()).toContain('20003');
unmount();
@@ -45,7 +43,7 @@ describe('<SlicingMaxSizedBox />', () => {
it('slices string data by lines when maxLines is provided', async () => {
const multilineString = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5';
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<OverflowProvider>
<SlicingMaxSizedBox
data={multilineString}
@@ -58,7 +56,6 @@ describe('<SlicingMaxSizedBox />', () => {
</SlicingMaxSizedBox>
</OverflowProvider>,
);
await waitUntilReady();
// maxLines=3, so it should keep 3-1 = 2 lines
expect(lastFrame()).toContain('Line 1');
expect(lastFrame()).toContain('Line 2');
@@ -71,7 +68,7 @@ describe('<SlicingMaxSizedBox />', () => {
it('slices array data when maxLines is provided', async () => {
const dataArray = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<OverflowProvider>
<SlicingMaxSizedBox
data={dataArray}
@@ -90,7 +87,6 @@ describe('<SlicingMaxSizedBox />', () => {
</SlicingMaxSizedBox>
</OverflowProvider>,
);
await waitUntilReady();
// maxLines=3, so it should keep 3-1 = 2 items
expect(lastFrame()).toContain('Item 1');
expect(lastFrame()).toContain('Item 2');
@@ -103,7 +99,7 @@ describe('<SlicingMaxSizedBox />', () => {
it('does not slice when isAlternateBuffer is true', async () => {
const multilineString = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5';
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<OverflowProvider>
<SlicingMaxSizedBox
data={multilineString}
@@ -115,7 +111,6 @@ describe('<SlicingMaxSizedBox />', () => {
</SlicingMaxSizedBox>
</OverflowProvider>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Line 5');
expect(lastFrame()).not.toContain('hidden');
unmount();
@@ -17,22 +17,20 @@ const MOCK_TABS: Tab[] = [
describe('TabHeader', () => {
describe('rendering', () => {
it('renders null for single tab', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader
tabs={[{ key: '0', header: 'Only Tab' }]}
currentIndex={0}
/>,
);
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toBe('');
unmount();
});
it('renders all tab headers', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('Tab 1');
expect(frame).toContain('Tab 2');
@@ -42,10 +40,9 @@ describe('TabHeader', () => {
});
it('renders separators between tabs', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
// Should have 2 separators for 3 tabs
const separatorCount = (frame?.match(/│/g) || []).length;
@@ -57,10 +54,9 @@ describe('TabHeader', () => {
describe('arrows', () => {
it('shows arrows by default', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('←');
expect(frame).toContain('→');
@@ -69,10 +65,9 @@ describe('TabHeader', () => {
});
it('hides arrows when showArrows is false', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} showArrows={false} />,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).not.toContain('←');
expect(frame).not.toContain('→');
@@ -83,10 +78,9 @@ describe('TabHeader', () => {
describe('status icons', () => {
it('shows status icons by default', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
// Default uncompleted icon is □
expect(frame).toContain('□');
@@ -95,10 +89,9 @@ describe('TabHeader', () => {
});
it('hides status icons when showStatusIcons is false', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={MOCK_TABS} currentIndex={0} showStatusIcons={false} />,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).not.toContain('□');
expect(frame).not.toContain('✓');
@@ -107,14 +100,13 @@ describe('TabHeader', () => {
});
it('shows checkmark for completed tabs', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader
tabs={MOCK_TABS}
currentIndex={0}
completedIndices={new Set([0, 2])}
/>,
);
await waitUntilReady();
const frame = lastFrame();
// Should have 2 checkmarks and 1 box
const checkmarkCount = (frame?.match(/✓/g) || []).length;
@@ -130,10 +122,9 @@ describe('TabHeader', () => {
{ key: '0', header: 'Tab 1' },
{ key: '1', header: 'Review', isSpecial: true },
];
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={tabsWithSpecial} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
// Special tab shows ≡ icon
expect(frame).toContain('≡');
@@ -146,10 +137,9 @@ describe('TabHeader', () => {
{ key: '0', header: 'Tab 1', statusIcon: '★' },
{ key: '1', header: 'Tab 2' },
];
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={tabsWithCustomIcon} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('★');
expect(frame).toMatchSnapshot();
@@ -158,14 +148,13 @@ describe('TabHeader', () => {
it('uses custom renderStatusIcon when provided', async () => {
const renderStatusIcon = () => '•';
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader
tabs={MOCK_TABS}
currentIndex={0}
renderStatusIcon={renderStatusIcon}
/>,
);
await waitUntilReady();
const frame = lastFrame();
const bulletCount = (frame?.match(/•/g) || []).length;
expect(bulletCount).toBe(3);
@@ -178,10 +167,9 @@ describe('TabHeader', () => {
{ key: '0', header: 'ThisIsAVeryLongHeaderThatShouldBeTruncated' },
{ key: '1', header: 'AnotherVeryLongHeader' },
];
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader tabs={longTabs} currentIndex={0} />,
);
await waitUntilReady();
const frame = lastFrame();
// Current tab (index 0) should NOT be truncated
@@ -197,14 +185,13 @@ describe('TabHeader', () => {
it('falls back to default when renderStatusIcon returns undefined', async () => {
const renderStatusIcon = () => undefined;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, unmount } = await renderWithProviders(
<TabHeader
tabs={MOCK_TABS}
currentIndex={0}
renderStatusIcon={renderStatusIcon}
/>,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('□');
expect(frame).toMatchSnapshot();
@@ -129,14 +129,13 @@ describe('TextInput', () => {
handleInput: vi.fn(),
setText: vi.fn(),
};
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<TextInput
buffer={buffer as unknown as TextBuffer}
onSubmit={onSubmit}
onCancel={onCancel}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('test');
unmount();
});
@@ -151,7 +150,7 @@ describe('TextInput', () => {
handleInput: vi.fn(),
setText: vi.fn(),
};
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<TextInput
buffer={buffer as unknown as TextBuffer}
placeholder="testing"
@@ -159,16 +158,14 @@ describe('TextInput', () => {
onCancel={onCancel}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('testing');
unmount();
});
it('handles character input', async () => {
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -197,10 +194,9 @@ describe('TextInput', () => {
it('handles backspace', async () => {
mockBuffer.setText('test');
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -229,10 +225,9 @@ describe('TextInput', () => {
it('handles left arrow', async () => {
mockBuffer.setText('test');
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -255,10 +250,9 @@ describe('TextInput', () => {
it('handles right arrow', async () => {
mockBuffer.setText('test');
mockBuffer.visualCursor[1] = 2; // Set initial cursor for right arrow test
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -279,10 +273,9 @@ describe('TextInput', () => {
it('calls onSubmit on return', async () => {
mockBuffer.setText('test');
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -306,10 +299,9 @@ describe('TextInput', () => {
const realContent = 'line1\nline2\nline3\nline4\nline5\nline6';
mockBuffer.setText(placeholder);
mockBuffer.pastedContent = { [placeholder]: realContent };
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -331,10 +323,9 @@ describe('TextInput', () => {
it('submits text unchanged when pastedContent is empty', async () => {
mockBuffer.setText('normal text');
mockBuffer.pastedContent = {};
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -355,10 +346,9 @@ describe('TextInput', () => {
it('calls onCancel on escape', async () => {
vi.useFakeTimers();
const { waitUntilReady, unmount } = render(
const { waitUntilReady, unmount } = await render(
<TextInput buffer={mockBuffer} onCancel={onCancel} onSubmit={onSubmit} />,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
await act(async () => {
@@ -385,17 +375,16 @@ describe('TextInput', () => {
it('renders the input value', async () => {
mockBuffer.setText('secret');
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('secret');
unmount();
});
it('does not show cursor when not focused', async () => {
mockBuffer.setText('test');
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<TextInput
buffer={mockBuffer}
focus={false}
@@ -403,7 +392,6 @@ describe('TextInput', () => {
onCancel={onCancel}
/>,
);
await waitUntilReady();
expect(lastFrame()).not.toContain('\u001b[7m'); // Inverse video chalk
unmount();
});
@@ -412,10 +400,9 @@ describe('TextInput', () => {
mockBuffer.text = 'line1\nline2';
mockBuffer.viewportVisualLines = ['line1', 'line2'];
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<TextInput buffer={mockBuffer} onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('line1');
expect(lastFrame()).toContain('line2');
@@ -59,7 +59,7 @@ describe('<VirtualizedList />', () => {
])(
'renders only visible items ($name)',
async ({ initialScrollIndex, visible, notVisible }) => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<Box height={10} width={100} borderStyle="round">
<VirtualizedList
data={longData}
@@ -70,22 +70,21 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
const frame = lastFrame();
const output = lastFrame();
visible.forEach((item) => {
expect(frame).toContain(item);
expect(output).toContain(item);
});
notVisible.forEach((item) => {
expect(frame).not.toContain(item);
expect(output).not.toContain(item);
});
expect(frame).toMatchSnapshot();
expect(output).toMatchSnapshot();
unmount();
},
);
it('sticks to bottom when new items added', async () => {
const { lastFrame, rerender, waitUntilReady, unmount } = render(
const { lastFrame, rerender, waitUntilReady, unmount } = await render(
<Box height={10} width={100} borderStyle="round">
<VirtualizedList
data={longData}
@@ -96,7 +95,6 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Item 99');
@@ -126,7 +124,7 @@ describe('<VirtualizedList />', () => {
it('scrolls down to show new items when requested via ref', async () => {
const ref = createRef<VirtualizedListRef<string>>();
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = await render(
<Box height={10} width={100} borderStyle="round">
<VirtualizedList
ref={ref}
@@ -137,7 +135,6 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Item 0');
@@ -180,7 +177,7 @@ describe('<VirtualizedList />', () => {
(_, i) => `Item ${i}`,
);
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<Box height={20} width={100} borderStyle="round">
<VirtualizedList
data={veryLongData}
@@ -193,7 +190,6 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
const frame = lastFrame();
expect(mountedCount).toBe(expectedMountedCount);
@@ -262,8 +258,9 @@ describe('<VirtualizedList />', () => {
return null;
};
const { lastFrame, waitUntilReady, unmount } = render(<TestComponent />);
await waitUntilReady();
const { lastFrame, unmount, waitUntilReady } = await render(
<TestComponent />,
);
// Initially, only Item 0 (height 10) fills the 10px viewport
expect(lastFrame()).toContain('Item 0');
@@ -295,7 +292,7 @@ describe('<VirtualizedList />', () => {
);
const keyExtractor = (item: string) => item;
const { waitUntilReady, unmount } = render(
const { unmount, waitUntilReady } = await render(
<Box height={10} width={100} borderStyle="round">
<VirtualizedList
ref={ref}
@@ -306,7 +303,6 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
expect(ref.current?.getScrollState().scrollTop).toBe(0);
@@ -335,7 +331,7 @@ describe('<VirtualizedList />', () => {
const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
// Use copy mode
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame, unmount } = await render(
<Box height={10} width={100}>
<VirtualizedList
data={longData}
@@ -350,7 +346,6 @@ describe('<VirtualizedList />', () => {
/>
</Box>,
);
await waitUntilReady();
// Item 50 should be visible
expect(lastFrame()).toContain('Item 50');
@@ -14,9 +14,9 @@ describe('text-buffer performance', () => {
vi.restoreAllMocks();
});
it('should handle pasting large amounts of text efficiently', () => {
it('should handle pasting large amounts of text efficiently', async () => {
const viewport = { width: 80, height: 24 };
const { result } = renderHook(() =>
const { result } = await renderHook(() =>
useTextBuffer({
viewport,
}),
@@ -39,7 +39,7 @@ describe('text-buffer performance', () => {
expect(duration).toBeLessThan(5000);
});
it('should handle character-by-character insertion in a large buffer efficiently', () => {
it('should handle character-by-character insertion in a large buffer efficiently', async () => {
const lines = 5000;
const initialText = Array.from(
{ length: lines },
@@ -47,7 +47,7 @@ describe('text-buffer performance', () => {
).join('\n');
const viewport = { width: 80, height: 24 };
const { result } = renderHook(() =>
const { result } = await renderHook(() =>
useTextBuffer({
initialText,
viewport,
File diff suppressed because it is too large Load Diff