Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
141
packages/web/tests/lib/useApi.test.ts
Normal file
141
packages/web/tests/lib/useApi.test.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
const { mockUseSession, mockUseSWR, mockApiClient } = vi.hoisted(() => ({
|
||||
mockUseSession: vi.fn(),
|
||||
mockUseSWR: vi.fn(),
|
||||
mockApiClient: {
|
||||
_accessToken: null as string | null,
|
||||
set accessToken(token: string) {
|
||||
this._accessToken = token;
|
||||
},
|
||||
get hasToken() {
|
||||
return this._accessToken !== null;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('next-auth/react', () => ({
|
||||
useSession: mockUseSession,
|
||||
}));
|
||||
|
||||
vi.mock('swr', () => ({
|
||||
default: mockUseSWR,
|
||||
}));
|
||||
|
||||
vi.mock('@/services/api-client', () => ({
|
||||
apiClient: mockApiClient,
|
||||
}));
|
||||
|
||||
import { useApi } from '../../src/lib/useApi';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockApiClient._accessToken = null;
|
||||
});
|
||||
|
||||
describe('useApi', () => {
|
||||
it('returns loading state when session is loading', () => {
|
||||
mockUseSession.mockReturnValue({ data: null, status: 'loading' });
|
||||
mockUseSWR.mockReturnValue({ data: undefined, mutate: vi.fn(), isLoading: false });
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
expect(result.current.householdId).toBeNull();
|
||||
});
|
||||
|
||||
it('returns unauthenticated state', () => {
|
||||
mockUseSession.mockReturnValue({ data: null, status: 'unauthenticated' });
|
||||
mockUseSWR.mockReturnValue({ data: undefined, mutate: vi.fn(), isLoading: false });
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('sets token on apiClient when session has accessToken', () => {
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { accessToken: 'test-jwt' },
|
||||
status: 'authenticated',
|
||||
});
|
||||
mockUseSWR.mockReturnValue({
|
||||
data: { householdIds: ['hh1'], displayName: 'Test' },
|
||||
mutate: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderHook(() => useApi());
|
||||
|
||||
expect(mockApiClient._accessToken).toBe('test-jwt');
|
||||
});
|
||||
|
||||
it('returns householdId from profile', () => {
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { accessToken: 'test-jwt' },
|
||||
status: 'authenticated',
|
||||
});
|
||||
mockUseSWR.mockReturnValue({
|
||||
data: { householdIds: ['hh1', 'hh2'], displayName: 'Test' },
|
||||
mutate: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.householdId).toBe('hh1');
|
||||
expect(result.current.householdIds).toEqual(['hh1', 'hh2']);
|
||||
expect(result.current.isAuthenticated).toBe(true);
|
||||
});
|
||||
|
||||
it('returns null householdId when profile has no households', () => {
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { accessToken: 'test-jwt' },
|
||||
status: 'authenticated',
|
||||
});
|
||||
mockUseSWR.mockReturnValue({
|
||||
data: { householdIds: [] },
|
||||
mutate: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.householdId).toBeNull();
|
||||
});
|
||||
|
||||
it('returns loading when authenticated but profile is still loading', () => {
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { accessToken: 'test-jwt' },
|
||||
status: 'authenticated',
|
||||
});
|
||||
mockUseSWR.mockReturnValue({
|
||||
data: undefined,
|
||||
mutate: vi.fn(),
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
|
||||
it('provides refreshProfile function from SWR mutate', () => {
|
||||
const mutateFn = vi.fn();
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { accessToken: 'test-jwt' },
|
||||
status: 'authenticated',
|
||||
});
|
||||
mockUseSWR.mockReturnValue({
|
||||
data: { householdIds: ['hh1'] },
|
||||
mutate: mutateFn,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.refreshProfile).toBe(mutateFn);
|
||||
});
|
||||
});
|
||||
107
packages/web/tests/lib/useShoppingListSync.test.ts
Normal file
107
packages/web/tests/lib/useShoppingListSync.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useShoppingListSync } from '../../src/lib/useShoppingListSync';
|
||||
|
||||
vi.mock('@/services/shopping-lists', () => ({
|
||||
getShoppingListSyncSocketUrl: vi.fn(() => 'ws://localhost/sync'),
|
||||
}));
|
||||
|
||||
class MockWebSocket {
|
||||
static OPEN = 1;
|
||||
static CONNECTING = 0;
|
||||
url: string;
|
||||
readyState = 1; // OPEN
|
||||
onopen: any = null;
|
||||
onmessage: any = null;
|
||||
onerror: any = null;
|
||||
onclose: any = null;
|
||||
send = vi.fn();
|
||||
close = vi.fn();
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
describe('useShoppingListSync', () => {
|
||||
let originalWebSocket: any;
|
||||
let createdSockets: MockWebSocket[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
createdSockets = [];
|
||||
originalWebSocket = global.WebSocket;
|
||||
const MockClass = class extends MockWebSocket {
|
||||
constructor(url: string) {
|
||||
super(url);
|
||||
createdSockets.push(this);
|
||||
}
|
||||
};
|
||||
(MockClass as any).OPEN = 1;
|
||||
(MockClass as any).CONNECTING = 0;
|
||||
global.WebSocket = MockClass as any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('initializes and connects to the correct socket URL', () => {
|
||||
const onSync = vi.fn();
|
||||
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||||
|
||||
expect(createdSockets).toHaveLength(1);
|
||||
expect(createdSockets[0].url).toBe('ws://localhost/sync');
|
||||
});
|
||||
|
||||
it('handles incoming item_updated messages correctly', () => {
|
||||
const onSync = vi.fn();
|
||||
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||||
|
||||
const ws = createdSockets[0];
|
||||
act(() => { if (ws.onopen) ws.onopen(); });
|
||||
|
||||
act(() => {
|
||||
if (ws.onmessage) ws.onmessage({ data: JSON.stringify({ type: 'ITEM_UPDATED', itemId: 'item1', updates: { checked: true } }) });
|
||||
});
|
||||
|
||||
expect(onSync).toHaveBeenCalledWith({ type: 'ITEM_UPDATED', itemId: 'item1', updates: { checked: true } });
|
||||
});
|
||||
|
||||
it('broadcasts toggle item messages when connected', () => {
|
||||
const onSync = vi.fn();
|
||||
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||||
|
||||
const ws = createdSockets[0];
|
||||
act(() => { if (ws.onopen) ws.onopen(); });
|
||||
|
||||
act(() => {
|
||||
result.current.toggleItemCheck('item1', true);
|
||||
});
|
||||
|
||||
expect(ws.send).toHaveBeenCalledWith(JSON.stringify({
|
||||
type: 'TOGGLE_ITEM',
|
||||
itemId: 'item1',
|
||||
checked: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('handles disconnect and reconnect backoff', () => {
|
||||
vi.useFakeTimers();
|
||||
const onSync = vi.fn();
|
||||
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||||
|
||||
let ws = createdSockets[0];
|
||||
act(() => { if (ws.onopen) ws.onopen(); });
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
|
||||
act(() => { if (ws.onclose) ws.onclose({ reason: 'test' }); });
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
|
||||
// After 1000ms it should attempt reconnect
|
||||
act(() => { vi.advanceTimersByTime(1000); });
|
||||
expect(createdSockets).toHaveLength(2);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue