/**
* Unit tests for MobileTerminalShortcuts component
* These tests verify the terminal shortcuts bar functionality and responsive behavior
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MobileTerminalShortcuts } from '../../../src/components/views/terminal-view/mobile-terminal-shortcuts.tsx';
import type { StickyModifier } from '../../../src/components/views/terminal-view/sticky-modifier-keys.tsx';
// Mock the StickyModifierKeys component
vi.mock('../../../src/components/views/terminal-view/sticky-modifier-keys.tsx', () => ({
StickyModifierKeys: ({
activeModifier,
onModifierChange,
isConnected,
}: {
activeModifier: StickyModifier;
onModifierChange: (m: StickyModifier) => void;
isConnected: boolean;
}) => (
),
}));
/**
* Helper to get arrow button by direction using the Lucide icon class
*/
function getArrowButton(direction: 'up' | 'down' | 'left' | 'right'): HTMLButtonElement | null {
const iconClass = `lucide-arrow-${direction}`;
const svg = document.querySelector(`svg.${iconClass}`);
return (svg?.closest('button') as HTMLButtonElement) || null;
}
/**
* Creates default props for MobileTerminalShortcuts component
*/
function createDefaultProps(overrides: Partial = {}) {
return {
...defaultProps,
...overrides,
};
}
const defaultProps = {
onSendInput: vi.fn(),
isConnected: true,
activeModifier: null as StickyModifier,
onModifierChange: vi.fn(),
onCopy: vi.fn(),
onPaste: vi.fn(),
onSelectAll: vi.fn(),
onToggleSelectMode: vi.fn(),
isSelectMode: false,
};
describe('MobileTerminalShortcuts', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Rendering', () => {
it('should render the shortcuts bar with all buttons', () => {
render();
// Check for collapse button
expect(screen.getByTitle('Hide shortcuts')).toBeInTheDocument();
// Check for sticky modifier keys
expect(screen.getByTestId('sticky-modifier-keys')).toBeInTheDocument();
// Check for special keys
expect(screen.getByText('Esc')).toBeInTheDocument();
expect(screen.getByText('Tab')).toBeInTheDocument();
// Check for Ctrl shortcuts
expect(screen.getByText('^C')).toBeInTheDocument();
expect(screen.getByText('^Z')).toBeInTheDocument();
expect(screen.getByText('^B')).toBeInTheDocument();
// Check for arrow buttons via SVG icons
expect(getArrowButton('left')).not.toBeNull();
expect(getArrowButton('down')).not.toBeNull();
expect(getArrowButton('up')).not.toBeNull();
expect(getArrowButton('right')).not.toBeNull();
// Check for navigation keys
expect(screen.getByText('Del')).toBeInTheDocument();
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('End')).toBeInTheDocument();
});
it('should render clipboard action buttons when callbacks provided', () => {
render();
expect(screen.getByTitle('Select text')).toBeInTheDocument();
expect(screen.getByTitle('Select all')).toBeInTheDocument();
expect(screen.getByTitle('Copy selection')).toBeInTheDocument();
expect(screen.getByTitle('Paste from clipboard')).toBeInTheDocument();
});
it('should not render clipboard buttons when callbacks are not provided', () => {
render(
);
expect(screen.queryByTitle('Select text')).not.toBeInTheDocument();
expect(screen.queryByTitle('Select all')).not.toBeInTheDocument();
expect(screen.queryByTitle('Copy selection')).not.toBeInTheDocument();
expect(screen.queryByTitle('Paste from clipboard')).not.toBeInTheDocument();
});
it('should render in collapsed state when collapsed', () => {
render();
// Click collapse button
fireEvent.click(screen.getByTitle('Hide shortcuts'));
// Should show collapsed view
expect(screen.getByText('Shortcuts')).toBeInTheDocument();
expect(screen.getByTitle('Show shortcuts')).toBeInTheDocument();
expect(screen.queryByText('Esc')).not.toBeInTheDocument();
});
it('should expand when clicking show shortcuts button', () => {
render();
// Collapse first
fireEvent.click(screen.getByTitle('Hide shortcuts'));
expect(screen.queryByText('Esc')).not.toBeInTheDocument();
// Expand
fireEvent.click(screen.getByTitle('Show shortcuts'));
expect(screen.getByText('Esc')).toBeInTheDocument();
});
});
describe('Special Keys', () => {
it('should send Escape key when Esc button is pressed', () => {
const onSendInput = vi.fn();
render();
const escButton = screen.getByText('Esc');
fireEvent.pointerDown(escButton);
expect(onSendInput).toHaveBeenCalledWith('\x1b');
});
it('should send Tab key when Tab button is pressed', () => {
const onSendInput = vi.fn();
render();
const tabButton = screen.getByText('Tab');
fireEvent.pointerDown(tabButton);
expect(onSendInput).toHaveBeenCalledWith('\t');
});
it('should send Delete key when Del button is pressed', () => {
const onSendInput = vi.fn();
render();
const delButton = screen.getByText('Del');
fireEvent.pointerDown(delButton);
expect(onSendInput).toHaveBeenCalledWith('\x1b[3~');
});
it('should send Home key when Home button is pressed', () => {
const onSendInput = vi.fn();
render();
const homeButton = screen.getByText('Home');
fireEvent.pointerDown(homeButton);
expect(onSendInput).toHaveBeenCalledWith('\x1b[H');
});
it('should send End key when End button is pressed', () => {
const onSendInput = vi.fn();
render();
const endButton = screen.getByText('End');
fireEvent.pointerDown(endButton);
expect(onSendInput).toHaveBeenCalledWith('\x1b[F');
});
});
describe('Ctrl Key Shortcuts', () => {
it('should send Ctrl+C when ^C button is pressed', () => {
const onSendInput = vi.fn();
render();
const ctrlCButton = screen.getByText('^C');
fireEvent.pointerDown(ctrlCButton);
expect(onSendInput).toHaveBeenCalledWith('\x03');
});
it('should send Ctrl+Z when ^Z button is pressed', () => {
const onSendInput = vi.fn();
render();
const ctrlZButton = screen.getByText('^Z');
fireEvent.pointerDown(ctrlZButton);
expect(onSendInput).toHaveBeenCalledWith('\x1a');
});
it('should send Ctrl+B when ^B button is pressed', () => {
const onSendInput = vi.fn();
render();
const ctrlBButton = screen.getByText('^B');
fireEvent.pointerDown(ctrlBButton);
expect(onSendInput).toHaveBeenCalledWith('\x02');
});
});
describe('Arrow Keys', () => {
it('should send arrow up key when pressed', () => {
const onSendInput = vi.fn();
render();
const upButton = getArrowButton('up');
expect(upButton).not.toBeNull();
fireEvent.pointerDown(upButton!);
expect(onSendInput).toHaveBeenCalledWith('\x1b[A');
});
it('should send arrow down key when pressed', () => {
const onSendInput = vi.fn();
render();
const downButton = getArrowButton('down');
expect(downButton).not.toBeNull();
fireEvent.pointerDown(downButton!);
expect(onSendInput).toHaveBeenCalledWith('\x1b[B');
});
it('should send arrow right key when pressed', () => {
const onSendInput = vi.fn();
render();
const rightButton = getArrowButton('right');
expect(rightButton).not.toBeNull();
fireEvent.pointerDown(rightButton!);
expect(onSendInput).toHaveBeenCalledWith('\x1b[C');
});
it('should send arrow left key when pressed', () => {
const onSendInput = vi.fn();
render();
const leftButton = getArrowButton('left');
expect(leftButton).not.toBeNull();
fireEvent.pointerDown(leftButton!);
expect(onSendInput).toHaveBeenCalledWith('\x1b[D');
});
it('should send initial arrow key immediately on press', () => {
const onSendInput = vi.fn();
render();
const upButton = getArrowButton('up');
expect(upButton).not.toBeNull();
fireEvent.pointerDown(upButton!);
// Initial press should send immediately
expect(onSendInput).toHaveBeenCalledTimes(1);
expect(onSendInput).toHaveBeenCalledWith('\x1b[A');
// Release the button - should not send more
fireEvent.pointerUp(upButton!);
expect(onSendInput).toHaveBeenCalledTimes(1);
});
it('should stop repeating when pointer leaves button', () => {
const onSendInput = vi.fn();
render();
const upButton = getArrowButton('up');
expect(upButton).not.toBeNull();
// Press and release via pointer leave
fireEvent.pointerDown(upButton!);
expect(onSendInput).toHaveBeenCalledTimes(1);
// Pointer leaves - should clear repeat timers
fireEvent.pointerLeave(upButton!);
// Only the initial press should have been sent
expect(onSendInput).toHaveBeenCalledTimes(1);
});
});
describe('Clipboard Actions', () => {
it('should call onCopy when copy button is pressed', () => {
const onCopy = vi.fn();
render();
const copyButton = screen.getByTitle('Copy selection');
fireEvent.pointerDown(copyButton);
expect(onCopy).toHaveBeenCalledTimes(1);
});
it('should call onPaste when paste button is pressed', () => {
const onPaste = vi.fn();
render();
const pasteButton = screen.getByTitle('Paste from clipboard');
fireEvent.pointerDown(pasteButton);
expect(onPaste).toHaveBeenCalledTimes(1);
});
it('should call onSelectAll when select all button is pressed', () => {
const onSelectAll = vi.fn();
render();
const selectAllButton = screen.getByTitle('Select all');
fireEvent.pointerDown(selectAllButton);
expect(onSelectAll).toHaveBeenCalledTimes(1);
});
it('should call onToggleSelectMode when select mode button is pressed', () => {
const onToggleSelectMode = vi.fn();
render();
const selectModeButton = screen.getByTitle('Select text');
fireEvent.pointerDown(selectModeButton);
expect(onToggleSelectMode).toHaveBeenCalledTimes(1);
});
it('should show active state when in select mode', () => {
render();
const selectModeButton = screen.getByTitle('Exit select mode');
expect(selectModeButton).toBeInTheDocument();
});
});
describe('Connection State', () => {
it('should disable all buttons when not connected', () => {
const onSendInput = vi.fn();
render(
);
// All shortcut buttons should not send input when disabled
const escButton = screen.getByText('Esc');
fireEvent.pointerDown(escButton);
expect(onSendInput).not.toHaveBeenCalled();
// Arrow keys should also be disabled
const upButton = getArrowButton('up');
expect(upButton).not.toBeNull();
fireEvent.pointerDown(upButton!);
expect(onSendInput).not.toHaveBeenCalled();
});
it('should pass connected state to StickyModifierKeys', () => {
render();
const stickyKeys = screen.getByTestId('sticky-modifier-keys');
expect(stickyKeys).toHaveAttribute('data-connected', 'false');
});
});
describe('Sticky Modifier Integration', () => {
it('should pass active modifier to StickyModifierKeys', () => {
render();
const stickyKeys = screen.getByTestId('sticky-modifier-keys');
expect(stickyKeys).toHaveAttribute('data-modifier', 'ctrl');
});
it('should call onModifierChange when modifier is changed', () => {
const onModifierChange = vi.fn();
render();
const ctrlBtn = screen.getByTestId('ctrl-btn');
fireEvent.click(ctrlBtn);
expect(onModifierChange).toHaveBeenCalledWith('ctrl');
});
});
});