Files
noteflow/client/src/hooks/meetings/use-meeting-mutations.test.tsx
Travis Vasceannie 2641a9fc03
Some checks failed
CI / test-python (push) Failing after 17m22s
CI / test-rust (push) Has been cancelled
CI / test-typescript (push) Has been cancelled
optimization
2026-01-25 01:40:14 +00:00

320 lines
9.4 KiB
TypeScript

import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useCreateMeeting, useDeleteMeeting } from './use-meeting-mutations';
import { meetingCache } from '@/lib/cache/meeting-cache';
import type { Meeting } from '@/api/types';
vi.mock('@/lib/cache/meeting-cache');
vi.mock('@/hooks/ui/use-toast', () => ({
useToast: () => ({
toast: vi.fn(),
}),
}));
const mockAPI = {
createMeeting: vi.fn(),
deleteMeeting: vi.fn(),
};
vi.mock('@/api/interface', () => ({
getAPI: () => mockAPI,
}));
const createMockMeeting = (overrides?: Partial<Meeting>): Meeting => ({
id: 'meeting-123',
title: 'Test Meeting',
state: 'created' as const,
created_at: Date.now(),
duration_seconds: 0,
segments: [],
metadata: {},
...overrides,
});
describe('useCreateMeeting', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should create optimistic meeting immediately', async () => {
const { result } = renderHook(() => useCreateMeeting());
const realMeeting = createMockMeeting({ id: 'real-123' });
mockAPI.createMeeting.mockImplementation(
() =>
new Promise((resolve) => setTimeout(() => resolve(realMeeting), 100))
);
const mutatePromise = result.current.mutate({ title: 'Test Meeting' });
await waitFor(() => {
expect(meetingCache.cacheMeeting).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.stringMatching(/^temp-/),
title: 'Test Meeting',
state: 'created' as const,
})
);
});
await mutatePromise;
});
it('should replace optimistic meeting with real meeting on success', async () => {
const { result } = renderHook(() => useCreateMeeting());
const realMeeting = createMockMeeting({ id: 'real-123' });
mockAPI.createMeeting.mockResolvedValue(realMeeting);
await result.current.mutate({ title: 'Test Meeting' });
expect(meetingCache.removeMeeting).toHaveBeenCalledWith(
expect.stringMatching(/^temp-/)
);
expect(meetingCache.cacheMeeting).toHaveBeenCalledWith(realMeeting);
});
it('should remove optimistic meeting on error', async () => {
const { result } = renderHook(() => useCreateMeeting());
const error = new Error('API Error');
mockAPI.createMeeting.mockRejectedValue(error);
await result.current.mutate({ title: 'Test Meeting' });
expect(meetingCache.removeMeeting).toHaveBeenCalledWith(
expect.stringMatching(/^temp-/)
);
});
it('should handle metadata and project_id correctly', async () => {
const { result } = renderHook(() => useCreateMeeting());
const realMeeting = createMockMeeting({
id: 'real-123',
project_id: 'proj-456',
metadata: { key: 'value' },
});
mockAPI.createMeeting.mockResolvedValue(realMeeting);
await result.current.mutate({
title: 'Test Meeting',
project_id: 'proj-456',
metadata: { key: 'value' },
});
expect(mockAPI.createMeeting).toHaveBeenCalledWith({
title: 'Test Meeting',
project_id: 'proj-456',
metadata: { key: 'value' },
});
});
it('should expose loading state', async () => {
const { result } = renderHook(() => useCreateMeeting());
expect(result.current.isLoading).toBe(false);
mockAPI.createMeeting.mockImplementation(
() =>
new Promise((resolve) => setTimeout(() => resolve(createMockMeeting()), 50))
);
const mutatePromise = result.current.mutate({ title: 'Test Meeting' });
await waitFor(() => {
expect(result.current.isLoading).toBe(true);
});
await act(async () => {
await mutatePromise;
});
expect(result.current.isLoading).toBe(false);
});
it('should expose error state', async () => {
const { result } = renderHook(() => useCreateMeeting());
const error = new Error('API Error');
mockAPI.createMeeting.mockRejectedValue(error);
await act(async () => {
await result.current.mutate({ title: 'Test Meeting' });
});
expect(result.current.error).toBeTruthy();
expect(result.current.error?.message).toBe('API Error');
});
it('should clear error on successful mutation', async () => {
const { result } = renderHook(() => useCreateMeeting());
mockAPI.createMeeting.mockRejectedValueOnce(new Error('First error'));
await act(async () => {
await result.current.mutate({ title: 'Test Meeting' });
});
expect(result.current.error).toBeTruthy();
mockAPI.createMeeting.mockResolvedValueOnce(createMockMeeting());
await act(async () => {
await result.current.mutate({ title: 'Test Meeting 2' });
});
expect(result.current.error).toBeNull();
});
it('should handle project_ids array', async () => {
const { result } = renderHook(() => useCreateMeeting());
const realMeeting = createMockMeeting({ id: 'real-123' });
mockAPI.createMeeting.mockResolvedValue(realMeeting);
await result.current.mutate({
title: 'Test Meeting',
project_ids: ['proj-1', 'proj-2'],
});
expect(mockAPI.createMeeting).toHaveBeenCalledWith({
title: 'Test Meeting',
project_ids: ['proj-1', 'proj-2'],
});
});
});
describe('useDeleteMeeting', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should remove meeting from cache immediately (optimistic)', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
mockAPI.deleteMeeting.mockImplementation(
() =>
new Promise((resolve) => setTimeout(() => resolve(true), 100))
);
const mutatePromise = result.current.mutate('meeting-123');
await waitFor(() => {
expect(meetingCache.removeMeeting).toHaveBeenCalledWith('meeting-123');
});
await mutatePromise;
});
it('should keep meeting removed on success', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
mockAPI.deleteMeeting.mockResolvedValue(true);
await result.current.mutate('meeting-123');
expect(meetingCache.removeMeeting).toHaveBeenCalledWith('meeting-123');
});
it('should restore meeting from context on error', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
const error = new Error('Delete failed');
mockAPI.deleteMeeting.mockRejectedValue(error);
await result.current.mutate('meeting-123');
expect(meetingCache.cacheMeeting).toHaveBeenCalledWith(meeting);
});
it('should handle missing meeting gracefully', async () => {
const { result } = renderHook(() => useDeleteMeeting());
vi.mocked(meetingCache.getMeeting).mockReturnValue(null);
mockAPI.deleteMeeting.mockResolvedValue(true);
await result.current.mutate('missing-meeting');
expect(meetingCache.removeMeeting).toHaveBeenCalledWith('missing-meeting');
});
it('should expose loading state', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
expect(result.current.isLoading).toBe(false);
mockAPI.deleteMeeting.mockImplementation(
() =>
new Promise((resolve) => setTimeout(() => resolve(true), 50))
);
const mutatePromise = result.current.mutate('meeting-123');
await waitFor(() => {
expect(result.current.isLoading).toBe(true);
});
await act(async () => {
await mutatePromise;
});
expect(result.current.isLoading).toBe(false);
});
it('should expose error state', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
const error = new Error('Delete failed');
mockAPI.deleteMeeting.mockRejectedValue(error);
await act(async () => {
await result.current.mutate('meeting-123');
});
expect(result.current.error).toBeTruthy();
expect(result.current.error?.message).toBe('Delete failed');
});
it('should handle API returning false (not found)', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
mockAPI.deleteMeeting.mockResolvedValue(false);
await result.current.mutate('meeting-123');
expect(meetingCache.removeMeeting).toHaveBeenCalledWith('meeting-123');
});
it('should clear error on successful mutation', async () => {
const { result } = renderHook(() => useDeleteMeeting());
const meeting = createMockMeeting({ id: 'meeting-123' });
vi.mocked(meetingCache.getMeeting).mockReturnValue(meeting);
mockAPI.deleteMeeting.mockRejectedValueOnce(new Error('First error'));
await act(async () => {
await result.current.mutate('meeting-123');
});
expect(result.current.error).toBeTruthy();
mockAPI.deleteMeeting.mockResolvedValueOnce(true);
await act(async () => {
await result.current.mutate('meeting-456');
});
expect(result.current.error).toBeNull();
});
});