Download packages/cli/src/ui/contexts/MouseContext.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/contexts/MouseContext.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/contexts/MouseContext.test.tsx
-
curl -L -o MouseContext.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/contexts/MouseContext.test.tsx
8.01 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { renderHookWithProviders } from '../../test-utils/render.js'; | |
| import { act } from 'react'; | |
| import { useMouseContext, useMouse } from './MouseContext.js'; | |
| import { vi, type Mock } from 'vitest'; | |
| import { useStdin } from 'ink'; | |
| import { EventEmitter } from 'node:events'; | |
| import { appEvents, AppEvent } from '../../utils/events.js'; | |
| // Mock the 'ink' module to control stdin | |
| vi.mock('ink', async (importOriginal) => { | |
| const original = await importOriginal<typeof import('ink')>(); | |
| return { | |
| ...original, | |
| useStdin: vi.fn(), | |
| }; | |
| }); | |
| // Mock appEvents | |
| vi.mock('../../utils/events.js', () => ({ | |
| appEvents: { | |
| emit: vi.fn(), | |
| on: vi.fn(), | |
| off: vi.fn(), | |
| }, | |
| AppEvent: { | |
| SelectionWarning: 'selection-warning', | |
| }, | |
| })); | |
| class MockStdin extends EventEmitter { | |
| isTTY = true; | |
| setRawMode = vi.fn(); | |
| override on = this.addListener; | |
| override removeListener = super.removeListener; | |
| resume = vi.fn(); | |
| pause = vi.fn(); | |
| write(text: string) { | |
| this.emit('data', text); | |
| } | |
| } | |
| describe('MouseContext', () => { | |
| let stdin: MockStdin; | |
| beforeEach(() => { | |
| stdin = new MockStdin(); | |
| (useStdin as Mock).mockReturnValue({ | |
| stdin, | |
| setRawMode: vi.fn(), | |
| }); | |
| vi.mocked(appEvents.emit).mockClear(); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| it('should subscribe and unsubscribe a handler', async () => { | |
| const handler = vi.fn(); | |
| const { result } = await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| result.current.subscribe(handler); | |
| }); | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| expect(handler).toHaveBeenCalledTimes(1); | |
| act(() => { | |
| result.current.unsubscribe(handler); | |
| }); | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| expect(handler).toHaveBeenCalledTimes(1); | |
| }); | |
| it('should not call handler if not active', async () => { | |
| const handler = vi.fn(); | |
| await renderHookWithProviders( | |
| () => useMouse(handler, { isActive: false }), | |
| { | |
| mouseEventsEnabled: true, | |
| }, | |
| ); | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| expect(handler).not.toHaveBeenCalled(); | |
| }); | |
| it('should emit SelectionWarning when move event is unhandled and has coordinates', async () => { | |
| await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| // Move event (32) at 10, 20 | |
| stdin.write('\x1b[<32;10;20M'); | |
| }); | |
| expect(appEvents.emit).toHaveBeenCalledWith(AppEvent.SelectionWarning); | |
| }); | |
| it('should not emit SelectionWarning when move event is handled', async () => { | |
| const handler = vi.fn().mockReturnValue(true); | |
| const { result } = await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| result.current.subscribe(handler); | |
| }); | |
| act(() => { | |
| // Move event (32) at 10, 20 | |
| stdin.write('\x1b[<32;10;20M'); | |
| }); | |
| expect(handler).toHaveBeenCalled(); | |
| expect(appEvents.emit).not.toHaveBeenCalled(); | |
| }); | |
| describe('SGR Mouse Events', () => { | |
| it.each([ | |
| { | |
| sequence: '\x1b[<0;10;20M', | |
| expected: { | |
| name: 'left-press', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<0;10;20m', | |
| expected: { | |
| name: 'left-release', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<2;10;20M', | |
| expected: { | |
| name: 'right-press', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<1;10;20M', | |
| expected: { | |
| name: 'middle-press', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<64;10;20M', | |
| expected: { | |
| name: 'scroll-up', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<65;10;20M', | |
| expected: { | |
| name: 'scroll-down', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<32;10;20M', | |
| expected: { | |
| name: 'move', | |
| shift: false, | |
| ctrl: false, | |
| meta: false, | |
| }, | |
| }, | |
| { | |
| sequence: '\x1b[<4;10;20M', | |
| expected: { name: 'left-press', shift: true }, | |
| }, // Shift + left press | |
| { | |
| sequence: '\x1b[<8;10;20M', | |
| expected: { name: 'left-press', meta: true }, | |
| }, // Alt + left press | |
| { | |
| sequence: '\x1b[<20;10;20M', | |
| expected: { name: 'left-press', shift: true, ctrl: true }, | |
| }, // Ctrl + Shift + left press | |
| { | |
| sequence: '\x1b[<68;10;20M', | |
| expected: { name: 'scroll-up', shift: true }, | |
| }, // Shift + scroll up | |
| ])( | |
| 'should recognize sequence "$sequence" as $expected.name', | |
| async ({ sequence, expected }) => { | |
| const mouseHandler = vi.fn(); | |
| const { result } = await renderHookWithProviders( | |
| () => useMouseContext(), | |
| { | |
| mouseEventsEnabled: true, | |
| }, | |
| ); | |
| act(() => result.current.subscribe(mouseHandler)); | |
| act(() => stdin.write(sequence)); | |
| expect(mouseHandler).toHaveBeenCalledWith( | |
| expect.objectContaining({ ...expected }), | |
| ); | |
| }, | |
| ); | |
| }); | |
| it('should emit a double-click event when two left-presses occur quickly at the same position', async () => { | |
| const handler = vi.fn(); | |
| const { result } = await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| result.current.subscribe(handler); | |
| }); | |
| // First click | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| expect(handler).toHaveBeenCalledTimes(1); | |
| expect(handler).toHaveBeenLastCalledWith( | |
| expect.objectContaining({ name: 'left-press', col: 10, row: 20 }), | |
| ); | |
| // Second click (within threshold) | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| // Should have called for the second left-press AND the double-click | |
| expect(handler).toHaveBeenCalledTimes(3); | |
| expect(handler).toHaveBeenCalledWith( | |
| expect.objectContaining({ name: 'double-click', col: 10, row: 20 }), | |
| ); | |
| }); | |
| it('should NOT emit a double-click event if clicks are too far apart', async () => { | |
| const handler = vi.fn(); | |
| const { result } = await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| result.current.subscribe(handler); | |
| }); | |
| // First click | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| // Second click (too far) | |
| act(() => { | |
| stdin.write('\x1b[<0;15;25M'); | |
| }); | |
| expect(handler).toHaveBeenCalledTimes(2); | |
| expect(handler).not.toHaveBeenCalledWith( | |
| expect.objectContaining({ name: 'double-click' }), | |
| ); | |
| }); | |
| it('should NOT emit a double-click event if too much time passes', async () => { | |
| vi.useFakeTimers(); | |
| const handler = vi.fn(); | |
| const { result } = await renderHookWithProviders(() => useMouseContext(), { | |
| mouseEventsEnabled: true, | |
| }); | |
| act(() => { | |
| result.current.subscribe(handler); | |
| }); | |
| // First click | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| await act(async () => { | |
| vi.advanceTimersByTime(500); // Threshold is 400ms | |
| }); | |
| // Second click | |
| act(() => { | |
| stdin.write('\x1b[<0;10;20M'); | |
| }); | |
| expect(handler).toHaveBeenCalledTimes(2); | |
| expect(handler).not.toHaveBeenCalledWith( | |
| expect.objectContaining({ name: 'double-click' }), | |
| ); | |
| vi.useRealTimers(); | |
| }); | |
| }); | |