summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/contexts/KeypressContext.test.tsx
diff options
context:
space:
mode:
authorDeepankar Sharma <[email protected]>2025-08-15 15:30:57 -0400
committerGitHub <[email protected]>2025-08-15 19:30:57 +0000
commitf5a5cdd9738ae8925d5336f060201d908500c7ef (patch)
tree4d7aad45cc0aae198fc959d9ba216463457af157 /packages/cli/src/ui/contexts/KeypressContext.test.tsx
parent2c07dc0757867a138b3832d781457ecf5d9afcf7 (diff)
fix(input): Handle numpad enter key in kitty protocol terminals (#6341)
Co-authored-by: Jacob Richman <[email protected]>
Diffstat (limited to 'packages/cli/src/ui/contexts/KeypressContext.test.tsx')
-rw-r--r--packages/cli/src/ui/contexts/KeypressContext.test.tsx307
1 files changed, 307 insertions, 0 deletions
diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
new file mode 100644
index 00000000..01630229
--- /dev/null
+++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
@@ -0,0 +1,307 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { renderHook, act } from '@testing-library/react';
+import { vi, Mock } from 'vitest';
+import {
+ KeypressProvider,
+ useKeypressContext,
+ Key,
+} from './KeypressContext.js';
+import { useStdin } from 'ink';
+import { EventEmitter } from 'events';
+import {
+ KITTY_KEYCODE_ENTER,
+ KITTY_KEYCODE_NUMPAD_ENTER,
+} from '../utils/platformConstants.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 the 'readline' module
+vi.mock('readline', () => {
+ const mockedReadline = {
+ createInterface: vi.fn().mockReturnValue({ close: vi.fn() }),
+ emitKeypressEvents: vi.fn(),
+ };
+ return {
+ ...mockedReadline,
+ default: mockedReadline,
+ };
+});
+
+class MockStdin extends EventEmitter {
+ isTTY = true;
+ setRawMode = vi.fn();
+ override on = this.addListener;
+ override removeListener = super.removeListener;
+ write = vi.fn();
+ resume = vi.fn();
+
+ // Helper to simulate a keypress event
+ pressKey(key: Partial<Key>) {
+ this.emit('keypress', null, key);
+ }
+
+ // Helper to simulate a kitty protocol sequence
+ sendKittySequence(sequence: string) {
+ // Kitty sequences come in multiple parts
+ // For example, ESC[13u comes as: ESC[ then 1 then 3 then u
+ // ESC[57414;2u comes as: ESC[ then 5 then 7 then 4 then 1 then 4 then ; then 2 then u
+ const escIndex = sequence.indexOf('\x1b[');
+ if (escIndex !== -1) {
+ // Send ESC[
+ this.emit('keypress', null, {
+ name: undefined,
+ sequence: '\x1b[',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ });
+
+ // Send the rest character by character
+ const rest = sequence.substring(escIndex + 2);
+ for (const char of rest) {
+ this.emit('keypress', null, {
+ name: /[a-zA-Z0-9]/.test(char) ? char : undefined,
+ sequence: char,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ });
+ }
+ }
+ }
+}
+
+describe('KeypressContext - Kitty Protocol', () => {
+ let stdin: MockStdin;
+ const mockSetRawMode = vi.fn();
+
+ const wrapper = ({
+ children,
+ kittyProtocolEnabled = true,
+ }: {
+ children: React.ReactNode;
+ kittyProtocolEnabled?: boolean;
+ }) => (
+ <KeypressProvider kittyProtocolEnabled={kittyProtocolEnabled}>
+ {children}
+ </KeypressProvider>
+ );
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ stdin = new MockStdin();
+ (useStdin as Mock).mockReturnValue({
+ stdin,
+ setRawMode: mockSetRawMode,
+ });
+ });
+
+ describe('Enter key handling', () => {
+ it('should recognize regular enter key (keycode 13) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for regular enter: ESC[13u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_ENTER}u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should recognize numpad enter key (keycode 57414) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter: ESC[57414u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER}u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with modifiers', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Shift (modifier 2): ESC[57414;2u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};2u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: false,
+ shift: true,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with Ctrl modifier', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};5u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: true,
+ meta: false,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should handle numpad enter with Alt modifier', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter with Alt (modifier 3): ESC[57414;3u
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER};3u`);
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ ctrl: false,
+ meta: true,
+ shift: false,
+ }),
+ );
+ });
+
+ it('should not process kitty sequences when kitty protocol is disabled', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: false }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for numpad enter
+ act(() => {
+ stdin.sendKittySequence(`\x1b[${KITTY_KEYCODE_NUMPAD_ENTER}u`);
+ });
+
+ // When kitty protocol is disabled, the sequence should be passed through
+ // as individual keypresses, not recognized as a single enter key
+ expect(keyHandler).not.toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'return',
+ kittyProtocol: true,
+ }),
+ );
+ });
+ });
+
+ describe('Escape key handling', () => {
+ it('should recognize escape key (keycode 27) in kitty protocol', async () => {
+ const keyHandler = vi.fn();
+
+ const { result } = renderHook(() => useKeypressContext(), {
+ wrapper: ({ children }) =>
+ wrapper({ children, kittyProtocolEnabled: true }),
+ });
+
+ act(() => {
+ result.current.subscribe(keyHandler);
+ });
+
+ // Send kitty protocol sequence for escape: ESC[27u
+ act(() => {
+ stdin.sendKittySequence('\x1b[27u');
+ });
+
+ expect(keyHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'escape',
+ kittyProtocol: true,
+ }),
+ );
+ });
+ });
+});