Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions packages/react/src/hooks/__tests__/useCrudShortcuts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Tests for useCrudShortcuts hook
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useCrudShortcuts } from '../useCrudShortcuts';

describe('useCrudShortcuts', () => {
it('renders without error with empty callbacks', () => {
expect(() => {
renderHook(() => useCrudShortcuts({}));
}).not.toThrow();
});

it('renders without error when disabled', () => {
expect(() => {
renderHook(() => useCrudShortcuts({}, false));
}).not.toThrow();
});

it('registers onCreate shortcut', () => {
const onCreate = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onCreate }));
}).not.toThrow();
});

it('registers onEdit shortcut', () => {
const onEdit = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onEdit }));
}).not.toThrow();
});

it('registers onDelete shortcut', () => {
const onDelete = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onDelete }));
}).not.toThrow();
});

it('registers onSave shortcut', () => {
const onSave = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onSave }));
}).not.toThrow();
});

it('registers onDuplicate shortcut', () => {
const onDuplicate = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onDuplicate }));
}).not.toThrow();
});

it('registers onCancel shortcut', () => {
const onCancel = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onCancel }));
}).not.toThrow();
});

it('registers onSearch shortcut', () => {
const onSearch = vi.fn();
expect(() => {
renderHook(() => useCrudShortcuts({ onSearch }));
}).not.toThrow();
});

it('registers all shortcuts together', () => {
expect(() => {
renderHook(() =>
useCrudShortcuts({
onCreate: vi.fn(),
onEdit: vi.fn(),
onDelete: vi.fn(),
onSave: vi.fn(),
onDuplicate: vi.fn(),
onCancel: vi.fn(),
onSearch: vi.fn(),
})
);
}).not.toThrow();
});
});
Comment on lines +1 to +85
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests only verify that the hook doesn't throw an error when called with various callback combinations. They don't actually verify that the keyboard shortcuts are registered correctly or that they would trigger the callbacks when the corresponding keys are pressed. Consider adding at least one test that simulates a keyboard event (e.g., Ctrl+N for onCreate) and verifies the callback is invoked, similar to the pattern used in useKeyboardShortcuts.test.ts lines 38-54.

Copilot uses AI. Check for mistakes.
247 changes: 247 additions & 0 deletions packages/react/src/hooks/__tests__/useFocusTrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,251 @@ describe('useFocusTrap', () => {
// Should not throw
expect(result.current.current).toBeNull();
});

it('auto-focuses the first focusable element when enabled', () => {
const container = document.createElement('div');
const button = document.createElement('button');
button.textContent = 'Click me';
container.appendChild(button);
document.body.appendChild(container);

const { result } = renderHook(() =>
useFocusTrap<HTMLDivElement>({ enabled: true, autoFocus: true })
);

act(() => {
(result.current as any).current = container;
});

// Re-render to trigger effect with the ref set
const { result: result2 } = renderHook(() =>
useFocusTrap<HTMLDivElement>({ enabled: true, autoFocus: true })
);

Comment on lines +55 to +67
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test creates the DOM element and sets up the focus trap, but then immediately renders a completely new hook instance (result2) without using it. The test doesn't verify that the button was actually focused or check any assertions on the focus behavior. Consider removing the unused second renderHook call and adding an assertion to verify the button received focus, such as expect(document.activeElement).toBe(button);

Suggested change
const { result } = renderHook(() =>
useFocusTrap<HTMLDivElement>({ enabled: true, autoFocus: true })
);
act(() => {
(result.current as any).current = container;
});
// Re-render to trigger effect with the ref set
const { result: result2 } = renderHook(() =>
useFocusTrap<HTMLDivElement>({ enabled: true, autoFocus: true })
);
const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({ enabled, autoFocus: true }),
{ initialProps: { enabled: false } }
);
// Set the ref to the container
(result.current as any).current = container;
// Enable the trap to trigger the effect
rerender({ enabled: true });
expect(document.activeElement).toBe(button);

Copilot uses AI. Check for mistakes.
document.body.removeChild(container);
});

it('focuses initialFocus selector when provided', () => {
const container = document.createElement('div');
const input1 = document.createElement('input');
const input2 = document.createElement('input');
input2.className = 'target-input';
container.appendChild(input1);
container.appendChild(input2);
document.body.appendChild(container);

const focusSpy = vi.spyOn(input2, 'focus');

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: true,
initialFocus: '.target-input',
}),
{ initialProps: { enabled: false } }
);

// Set the ref
(result.current as any).current = container;

// Enable the trap to trigger the effect
rerender({ enabled: true });

expect(focusSpy).toHaveBeenCalled();

document.body.removeChild(container);
});

it('handles Escape key when escapeDeactivates is true', () => {
const container = document.createElement('div');
const button = document.createElement('button');
container.appendChild(button);
document.body.appendChild(container);

const onEscape = vi.fn();

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
escapeDeactivates: true,
onEscape,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

act(() => {
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
container.dispatchEvent(event);
});

expect(onEscape).toHaveBeenCalled();

document.body.removeChild(container);
});

it('traps Tab focus at the end of focusable elements', () => {
const container = document.createElement('div');
const button1 = document.createElement('button');
button1.textContent = 'First';
const button2 = document.createElement('button');
button2.textContent = 'Last';
container.appendChild(button1);
container.appendChild(button2);
document.body.appendChild(container);

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: false,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

// Focus the last element
button2.focus();

act(() => {
const event = new KeyboardEvent('keydown', {
key: 'Tab',
bubbles: true,
});
container.dispatchEvent(event);
});

document.body.removeChild(container);
});

it('traps Shift+Tab focus at the start of focusable elements', () => {
const container = document.createElement('div');
const button1 = document.createElement('button');
button1.textContent = 'First';
const button2 = document.createElement('button');
button2.textContent = 'Last';
container.appendChild(button1);
container.appendChild(button2);
document.body.appendChild(container);

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: false,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

// Focus the first element
button1.focus();

act(() => {
const event = new KeyboardEvent('keydown', {
key: 'Tab',
shiftKey: true,
bubbles: true,
});
container.dispatchEvent(event);
});

document.body.removeChild(container);
});

it('restores focus on unmount when restoreFocus is true', () => {
const outsideButton = document.createElement('button');
outsideButton.textContent = 'Outside';
document.body.appendChild(outsideButton);
outsideButton.focus();

const container = document.createElement('div');
const innerButton = document.createElement('button');
innerButton.textContent = 'Inside';
container.appendChild(innerButton);
document.body.appendChild(container);

const { result, rerender, unmount } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: true,
restoreFocus: true,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

unmount();

document.body.removeChild(container);
document.body.removeChild(outsideButton);
});

it('ignores non-Tab and non-Escape keys', () => {
const container = document.createElement('div');
const button = document.createElement('button');
container.appendChild(button);
document.body.appendChild(container);

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: false,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

act(() => {
const event = new KeyboardEvent('keydown', {
key: 'a',
bubbles: true,
});
container.dispatchEvent(event);
});

document.body.removeChild(container);
});

it('handles container with no focusable elements', () => {
const container = document.createElement('div');
container.innerHTML = '<span>No focusable elements</span>';
document.body.appendChild(container);

const { result, rerender } = renderHook(
({ enabled }) =>
useFocusTrap<HTMLDivElement>({
enabled,
autoFocus: true,
}),
{ initialProps: { enabled: false } }
);

(result.current as any).current = container;
rerender({ enabled: true });

act(() => {
const event = new KeyboardEvent('keydown', {
key: 'Tab',
bubbles: true,
});
container.dispatchEvent(event);
});

document.body.removeChild(container);
});
});
Loading