Download packages/cli/src/ui/contexts/ScrollProvider.drag.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/contexts/ScrollProvider.drag.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/contexts/ScrollProvider.drag.test.tsx
-
curl -L -o ScrollProvider.drag.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/contexts/ScrollProvider.drag.test.tsx
10.8 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { render } from '../../test-utils/render.js'; | |
| import { | |
| ScrollProvider, | |
| useScrollable, | |
| type ScrollState, | |
| } from './ScrollProvider.js'; | |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
| import { useRef, useImperativeHandle, forwardRef, type RefObject } from 'react'; | |
| import { Box, type DOMElement } from 'ink'; | |
| import type { MouseEvent } from '../hooks/useMouse.js'; | |
| // Mock useMouse hook | |
| const mockUseMouseCallbacks = new Set<(event: MouseEvent) => void>(); | |
| vi.mock('../hooks/useMouse.js', async () => { | |
| // We need to import React dynamically because this factory runs before top-level imports | |
| const React = await import('react'); | |
| return { | |
| useMouse: (callback: (event: MouseEvent) => void) => { | |
| React.useEffect(() => { | |
| mockUseMouseCallbacks.add(callback); | |
| return () => { | |
| mockUseMouseCallbacks.delete(callback); | |
| }; | |
| }, [callback]); | |
| }, | |
| }; | |
| }); | |
| // Mock ink's getBoundingBox | |
| vi.mock('ink', async (importOriginal) => { | |
| const actual = await importOriginal<typeof import('ink')>(); | |
| return { | |
| ...actual, | |
| getBoundingBox: vi.fn(() => ({ x: 0, y: 0, width: 10, height: 10 })), | |
| }; | |
| }); | |
| const TestScrollable = forwardRef( | |
| ( | |
| props: { | |
| id: string; | |
| scrollBy: (delta: number) => void; | |
| getScrollState: () => ScrollState; | |
| }, | |
| ref, | |
| ) => { | |
| const elementRef = useRef<DOMElement>(null); | |
| useImperativeHandle(ref, () => elementRef.current); | |
| useScrollable( | |
| { | |
| ref: elementRef as RefObject<DOMElement>, | |
| getScrollState: props.getScrollState, | |
| scrollBy: props.scrollBy, | |
| hasFocus: () => true, | |
| flashScrollbar: () => {}, | |
| }, | |
| true, | |
| ); | |
| return <Box ref={elementRef} />; | |
| }, | |
| ); | |
| TestScrollable.displayName = 'TestScrollable'; | |
| describe('ScrollProvider Drag', () => { | |
| beforeEach(() => { | |
| vi.useFakeTimers(); | |
| mockUseMouseCallbacks.clear(); | |
| }); | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| }); | |
| it('drags the scrollbar thumb', async () => { | |
| const scrollBy = vi.fn(); | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 0, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollable | |
| id="test-scrollable" | |
| scrollBy={scrollBy} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Scrollbar at x + width = 10. | |
| // Height 10. | |
| // scrollHeight 100, innerHeight 10. | |
| // thumbHeight = 1. | |
| // maxScrollTop = 90. maxThumbY = 9. Ratio = 10. | |
| // Thumb at 0. | |
| // 1. Click on thumb (row 0) | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 0, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // 2. Move mouse to row 1 | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'move', | |
| col: 10, // col doesn't matter for move if dragging | |
| row: 1, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Delta row = 1. Delta scroll = 10. | |
| // scrollBy called with 10. | |
| expect(scrollBy).toHaveBeenCalledWith(10); | |
| // 3. Move mouse to row 2 | |
| scrollBy.mockClear(); | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'move', | |
| col: 10, | |
| row: 2, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Delta row from start (0) is 2. Delta scroll = 20. | |
| // startScrollTop was 0. target 20. | |
| // scrollBy called with (20 - scrollTop). scrollTop is still 0 in mock. | |
| expect(scrollBy).toHaveBeenCalledWith(20); | |
| // 4. Release | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-release', | |
| col: 10, | |
| row: 2, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // 5. Move again - should not scroll | |
| scrollBy.mockClear(); | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'move', | |
| col: 10, | |
| row: 3, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'none', | |
| }); | |
| } | |
| expect(scrollBy).not.toHaveBeenCalled(); | |
| }); | |
| it('jumps to position and starts drag when clicking track below thumb', async () => { | |
| const scrollBy = vi.fn(); | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 0, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollable | |
| id="test-scrollable" | |
| scrollBy={scrollBy} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Thumb at 0. Click at 5. | |
| // thumbHeight 1. | |
| // targetThumbY = 5. | |
| // targetScrollTop = 50. | |
| // 1. Click on track below thumb | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 5, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Should jump to 50 (delta 50) | |
| expect(scrollBy).toHaveBeenCalledWith(50); | |
| scrollBy.mockClear(); | |
| // 2. Move mouse to 6 - should drag | |
| // Start drag captured at row 5, startScrollTop 50. | |
| // Move to 6. Delta row 1. Delta scroll 10. | |
| // Target = 60. | |
| // scrollBy called with 60 - 0 (current state still 0). | |
| // Note: In real app, state would update, but here getScrollState is static mock 0. | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'move', | |
| col: 10, | |
| row: 6, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| expect(scrollBy).toHaveBeenCalledWith(60); | |
| }); | |
| it('jumps to position when clicking track above thumb', async () => { | |
| const scrollBy = vi.fn(); | |
| // Start scrolled down | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 50, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollable | |
| id="test-scrollable" | |
| scrollBy={scrollBy} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Thumb at 5. Click at 2. | |
| // targetThumbY = 2. | |
| // targetScrollTop = 20. | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 2, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Jump to 20 (delta = 20 - 50 = -30) | |
| expect(scrollBy).toHaveBeenCalledWith(-30); | |
| }); | |
| it('jumps to top when clicking very top of track', async () => { | |
| const scrollBy = vi.fn(); | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 50, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollable | |
| id="test-scrollable" | |
| scrollBy={scrollBy} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Thumb at 5. Click at 0. | |
| // targetThumbY = 0. | |
| // targetScrollTop = 0. | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 0, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Scroll to top (delta = 0 - 50 = -50) | |
| expect(scrollBy).toHaveBeenCalledWith(-50); | |
| }); | |
| it('jumps to bottom when clicking very bottom of track', async () => { | |
| const scrollBy = vi.fn(); | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 0, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollable | |
| id="test-scrollable" | |
| scrollBy={scrollBy} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Thumb at 0. Click at 9. | |
| // targetThumbY = 9. | |
| // targetScrollTop = 90. | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 9, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Scroll to bottom (delta = 90 - 0 = 90) | |
| expect(scrollBy).toHaveBeenCalledWith(90); | |
| }); | |
| it('uses scrollTo with 0 duration if provided', async () => { | |
| const scrollBy = vi.fn(); | |
| const scrollTo = vi.fn(); | |
| const getScrollState = vi.fn(() => ({ | |
| scrollTop: 0, | |
| scrollHeight: 100, | |
| innerHeight: 10, | |
| })); | |
| // Custom component that provides scrollTo | |
| const TestScrollableWithScrollTo = forwardRef( | |
| ( | |
| props: { | |
| id: string; | |
| scrollBy: (delta: number) => void; | |
| scrollTo: (scrollTop: number, duration?: number) => void; | |
| getScrollState: () => ScrollState; | |
| }, | |
| ref, | |
| ) => { | |
| const elementRef = useRef<DOMElement>(null); | |
| useImperativeHandle(ref, () => elementRef.current); | |
| useScrollable( | |
| { | |
| ref: elementRef as RefObject<DOMElement>, | |
| getScrollState: props.getScrollState, | |
| scrollBy: props.scrollBy, | |
| scrollTo: props.scrollTo, | |
| hasFocus: () => true, | |
| flashScrollbar: () => {}, | |
| }, | |
| true, | |
| ); | |
| return <Box ref={elementRef} />; | |
| }, | |
| ); | |
| TestScrollableWithScrollTo.displayName = 'TestScrollableWithScrollTo'; | |
| await render( | |
| <ScrollProvider> | |
| <TestScrollableWithScrollTo | |
| id="test-scrollable-scrollto" | |
| scrollBy={scrollBy} | |
| scrollTo={scrollTo} | |
| getScrollState={getScrollState} | |
| /> | |
| </ScrollProvider>, | |
| ); | |
| // Click on track (jump) | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'left-press', | |
| col: 10, | |
| row: 5, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Expect scrollTo to be called with target (and undefined/default duration) | |
| expect(scrollTo).toHaveBeenCalledWith(50); | |
| scrollTo.mockClear(); | |
| // Move mouse (drag) | |
| for (const callback of mockUseMouseCallbacks) { | |
| callback({ | |
| name: 'move', | |
| col: 10, | |
| row: 6, | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| button: 'left', | |
| }); | |
| } | |
| // Expect scrollTo to be called with target and duration 0 | |
| expect(scrollTo).toHaveBeenCalledWith(60, 0); | |
| }); | |
| }); | |