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
55 changes: 24 additions & 31 deletions cardinal/src/hooks/__tests__/useAppHotkeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { subscribeQuickLookKeydown } from '../../runtime/tauriEventRuntime';
import { openResultPath } from '../../utils/openResultPath';
import { useAppHotkeys } from '../useAppHotkeys';

vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn(),
}));

vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));

vi.mock('../../runtime/tauriEventRuntime', () => ({
subscribeQuickLookKeydown: vi.fn(),
}));

vi.mock('../../utils/openResultPath', () => ({
openResultPath: vi.fn(),
}));

const mockedListen = vi.mocked(listen);
const mockedSubscribeQuickLookKeydown = vi.mocked(subscribeQuickLookKeydown);
const mockedInvoke = vi.mocked(invoke);
const mockedOpenResultPath = vi.mocked(openResultPath);

Expand All @@ -36,7 +36,7 @@ describe('useAppHotkeys', () => {
const navigateSelection = vi.fn();
const triggerQuickLook = vi.fn();

let quickLookListener: ((event: any) => void) | null;
let quickLookListener: ((payload: any) => void) | null;

const renderHotkeys = (overrides: Partial<HookProps> = {}) =>
renderHook((props: HookProps) => useAppHotkeys(props), {
Expand All @@ -56,12 +56,9 @@ describe('useAppHotkeys', () => {
quickLookListener = null;
mockedInvoke.mockResolvedValue(undefined);

mockedListen.mockImplementation(async (eventName: string, callback: (event: any) => void) => {
if (eventName === 'quicklook-keydown') {
quickLookListener = callback;
return quickLookUnlisten;
}
return vi.fn();
mockedSubscribeQuickLookKeydown.mockImplementation((listener) => {
quickLookListener = listener;
return quickLookUnlisten;
});
});

Expand Down Expand Up @@ -148,7 +145,7 @@ describe('useAppHotkeys', () => {
expect(navigateSelection).toHaveBeenCalledWith(-1, { extend: false });
});

it('handles Quick Look native keydown events and cleanup', async () => {
it('handles Quick Look runtime keydown events and cleanup', async () => {
const { rerender, unmount } = renderHotkeys();

await waitFor(() => {
Expand All @@ -157,14 +154,12 @@ describe('useAppHotkeys', () => {

act(() => {
quickLookListener?.({
payload: {
keyCode: 125,
modifiers: {
shift: true,
control: false,
option: false,
command: false,
},
keyCode: 125,
modifiers: {
shift: true,
control: false,
option: false,
command: false,
},
});
});
Expand All @@ -182,20 +177,18 @@ describe('useAppHotkeys', () => {
navigateSelection.mockClear();
act(() => {
quickLookListener?.({
payload: {
keyCode: 126,
modifiers: {
shift: false,
control: false,
option: false,
command: false,
},
keyCode: 126,
modifiers: {
shift: false,
control: false,
option: false,
command: false,
},
});
});
expect(navigateSelection).not.toHaveBeenCalled();

unmount();
expect(quickLookUnlisten).toHaveBeenCalledTimes(1);
expect(quickLookUnlisten).toHaveBeenCalled();
});
});
94 changes: 52 additions & 42 deletions cardinal/src/hooks/__tests__/useAppWindowListeners.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import {
subscribeLifecycleState,
subscribeQuickLaunch,
subscribeStatusBarUpdate,
subscribeWindowDragDrop,
} from '../../runtime/tauriEventRuntime';
import { useAppWindowListeners } from '../useAppWindowListeners';

vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn(),
vi.mock('../../runtime/tauriEventRuntime', () => ({
subscribeStatusBarUpdate: vi.fn(),
subscribeLifecycleState: vi.fn(),
subscribeQuickLaunch: vi.fn(),
subscribeWindowDragDrop: vi.fn(),
}));

vi.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: vi.fn(),
}));

const mockedListen = vi.mocked(listen);
const mockedGetCurrentWindow = vi.mocked(getCurrentWindow);
const mockedSubscribeStatusBarUpdate = vi.mocked(subscribeStatusBarUpdate);
const mockedSubscribeLifecycleState = vi.mocked(subscribeLifecycleState);
const mockedSubscribeQuickLaunch = vi.mocked(subscribeQuickLaunch);
const mockedSubscribeWindowDragDrop = vi.mocked(subscribeWindowDragDrop);

type HookProps = {
activeTab: 'files' | 'events';
Expand Down Expand Up @@ -41,8 +46,12 @@ describe('useAppWindowListeners', () => {
const setEventFilterQuery = vi.fn();
const updateHistoryFromInput = vi.fn();

let tauriListeners: Record<string, (event: any) => void>;
let dragDropListener: ((event: any) => void) | null;
let statusCallback:
| ((payload: { scannedFiles: number; processedEvents: number; rescanErrors: number }) => void)
| null;
let lifecycleCallback: ((status: 'Initializing' | 'Updating' | 'Ready') => void) | null;
let quickLaunchCallback: (() => void) | null;
let dragDropCallback: ((event: any) => void) | null;

const renderWindowListeners = (overrides: Partial<HookProps> = {}) =>
renderHook((props: HookProps) => useAppWindowListeners(props), {
Expand All @@ -60,47 +69,52 @@ describe('useAppWindowListeners', () => {

beforeEach(() => {
vi.clearAllMocks();
tauriListeners = {};
dragDropListener = null;
statusCallback = null;
lifecycleCallback = null;
quickLaunchCallback = null;
dragDropCallback = null;
document.documentElement.removeAttribute('data-window-focused');

mockedListen.mockImplementation(async (eventName: string, callback: (event: any) => void) => {
tauriListeners[eventName] = callback;
if (eventName === 'status_bar_update') return statusUnlisten;
if (eventName === 'app_lifecycle_state') return lifecycleUnlisten;
if (eventName === 'quick_launch') return quickLaunchUnlisten;
return vi.fn();
mockedSubscribeStatusBarUpdate.mockImplementation((listener) => {
statusCallback = listener;
return statusUnlisten;
});
mockedSubscribeLifecycleState.mockImplementation((listener) => {
lifecycleCallback = listener;
return lifecycleUnlisten;
});
mockedSubscribeQuickLaunch.mockImplementation((listener) => {
quickLaunchCallback = listener;
return quickLaunchUnlisten;
});
mockedSubscribeWindowDragDrop.mockImplementation((listener) => {
dragDropCallback = listener;
return dragDropUnlisten;
});

mockedGetCurrentWindow.mockReturnValue({
onDragDropEvent: vi.fn(async (callback: (event: any) => void) => {
dragDropListener = callback;
return dragDropUnlisten;
}),
} as unknown as ReturnType<typeof getCurrentWindow>);
});

it('subscribes to tauri events and dispatches payloads to handlers', async () => {
it('subscribes to runtime events and dispatches payloads to handlers', async () => {
renderWindowListeners();

await waitFor(() => {
expect(mockedListen).toHaveBeenCalledTimes(3);
expect(mockedSubscribeStatusBarUpdate).toHaveBeenCalledTimes(1);
expect(mockedSubscribeLifecycleState).toHaveBeenCalledTimes(1);
expect(mockedSubscribeQuickLaunch).toHaveBeenCalledTimes(1);
expect(mockedSubscribeWindowDragDrop).toHaveBeenCalledTimes(1);
});

act(() => {
tauriListeners.status_bar_update?.({
payload: { scannedFiles: 11, processedEvents: 22, rescanErrors: 3 },
});
statusCallback?.({ scannedFiles: 11, processedEvents: 22, rescanErrors: 3 });
});
expect(handleStatusUpdate).toHaveBeenCalledWith(11, 22, 3);

act(() => {
tauriListeners.app_lifecycle_state?.({ payload: 'Ready' });
lifecycleCallback?.('Ready');
});
expect(setLifecycleState).toHaveBeenCalledWith('Ready');

act(() => {
tauriListeners.quick_launch?.({});
quickLaunchCallback?.();
});
expect(focusSearchInput).toHaveBeenCalledTimes(1);
});
Expand All @@ -109,11 +123,11 @@ describe('useAppWindowListeners', () => {
const { rerender } = renderWindowListeners();

await waitFor(() => {
expect(dragDropListener).not.toBeNull();
expect(dragDropCallback).not.toBeNull();
});

act(() => {
dragDropListener?.({
dragDropCallback?.({
payload: { type: 'drop', paths: [' /tmp/file-a '] },
});
});
Expand All @@ -133,20 +147,16 @@ describe('useAppWindowListeners', () => {
});

act(() => {
dragDropListener?.({
dragDropCallback?.({
payload: { type: 'drop', paths: [' /tmp/file-b '] },
});
});
expect(setEventFilterQuery).toHaveBeenCalledWith('"/tmp/file-b"');
});

it('syncs window focus attribute and cleans up listeners on unmount', async () => {
it('syncs window focus attribute and cleans up runtime subscriptions on unmount', async () => {
const { unmount } = renderWindowListeners();

await waitFor(() => {
expect(dragDropListener).not.toBeNull();
});

act(() => {
window.dispatchEvent(new Event('blur'));
});
Expand Down
19 changes: 14 additions & 5 deletions cardinal/src/hooks/__tests__/useDataLoader.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { subscribeIconUpdate } from '../../runtime/tauriEventRuntime';
import type { SlabIndex } from '../../types/slab';
import { useDataLoader } from '../useDataLoader';

vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));

vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn(),
vi.mock('../../runtime/tauriEventRuntime', () => ({
subscribeIconUpdate: vi.fn(),
}));

const mockedInvoke = vi.mocked(invoke);
const mockedListen = vi.mocked(listen);
const mockedSubscribeIconUpdate = vi.mocked(subscribeIconUpdate);
type HookProps = { results: SlabIndex[]; version: number };

const buildNodeInfo = (slabIndex: SlabIndex) => ({
Expand All @@ -32,8 +32,10 @@ const renderDataLoader = (initialProps: HookProps) =>
});

describe('useDataLoader', () => {
const iconUpdateUnlisten = vi.fn();

beforeEach(() => {
mockedListen.mockResolvedValue((() => {}) as () => void);
mockedSubscribeIconUpdate.mockImplementation(() => iconUpdateUnlisten);
mockedInvoke.mockImplementation((command: string, payload?: unknown) => {
if (command !== 'get_nodes_info') {
return Promise.resolve(null);
Expand Down Expand Up @@ -97,4 +99,11 @@ describe('useDataLoader', () => {
await waitFor(() => expect(result.current.cache.size).toBe(2));
expect(mockedInvoke).toHaveBeenCalledTimes(2);
});

it('cleans up icon update subscription on unmount', async () => {
const { unmount } = renderDataLoader({ results: [11 as SlabIndex], version: 1 });
unmount();

expect(iconUpdateUnlisten).toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions cardinal/src/hooks/__tests__/useRecentFSEvents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { subscribeFSEventsBatch } from '../../runtime/tauriEventRuntime';
import { useRecentFSEvents } from '../useRecentFSEvents';

vi.mock('../../runtime/tauriEventRuntime', () => ({
subscribeFSEventsBatch: vi.fn(),
}));

const mockedSubscribeFSEventsBatch = vi.mocked(subscribeFSEventsBatch);

describe('useRecentFSEvents', () => {
const unlisten = vi.fn();
let fsEventsBatchListener: ((payload: any) => void) | null = null;

beforeEach(() => {
vi.clearAllMocks();
fsEventsBatchListener = null;

mockedSubscribeFSEventsBatch.mockImplementation((callback: (payload: any) => void) => {
fsEventsBatchListener = callback;
return unlisten;
});
});

it('stores and filters streamed events when active', async () => {
const { result } = renderHook(() =>
useRecentFSEvents({ caseSensitive: false, isActive: true }),
);

await waitFor(() => {
expect(fsEventsBatchListener).not.toBeNull();
});

act(() => {
fsEventsBatchListener?.([
{ path: '/tmp/Alpha.txt', eventId: 1, timestamp: 1, flagBits: 0 },
{ path: '/tmp/beta.txt', eventId: 2, timestamp: 2, flagBits: 0 },
]);
});

expect(result.current.filteredEvents).toHaveLength(2);

act(() => {
result.current.setEventFilterQuery('alpha');
});

expect(result.current.filteredEvents).toHaveLength(1);
expect(result.current.filteredEvents[0]?.path).toBe('/tmp/Alpha.txt');
});

it('cleans up runtime subscription on unmount', async () => {
const { unmount } = renderHook(() =>
useRecentFSEvents({ caseSensitive: false, isActive: true }),
);
unmount();

expect(unlisten).toHaveBeenCalledTimes(1);
});
});
Loading