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
130 changes: 130 additions & 0 deletions frontend/hooks/useCommandPalette.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { renderHook, act } from '@testing-library/react';
import { useCommandPalette } from './useCommandPalette';

describe('useCommandPalette', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should initialize with isOpen as false', () => {
const { result } = renderHook(() => useCommandPalette());
expect(result.current.isOpen).toBe(false);
});

it('should set isOpen to true when open() is called', () => {
const { result } = renderHook(() => useCommandPalette());

act(() => {
result.current.open();
});

expect(result.current.isOpen).toBe(true);
});

it('should set isOpen to false when close() is called', () => {
const { result } = renderHook(() => useCommandPalette());

// First open it
act(() => {
result.current.open();
});
expect(result.current.isOpen).toBe(true);

// Then close it
act(() => {
result.current.close();
});

expect(result.current.isOpen).toBe(false);
});

it('should toggle isOpen state when toggle() is called', () => {
const { result } = renderHook(() => useCommandPalette());

// First toggle: false -> true
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBe(true);

// Second toggle: true -> false
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBe(false);
});

it('should toggle the palette when Ctrl+K is pressed', () => {
const { result } = renderHook(() => useCommandPalette());
expect(result.current.isOpen).toBe(false);

// Dispatch Ctrl+K keydown event
const ctrlKEvent = new KeyboardEvent('keydown', {
ctrlKey: true,
key: 'k',
});
const preventDefaultSpy = jest.spyOn(ctrlKEvent, 'preventDefault');
document.dispatchEvent(ctrlKEvent);

// Check that preventDefault was called and state toggled
expect(preventDefaultSpy).toHaveBeenCalled();
// Need to act to process the state update from the event listener
act(() => {
jest.runAllTimers();
});
expect(result.current.isOpen).toBe(true);
});

it('should toggle the palette when Cmd+K (Mac) is pressed', () => {
const { result } = renderHook(() => useCommandPalette());
expect(result.current.isOpen).toBe(false);

// Dispatch Cmd+K keydown event (metaKey is Cmd on Mac)
const cmdKEvent = new KeyboardEvent('keydown', {
metaKey: true,
key: 'k',
});
const preventDefaultSpy = jest.spyOn(cmdKEvent, 'preventDefault');
document.dispatchEvent(cmdKEvent);

expect(preventDefaultSpy).toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
expect(result.current.isOpen).toBe(true);
});

it('should not toggle for other key combinations', () => {
const { result } = renderHook(() => useCommandPalette());
expect(result.current.isOpen).toBe(false);

// Dispatch unrelated keys
const randomEvent = new KeyboardEvent('keydown', {
ctrlKey: true,
key: 'l',
});
document.dispatchEvent(randomEvent);

const anotherEvent = new KeyboardEvent('keydown', {
key: 'k',
});
document.dispatchEvent(anotherEvent);

// State should remain false
expect(result.current.isOpen).toBe(false);
});

it('should remove the event listener when the component unmounts', () => {
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
const { unmount } = renderHook(() => useCommandPalette());

// Unmount the hook
unmount();

// Verify that the event listener was removed
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'keydown',
expect.any(Function)
);
});
});
122 changes: 122 additions & 0 deletions frontend/hooks/useLocalStorageState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { renderHook, act } from '@testing-library/react';
import { useLocalStorageState } from './useLocalStorageState';

describe('useLocalStorageState', () => {
const testKey = 'test-key';
const initialValue = 'initial-value';

beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
});

it('should initialize with initial value when no stored value exists', () => {
const { result } = renderHook(() => useLocalStorageState(testKey, initialValue));

// Before hydration, it should still have initial value
expect(result.current.value).toBe(initialValue);
expect(result.current.isHydrated).toBe(false);
});

it('should hydrate and read existing value from localStorage', async () => {
const storedValue = 'stored-value';
localStorage.setItem(testKey, JSON.stringify(storedValue));

const { result } = renderHook(() => useLocalStorageState(testKey, initialValue));

// Wait for hydration effect to run
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});

expect(result.current.isHydrated).toBe(true);
expect(result.current.value).toBe(storedValue);
});

it('should persist updated values to localStorage (write-through)', async () => {
const { result } = renderHook(() => useLocalStorageState(testKey, initialValue));

// Wait for hydration
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
expect(result.current.isHydrated).toBe(true);

// Update the value
const newValue = 'new-value';
act(() => {
result.current.setValue(newValue);
});

// Wait for the effect to persist to localStorage
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});

// Check that localStorage was updated
expect(localStorage.getItem(testKey)).toBe(JSON.stringify(newValue));
expect(result.current.value).toBe(newValue);
});

it('should handle localStorage being unavailable (throws errors)', async () => {
// Mock localStorage.getItem to throw (simulates private browsing/disabled storage)
const originalGetItem = Storage.prototype.getItem;
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.getItem = jest.fn().mockImplementation(() => {
throw new Error('Storage unavailable');
});
Storage.prototype.setItem = jest.fn().mockImplementation(() => {
throw new Error('Storage unavailable');
});

const { result } = renderHook(() => useLocalStorageState(testKey, initialValue));

// Wait for hydration
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});

// Should still hydrate successfully, even if storage is unavailable
expect(result.current.isHydrated).toBe(true);
// Should keep the initial value since it couldn't read from storage
expect(result.current.value).toBe(initialValue);

// Try to update the value - should not throw, and should update local state
const newValue = 'test-new-value';
act(() => {
result.current.setValue(newValue);
});

await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});

// Local state should update even if storage fails
expect(result.current.value).toBe(newValue);
// The mocks were called (proves we tried to access storage)
expect(Storage.prototype.getItem).toHaveBeenCalled();
expect(Storage.prototype.setItem).toHaveBeenCalled();

// Restore original methods
Storage.prototype.getItem = originalGetItem;
Storage.prototype.setItem = originalSetItem;
});

it('should maintain correct isHydrated state throughout lifecycle', async () => {
const { result, rerender } = renderHook(() => useLocalStorageState(testKey, initialValue));

// Initially, isHydrated is false
expect(result.current.isHydrated).toBe(false);

// After hydration, it becomes true
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
expect(result.current.isHydrated).toBe(true);

// Rerender with different key
rerender();
// Should stay hydrated
expect(result.current.isHydrated).toBe(true);
});
});
Loading
Loading