import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { ThemeProvider } from '@mui/material/styles';
import { lightTheme } from '@services/theme/defaultTheme';
import { KeyboardShortcutRegister } from '../KeyboardShortcutRegister';
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
);
describe('KeyboardShortcutRegister Component', () => {
let mockOnChange: ReturnType;
beforeEach(() => {
vi.clearAllMocks();
mockOnChange = vi.fn();
// Mock window.service.context.get
Object.defineProperty(window.service.context, 'get', {
value: vi.fn().mockImplementation((key: string) => {
if (key === 'platform') {
// Return platform based on process.platform for testing
return Promise.resolve(process.platform === 'darwin' ? 'darwin' : 'win32');
}
return Promise.resolve('win32');
}),
writable: true,
});
});
const renderComponent = (overrides: {
value?: string;
onChange?: (value: string) => void;
label?: string;
disabled?: boolean;
} = {}) => {
const defaultProps = {
value: '',
onChange: mockOnChange,
label: 'Test Shortcut',
disabled: false,
...overrides,
};
return render(
,
);
};
describe('Initial state and rendering', () => {
it('should display button with label and current shortcut value', () => {
renderComponent({
value: 'Ctrl+Shift+T',
label: 'Toggle Window',
});
const button = screen.getByTestId('shortcut-register-button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Toggle Window');
expect(button).toHaveTextContent('Ctrl+Shift+T');
});
it('should display "None" when no shortcut is set', () => {
renderComponent({
value: '',
label: 'Toggle Window',
});
const button = screen.getByTestId('shortcut-register-button');
expect(button).toHaveTextContent('KeyboardShortcut.None');
});
it('should respect disabled state', () => {
renderComponent({
disabled: true,
});
const button = screen.getByTestId('shortcut-register-button');
expect(button).toBeDisabled();
});
it('should not show dialog initially', () => {
renderComponent();
expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
});
});
describe('Dialog interaction', () => {
it('should open dialog when button is clicked', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
expect(screen.getByText('KeyboardShortcut.RegisterShortcut')).toBeInTheDocument();
});
it('should display current shortcut in dialog initially', async () => {
const user = userEvent.setup();
renderComponent({
value: 'Ctrl+A',
});
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+A');
});
});
it('should close dialog when cancel button is clicked', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
// Press ESC to close dialog (MUI Dialog default behavior)
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
await waitFor(() => {
expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
}, { timeout: 500 });
});
});
describe('Keyboard shortcut capture', () => {
it('should capture Ctrl key combination', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
// Simulate keyboard event with Ctrl+Shift+T
fireEvent.keyDown(dialogContent, {
key: 'T',
ctrlKey: true,
shiftKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+Shift+T');
});
});
it('should capture Cmd key combination on macOS', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
// Simulate Cmd+K key press on document
fireEvent.keyDown(document, {
key: 'K',
metaKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Cmd+K');
});
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});
it('should capture Alt key combination', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
fireEvent.keyDown(dialogContent, {
key: 'F4',
altKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Alt+F4');
});
});
it('should ignore modifier keys pressed alone', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
const display = screen.getByTestId('shortcut-display');
// Should show initial "Press keys" message
expect(display).toHaveTextContent('KeyboardShortcut.PressKeys');
const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta'];
for (const key of modifierKeys) {
fireEvent.keyDown(dialogContent, {
key,
bubbles: true,
});
}
// Should still show "Press keys" message
expect(display).toHaveTextContent('KeyboardShortcut.PressKeys');
});
it('should update display when keys are pressed', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
// Press first combination
fireEvent.keyDown(dialogContent, {
key: 'A',
ctrlKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+A');
});
// Press second combination - should replace
fireEvent.keyDown(dialogContent, {
key: 'B',
ctrlKey: true,
shiftKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+Shift+B');
});
});
});
describe('Clear functionality', () => {
it('should clear current shortcut when clear button is clicked', async () => {
const user = userEvent.setup();
renderComponent({
value: 'Ctrl+T',
});
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
// Initially should show the current value
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+T');
// Verify clear button exists and is enabled
const clearButton = screen.getByTestId('shortcut-clear-button');
expect(clearButton).toBeInTheDocument();
expect(clearButton).toBeEnabled();
});
it('should call onChange with empty string when cleared and confirmed', async () => {
const user = userEvent.setup();
renderComponent({
value: '', // Start with empty to test setting empty and confirming
});
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
// Don't set any keys, just press Enter to confirm empty
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('');
});
});
});
describe('Confirm functionality', () => {
it('should call onChange with new shortcut on confirm', async () => {
const user = userEvent.setup();
renderComponent({
value: '',
});
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
fireEvent.keyDown(dialogContent, {
key: 'N',
ctrlKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+N');
});
// Press Enter to confirm
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('Ctrl+N');
});
});
it('should not call onChange when cancel is clicked', async () => {
const user = userEvent.setup();
renderComponent({
value: 'Ctrl+A',
});
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
fireEvent.keyDown(dialogContent, {
key: 'B',
ctrlKey: true,
bubbles: true,
});
await waitFor(() => {
const display = screen.getByTestId('shortcut-display');
expect(display).toHaveTextContent('Ctrl+B');
});
// Press ESC to cancel without saving
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
await waitFor(() => {
expect(mockOnChange).not.toHaveBeenCalled();
});
await waitFor(() => {
expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
}, { timeout: 500 });
});
it('should close dialog after confirm', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
const dialogContent = screen.getByTestId('shortcut-dialog-content');
fireEvent.keyDown(dialogContent, {
key: 'T',
ctrlKey: true,
bubbles: true,
});
// Press Enter to confirm and close dialog
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
}, { timeout: 500 });
});
});
describe('Help text', () => {
it('should display help text in dialog', async () => {
const user = userEvent.setup();
renderComponent();
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByText('KeyboardShortcut.HelpText')).toBeInTheDocument();
});
});
});
describe('Props validation', () => {
it('should use custom label', () => {
renderComponent({ label: 'Custom Label' });
const button = screen.getByTestId('shortcut-register-button');
expect(button).toHaveTextContent('Custom Label');
});
it('should handle onChange callback', async () => {
const customOnChange = vi.fn();
const user = userEvent.setup();
renderComponent({ onChange: customOnChange });
const button = screen.getByTestId('shortcut-register-button');
await user.click(button);
await waitFor(() => {
expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
});
// Simulate Ctrl+X key press on document
fireEvent.keyDown(document, {
key: 'X',
ctrlKey: true,
bubbles: true,
});
// Wait for the key combination to be processed
await waitFor(() => {
expect(screen.getByText('Ctrl+X')).toBeInTheDocument();
});
// Press Enter to confirm
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(customOnChange).toHaveBeenCalledWith('Ctrl+X');
});
});
});
});