320 lines
9.4 KiB
TypeScript
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();
|
|
});
|
|
});
|