Download packages/cli/src/ui/components/shared/VirtualizedList.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
-
curl -L -o VirtualizedList.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
10.4 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { render } from '../../../test-utils/render.js'; | |
| import { waitFor } from '../../../test-utils/async.js'; | |
| import { VirtualizedList, type VirtualizedListRef } from './VirtualizedList.js'; | |
| import { Text, Box } from 'ink'; | |
| import { | |
| createRef, | |
| act, | |
| useEffect, | |
| createContext, | |
| useContext, | |
| useState, | |
| } from 'react'; | |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | |
| describe('<VirtualizedList />', () => { | |
| const keyExtractor = (item: string) => item; | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| describe('with 10px height and 100 items', () => { | |
| const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`); | |
| // We use 1px for items. Container is 10px. | |
| // Viewport shows 10 items. Overscan adds 10 items. | |
| const itemHeight = 1; | |
| const renderItem1px = ({ item }: { item: string }) => ( | |
| <Box height={itemHeight}> | |
| <Text>{item}</Text> | |
| </Box> | |
| ); | |
| it.each([ | |
| { | |
| name: 'top', | |
| initialScrollIndex: undefined, | |
| visible: ['Item 0', 'Item 7'], | |
| notVisible: ['Item 8', 'Item 15', 'Item 50', 'Item 99'], | |
| }, | |
| { | |
| name: 'scrolled to bottom', | |
| initialScrollIndex: 99, | |
| visible: ['Item 99', 'Item 92'], | |
| notVisible: ['Item 91', 'Item 85', 'Item 50', 'Item 0'], | |
| }, | |
| ])( | |
| 'renders only visible items ($name)', | |
| async ({ initialScrollIndex, visible, notVisible }) => { | |
| const { lastFrame, unmount } = await render( | |
| <Box height={10} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| data={longData} | |
| renderItem={renderItem1px} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => itemHeight} | |
| initialScrollIndex={initialScrollIndex} | |
| /> | |
| </Box>, | |
| ); | |
| const output = lastFrame(); | |
| visible.forEach((item) => { | |
| expect(output).toContain(item); | |
| }); | |
| notVisible.forEach((item) => { | |
| expect(output).not.toContain(item); | |
| }); | |
| expect(output).toMatchSnapshot(); | |
| unmount(); | |
| }, | |
| ); | |
| it('sticks to bottom when new items added', async () => { | |
| const { lastFrame, rerender, waitUntilReady, unmount } = await render( | |
| <Box height={10} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| data={longData} | |
| renderItem={renderItem1px} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => itemHeight} | |
| initialScrollIndex={99} | |
| /> | |
| </Box>, | |
| ); | |
| expect(lastFrame()).toContain('Item 99'); | |
| // Add items | |
| const newData = [...longData, 'Item 100', 'Item 101']; | |
| await act(async () => { | |
| rerender( | |
| <Box height={10} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| data={newData} | |
| renderItem={renderItem1px} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => itemHeight} | |
| // We don't need to pass initialScrollIndex again for it to stick, | |
| // but passing it doesn't hurt. The component should auto-stick because it was at bottom. | |
| /> | |
| </Box>, | |
| ); | |
| }); | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| expect(frame).toContain('Item 101'); | |
| expect(frame).not.toContain('Item 0'); | |
| unmount(); | |
| }); | |
| it('scrolls down to show new items when requested via ref', async () => { | |
| const ref = createRef<VirtualizedListRef<string>>(); | |
| const { lastFrame, waitUntilReady, unmount } = await render( | |
| <Box height={10} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| ref={ref} | |
| data={longData} | |
| renderItem={renderItem1px} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => itemHeight} | |
| /> | |
| </Box>, | |
| ); | |
| expect(lastFrame()).toContain('Item 0'); | |
| // Scroll to bottom via ref | |
| await act(async () => { | |
| ref.current?.scrollToEnd(); | |
| }); | |
| await waitUntilReady(); | |
| const frame = lastFrame(); | |
| expect(frame).toContain('Item 99'); | |
| unmount(); | |
| }); | |
| it.each([ | |
| { initialScrollIndex: 0, expectedMountedCount: 5 }, | |
| { initialScrollIndex: 500, expectedMountedCount: 6 }, | |
| { initialScrollIndex: 999, expectedMountedCount: 5 }, | |
| ])( | |
| 'mounts only visible items with 1000 items and 10px height (scroll: $initialScrollIndex)', | |
| async ({ initialScrollIndex, expectedMountedCount }) => { | |
| let mountedCount = 0; | |
| const tallItemHeight = 5; | |
| const ItemWithEffect = ({ item }: { item: string }) => { | |
| useEffect(() => { | |
| mountedCount++; | |
| return () => { | |
| mountedCount--; | |
| }; | |
| }, []); | |
| return ( | |
| <Box height={tallItemHeight}> | |
| <Text>{item}</Text> | |
| </Box> | |
| ); | |
| }; | |
| const veryLongData = Array.from( | |
| { length: 1000 }, | |
| (_, i) => `Item ${i}`, | |
| ); | |
| const { lastFrame, unmount } = await render( | |
| <Box height={20} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| data={veryLongData} | |
| renderItem={({ item }) => ( | |
| <ItemWithEffect key={item} item={item} /> | |
| )} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => tallItemHeight} | |
| initialScrollIndex={initialScrollIndex} | |
| /> | |
| </Box>, | |
| ); | |
| const frame = lastFrame(); | |
| expect(mountedCount).toBe(expectedMountedCount); | |
| expect(frame).toMatchSnapshot(); | |
| unmount(); | |
| }, | |
| ); | |
| }); | |
| it('renders more items when a visible item shrinks via context update', async () => { | |
| const SizeContext = createContext<{ | |
| firstItemHeight: number; | |
| setFirstItemHeight: (h: number) => void; | |
| }>({ | |
| firstItemHeight: 10, | |
| setFirstItemHeight: () => {}, | |
| }); | |
| const items = Array.from({ length: 20 }, (_, i) => ({ | |
| id: `Item ${i}`, | |
| })); | |
| const ItemWithContext = ({ | |
| item, | |
| index, | |
| }: { | |
| item: { id: string }; | |
| index: number; | |
| }) => { | |
| const { firstItemHeight } = useContext(SizeContext); | |
| const height = index === 0 ? firstItemHeight : 1; | |
| return ( | |
| <Box height={height}> | |
| <Text>{item.id}</Text> | |
| </Box> | |
| ); | |
| }; | |
| const TestComponent = () => { | |
| const [firstItemHeight, setFirstItemHeight] = useState(10); | |
| return ( | |
| <SizeContext.Provider value={{ firstItemHeight, setFirstItemHeight }}> | |
| <Box height={10} width={100}> | |
| <VirtualizedList | |
| data={items} | |
| renderItem={({ item, index }) => ( | |
| <ItemWithContext item={item} index={index} /> | |
| )} | |
| keyExtractor={(item) => item.id} | |
| estimatedItemHeight={() => 1} | |
| /> | |
| </Box> | |
| {/* Expose setter for testing */} | |
| <TestControl setFirstItemHeight={setFirstItemHeight} /> | |
| </SizeContext.Provider> | |
| ); | |
| }; | |
| let setHeightFn: (h: number) => void = () => {}; | |
| const TestControl = ({ | |
| setFirstItemHeight, | |
| }: { | |
| setFirstItemHeight: (h: number) => void; | |
| }) => { | |
| setHeightFn = setFirstItemHeight; | |
| return null; | |
| }; | |
| const { lastFrame, unmount, waitUntilReady } = await render( | |
| <TestComponent />, | |
| ); | |
| // Initially, only Item 0 (height 10) fills the 10px viewport | |
| expect(lastFrame()).toContain('Item 0'); | |
| expect(lastFrame()).not.toContain('Item 1'); | |
| // Shrink Item 0 to 1px via context | |
| await act(async () => { | |
| setHeightFn(1); | |
| }); | |
| await waitUntilReady(); | |
| // Now Item 0 is 1px, so Items 1-9 should also be visible to fill 10px | |
| await waitFor(() => { | |
| expect(lastFrame()).toContain('Item 0'); | |
| expect(lastFrame()).toContain('Item 1'); | |
| expect(lastFrame()).toContain('Item 9'); | |
| }); | |
| unmount(); | |
| }); | |
| it('updates scroll position correctly when scrollBy is called multiple times in the same tick', async () => { | |
| const ref = createRef<VirtualizedListRef<string>>(); | |
| const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`); | |
| const itemHeight = 1; | |
| const renderItem1px = ({ item }: { item: string }) => ( | |
| <Box height={itemHeight}> | |
| <Text>{item}</Text> | |
| </Box> | |
| ); | |
| const keyExtractor = (item: string) => item; | |
| const { unmount, waitUntilReady } = await render( | |
| <Box height={10} width={100} borderStyle="round"> | |
| <VirtualizedList | |
| ref={ref} | |
| data={longData} | |
| renderItem={renderItem1px} | |
| keyExtractor={keyExtractor} | |
| estimatedItemHeight={() => itemHeight} | |
| /> | |
| </Box>, | |
| ); | |
| expect(ref.current?.getScrollState().scrollTop).toBe(0); | |
| await act(async () => { | |
| ref.current?.scrollBy(1); | |
| ref.current?.scrollBy(1); | |
| }); | |
| await waitUntilReady(); | |
| expect(ref.current?.getScrollState().scrollTop).toBe(2); | |
| await act(async () => { | |
| ref.current?.scrollBy(2); | |
| }); | |
| await waitUntilReady(); | |
| expect(ref.current?.getScrollState().scrollTop).toBe(4); | |
| unmount(); | |
| }); | |
| it('renders correctly in copyModeEnabled when scrolled', async () => { | |
| const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`); | |
| // Use copy mode | |
| const { lastFrame, unmount } = await render( | |
| <Box height={10} width={100}> | |
| <VirtualizedList | |
| data={longData} | |
| renderItem={({ item }) => ( | |
| <Box height={1}> | |
| <Text>{item}</Text> | |
| </Box> | |
| )} | |
| keyExtractor={(item) => item} | |
| estimatedItemHeight={() => 1} | |
| initialScrollIndex={50} | |
| copyModeEnabled={true} | |
| /> | |
| </Box>, | |
| ); | |
| // Item 50 should be visible | |
| expect(lastFrame()).toContain('Item 50'); | |
| // And surrounding items | |
| expect(lastFrame()).toContain('Item 59'); | |
| // But far away items should not be (ensures we are actually scrolled) | |
| expect(lastFrame()).not.toContain('Item 0'); | |
| unmount(); | |
| }); | |
| }); | |