diff --git a/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts b/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts new file mode 100644 index 000000000..336565286 --- /dev/null +++ b/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { renderHook, act, waitFor } from '@testing-library/react' +import type { User } from '@/lib/level-types' +import { useAuth } from '@/hooks/useAuth' +import { fetchSession } from '@/lib/auth/api/fetch-session' +import { login as loginRequest } from '@/lib/auth/api/login' +import { logout as logoutRequest } from '@/lib/auth/api/logout' + +vi.mock('@/lib/auth/api/fetch-session', () => ({ + fetchSession: vi.fn(), +})) + +vi.mock('@/lib/auth/api/login', () => ({ + login: vi.fn(), +})) + +vi.mock('@/lib/auth/api/logout', () => ({ + logout: vi.fn(), +})) + +const mockFetchSession = vi.mocked(fetchSession) +const mockLogin = vi.mocked(loginRequest) +const mockLogout = vi.mocked(logoutRequest) + +const createUser = (overrides?: Partial): User => ({ + id: 'user_1', + username: 'alice', + email: 'alice@example.com', + role: 'user', + createdAt: 1000, + tenantId: undefined, + profilePicture: undefined, + bio: undefined, + isInstanceOwner: false, + ...overrides, +}) + +const waitForIdle = async (result: { current: { isLoading: boolean } }) => { + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) +} + +const resetAuthStore = async () => { + const { result, unmount } = renderHook(() => useAuth()) + await waitForIdle(result) + await act(async () => { + await result.current.logout() + }) + await waitForIdle(result) + unmount() +} + +describe('useAuth role mapping', () => { + beforeEach(async () => { + mockFetchSession.mockReset() + mockLogin.mockReset() + mockLogout.mockReset() + mockFetchSession.mockResolvedValue(null) + mockLogout.mockResolvedValue(undefined) + + await resetAuthStore() + }) + + it.each([ + { role: 'public', expectedLevel: 1 }, + { role: 'user', expectedLevel: 2 }, + { role: 'admin', expectedLevel: 4 }, + { role: 'supergod', expectedLevel: 6 }, + { role: 'unknown', expectedLevel: 0 }, + ])('applies level for role "$role"', async ({ role, expectedLevel }) => { + const { result, unmount } = renderHook(() => useAuth()) + + mockLogin.mockResolvedValue(createUser({ role })) + + await waitForIdle(result) + await act(async () => { + await result.current.login('alice@example.com', 'password') + }) + + expect(result.current.user?.level).toBe(expectedLevel) + + unmount() + }) + + it('maps refreshed session roles to levels', async () => { + const { result, unmount } = renderHook(() => useAuth()) + + mockFetchSession.mockResolvedValue(createUser({ role: 'moderator' })) + + await act(async () => { + await result.current.refresh() + }) + await waitForIdle(result) + + expect(result.current.user?.level).toBe(3) + + unmount() + }) +}) diff --git a/frontends/nextjs/src/hooks/useAuth.test.ts b/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts similarity index 77% rename from frontends/nextjs/src/hooks/useAuth.test.ts rename to frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts index 66531dcd9..ffb955ac2 100644 --- a/frontends/nextjs/src/hooks/useAuth.test.ts +++ b/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' import type { User } from '@/lib/level-types' +import { useAuth } from '@/hooks/useAuth' +import { fetchSession } from '@/lib/auth/api/fetch-session' +import { login as loginRequest } from '@/lib/auth/api/login' +import { register as registerRequest } from '@/lib/auth/api/register' +import { logout as logoutRequest } from '@/lib/auth/api/logout' vi.mock('@/lib/auth/api/fetch-session', () => ({ fetchSession: vi.fn(), @@ -18,12 +23,6 @@ vi.mock('@/lib/auth/api/logout', () => ({ logout: vi.fn(), })) -import { useAuth } from '@/hooks/useAuth' -import { fetchSession } from '@/lib/auth/api/fetch-session' -import { login as loginRequest } from '@/lib/auth/api/login' -import { register as registerRequest } from '@/lib/auth/api/register' -import { logout as logoutRequest } from '@/lib/auth/api/logout' - const mockFetchSession = vi.mocked(fetchSession) const mockLogin = vi.mocked(loginRequest) const mockRegister = vi.mocked(registerRequest) @@ -48,7 +47,17 @@ const waitForIdle = async (result: { current: { isLoading: boolean } }) => { }) } -describe('useAuth', () => { +const resetAuthStore = async () => { + const { result, unmount } = renderHook(() => useAuth()) + await waitForIdle(result) + await act(async () => { + await result.current.logout() + }) + await waitForIdle(result) + unmount() +} + +describe('useAuth session flows', () => { beforeEach(async () => { mockFetchSession.mockReset() mockLogin.mockReset() @@ -57,16 +66,10 @@ describe('useAuth', () => { mockFetchSession.mockResolvedValue(null) mockLogout.mockResolvedValue(undefined) - const { result, unmount } = renderHook(() => useAuth()) - await waitForIdle(result) - await act(async () => { - await result.current.logout() - }) - await waitForIdle(result) - unmount() + await resetAuthStore() }) - it('should start unauthenticated after session check', async () => { + it('starts unauthenticated after session check', async () => { const { result, unmount } = renderHook(() => useAuth()) await waitForIdle(result) @@ -77,28 +80,20 @@ describe('useAuth', () => { unmount() }) - it.each([ - { email: 'alice@example.com', expectedName: 'alice' }, - { email: 'bob.smith@corp.io', expectedName: 'bob.smith' }, - ])('should authenticate $email', async ({ email, expectedName }) => { + it('authenticates on login', async () => { const { result, unmount } = renderHook(() => useAuth()) - mockLogin.mockResolvedValue(createUser({ - id: 'user_1', - username: expectedName, - email, - })) + mockLogin.mockResolvedValue(createUser()) await waitForIdle(result) await act(async () => { - await result.current.login(email, 'password') + await result.current.login('alice@example.com', 'password') }) expect(result.current.user).toMatchObject({ id: 'user_1', - email, - name: expectedName, - username: expectedName, + email: 'alice@example.com', + username: 'alice', level: 2, }) expect(result.current.isAuthenticated).toBe(true) @@ -106,7 +101,7 @@ describe('useAuth', () => { unmount() }) - it('should clear user on logout', async () => { + it('clears user on logout', async () => { const { result, unmount } = renderHook(() => useAuth()) mockLogin.mockResolvedValue(createUser()) @@ -126,14 +121,16 @@ describe('useAuth', () => { unmount() }) - it('should register and authenticate', async () => { + it('registers and authenticates', async () => { const { result, unmount } = renderHook(() => useAuth()) - mockRegister.mockResolvedValue(createUser({ - id: 'user_2', - username: 'newbie', - email: 'newbie@example.com', - })) + mockRegister.mockResolvedValue( + createUser({ + id: 'user_2', + username: 'newbie', + email: 'newbie@example.com', + }) + ) await waitForIdle(result) await act(async () => { @@ -143,7 +140,6 @@ describe('useAuth', () => { expect(result.current.user).toMatchObject({ id: 'user_2', email: 'newbie@example.com', - name: 'newbie', username: 'newbie', level: 2, }) @@ -152,7 +148,7 @@ describe('useAuth', () => { unmount() }) - it('should sync state across hooks', async () => { + it('syncs state across hooks', async () => { const first = renderHook(() => useAuth()) const second = renderHook(() => useAuth()) diff --git a/frontends/nextjs/src/hooks/data/useKV.test.ts b/frontends/nextjs/src/hooks/data/__tests__/useKV.store.test.ts similarity index 54% rename from frontends/nextjs/src/hooks/data/useKV.test.ts rename to frontends/nextjs/src/hooks/data/__tests__/useKV.store.test.ts index d842a5c57..1334e1226 100644 --- a/frontends/nextjs/src/hooks/data/useKV.test.ts +++ b/frontends/nextjs/src/hooks/data/__tests__/useKV.store.test.ts @@ -1,54 +1,34 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' -import { useKV } from '@/hooks/useKV' +import { useKV } from '@/hooks/data/useKV' -describe('useKV', () => { - const STORAGE_PREFIX = 'mb_kv:' - let store: Record +const STORAGE_PREFIX = 'mb_kv:' +let store: Record +const setupLocalStorage = (): void => { + store = {} + vi.stubGlobal('localStorage', { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + Object.keys(store).forEach(k => delete store[k]) + }), + length: 0, + key: vi.fn(() => null), + }) +} + +describe('useKV storage', () => { beforeEach(() => { - // Mock localStorage - store = {} - vi.stubGlobal('localStorage', { - getItem: vi.fn((key: string) => store[key] ?? null), - setItem: vi.fn((key: string, value: string) => { store[key] = value }), - removeItem: vi.fn((key: string) => { delete store[key] }), - clear: vi.fn(() => { Object.keys(store).forEach(k => delete store[k]) }), - length: 0, - key: vi.fn(() => null), - }) + setupLocalStorage() }) - it.each([ - { key: 'user_name', defaultValue: 'John', description: 'string value' }, - { key: 'user_count', defaultValue: 0, description: 'number value' }, - { key: 'is_active', defaultValue: true, description: 'boolean value' }, - { key: 'user_data', defaultValue: { id: 1, name: 'John' }, description: 'object value' }, - ])('should initialize hook with $description', ({ key, defaultValue }) => { - const { result } = renderHook(() => useKV(key, defaultValue)) - const [value] = result.current - - expect(value).toBe(defaultValue) - }) - - it('should initialize with undefined when no default value provided', () => { - const { result } = renderHook(() => useKV('empty_key')) - const [value] = result.current - - expect(value).toBeUndefined() - }) - - it('should load value from localStorage when available', async () => { - localStorage.setItem(`${STORAGE_PREFIX}stored_key`, JSON.stringify('stored')) - - const { result } = renderHook(() => useKV('stored_key', 'default')) - - await waitFor(() => { - expect(result.current[0]).toBe('stored') - }) - }) - - it('should migrate legacy localStorage entries to namespaced keys', () => { + it('migrates legacy localStorage entries to namespaced keys', () => { localStorage.setItem('legacy_key', JSON.stringify('legacy')) const { result } = renderHook(() => useKV('legacy_key', 'default')) @@ -58,7 +38,7 @@ describe('useKV', () => { expect(localStorage.getItem('legacy_key')).toBeNull() }) - it('should update value when using updater function', async () => { + it('updates value when using updater function', async () => { const { result } = renderHook(() => useKV('counter', 0)) const [, updateValue] = result.current @@ -71,9 +51,8 @@ describe('useKV', () => { expect(newValue).toBe(1) }) - it('should update value when providing direct value', async () => { + it('updates value when providing direct value', async () => { const { result } = renderHook(() => useKV('name', 'John')) - const [, updateValue] = result.current await act(async () => { @@ -84,7 +63,7 @@ describe('useKV', () => { expect(newValue).toBe('Jane') }) - it('should handle complex object updates', async () => { + it('handles complex object updates', async () => { const initialObject = { id: 1, name: 'John', email: 'john@example.com' } const { result } = renderHook(() => useKV('user', initialObject)) @@ -101,7 +80,7 @@ describe('useKV', () => { expect(newValue).toEqual({ id: 1, name: 'Jane', email: 'john@example.com' }) }) - it('should handle array updates', async () => { + it('handles array updates', async () => { const initialArray = [1, 2, 3] const { result } = renderHook(() => useKV('items', initialArray)) @@ -115,7 +94,7 @@ describe('useKV', () => { expect(newValue).toEqual([1, 2, 3, 4]) }) - it('should maintain separate state for different keys', async () => { + it('maintains separate state for different keys', () => { const { result: result1 } = renderHook(() => useKV('key1', 'value1')) const { result: result2 } = renderHook(() => useKV('key2', 'value2')) @@ -126,7 +105,7 @@ describe('useKV', () => { expect(value2).toBe('value2') }) - it('should persist updates across multiple hooks with same key', async () => { + it('persists updates across multiple hooks with same key', async () => { const { result: firstHook } = renderHook(() => useKV('shared_key', 'initial')) const [, updateValue] = firstHook.current @@ -134,14 +113,13 @@ describe('useKV', () => { await updateValue('updated') }) - // Create a new hook with the same key const { result: secondHook } = renderHook(() => useKV('shared_key', 'initial')) const [value] = secondHook.current expect(value).toBe('updated') }) - it('should sync updates across mounted hooks with same key', async () => { + it('syncs updates across mounted hooks with same key', async () => { const { result: firstHook } = renderHook(() => useKV('sync_key', 'initial')) const { result: secondHook } = renderHook(() => useKV('sync_key', 'initial')) @@ -154,19 +132,7 @@ describe('useKV', () => { }) }) - it.each([ - { initialValue: null, key: 'falsy_key_null', description: 'null value' }, - { initialValue: false, key: 'falsy_key_false', description: 'false boolean' }, - { initialValue: 0, key: 'falsy_key_zero', description: 'zero number' }, - { initialValue: '', key: 'falsy_key_empty', description: 'empty string' }, - ])('should handle falsy $description correctly', ({ initialValue, key }) => { - const { result } = renderHook(() => useKV(key, initialValue)) - const [value] = result.current - - expect(value).toBe(initialValue) - }) - - it('should handle rapid updates correctly', async () => { + it('handles rapid updates correctly', async () => { const { result } = renderHook(() => useKV('rapid_key', 0)) const [, updateValue] = result.current @@ -183,7 +149,7 @@ describe('useKV', () => { expect(finalValue).toBeGreaterThanOrEqual(1) }) - it('should persist updates to localStorage', async () => { + it('persists updates to localStorage', async () => { const { result } = renderHook(() => useKV('persist_key', 'initial')) const [, updateValue] = result.current diff --git a/frontends/nextjs/src/hooks/data/__tests__/useKV.validation.test.ts b/frontends/nextjs/src/hooks/data/__tests__/useKV.validation.test.ts new file mode 100644 index 000000000..19b6f25a5 --- /dev/null +++ b/frontends/nextjs/src/hooks/data/__tests__/useKV.validation.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { renderHook } from '@testing-library/react' +import { useKV } from '@/hooks/data/useKV' + +const STORAGE_PREFIX = 'mb_kv:' +let store: Record + +const setupLocalStorage = (): void => { + store = {} + vi.stubGlobal('localStorage', { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + Object.keys(store).forEach(k => delete store[k]) + }), + length: 0, + key: vi.fn(() => null), + }) +} + +describe('useKV validation', () => { + beforeEach(() => { + setupLocalStorage() + }) + + it.each([ + { key: 'user_name', defaultValue: 'John', description: 'string value' }, + { key: 'user_count', defaultValue: 0, description: 'number value' }, + { key: 'is_active', defaultValue: true, description: 'boolean value' }, + { key: 'user_data', defaultValue: { id: 1, name: 'John' }, description: 'object value' }, + ])('initializes with $description', ({ key, defaultValue }) => { + const { result } = renderHook(() => useKV(key, defaultValue)) + const [value] = result.current + + expect(value).toBe(defaultValue) + }) + + it('initializes with undefined when no default value provided', () => { + const { result } = renderHook(() => useKV('empty_key')) + const [value] = result.current + + expect(value).toBeUndefined() + }) + + it.each([ + { initialValue: null, key: 'falsy_key_null', description: 'null value' }, + { initialValue: false, key: 'falsy_key_false', description: 'false boolean' }, + { initialValue: 0, key: 'falsy_key_zero', description: 'zero number' }, + { initialValue: '', key: 'falsy_key_empty', description: 'empty string' }, + ])('handles $description correctly', ({ initialValue, key }) => { + const { result } = renderHook(() => useKV(key, initialValue)) + const [value] = result.current + + expect(value).toBe(initialValue) + }) + + it('loads value from localStorage when available', () => { + localStorage.setItem(`${STORAGE_PREFIX}stored_key`, JSON.stringify('stored')) + + const { result } = renderHook(() => useKV('stored_key', 'default')) + const [value] = result.current + + expect(value).toBe('stored') + }) +}) diff --git a/frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.error-handling.test.ts b/frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.error-handling.test.ts new file mode 100644 index 000000000..1d9f1b6db --- /dev/null +++ b/frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.error-handling.test.ts @@ -0,0 +1,74 @@ +/** + * Auto-refresh error-handling tests + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useAutoRefresh } from '../useAutoRefresh' + +describe('useAutoRefresh error handling', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('stops refresh when disabled after being enabled', () => { + const onRefresh = vi.fn().mockResolvedValue(undefined) + + const { result } = renderHook(() => + useAutoRefresh({ + intervalMs: 5000, + onRefresh, + enabled: true, + }) + ) + + act(() => { + vi.advanceTimersByTime(2500) + }) + + act(() => { + result.current.setEnabled(false) + }) + + act(() => { + vi.advanceTimersByTime(10000) + }) + + expect(onRefresh).not.toHaveBeenCalled() + }) + + it('continues scheduling refreshes after onRefresh errors', () => { + const erroringRefresh = vi.fn().mockImplementation(() => + Promise.reject(new Error('refresh failed')).catch(() => {}) + ) + + const { result } = renderHook(() => + useAutoRefresh({ + intervalMs: 2000, + onRefresh: erroringRefresh, + enabled: true, + }) + ) + + expect(result.current.secondsUntilNextRefresh).toBe(2) + + act(() => { + vi.advanceTimersByTime(2000) + }) + expect(erroringRefresh).toHaveBeenCalledTimes(1) + + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(result.current.secondsUntilNextRefresh).toBe(1) + + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(result.current.secondsUntilNextRefresh).toBe(2) + expect(erroringRefresh).toHaveBeenCalledTimes(2) + }) +}) diff --git a/frontends/nextjs/src/hooks/ui/state/useAutoRefresh.test.ts b/frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.polling.test.ts similarity index 71% rename from frontends/nextjs/src/hooks/ui/state/useAutoRefresh.test.ts rename to frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.polling.test.ts index c61dda058..13c26ff29 100644 --- a/frontends/nextjs/src/hooks/ui/state/useAutoRefresh.test.ts +++ b/frontends/nextjs/src/hooks/ui/state/__tests__/useAutoRefresh.polling.test.ts @@ -1,13 +1,11 @@ /** - * Tests for useAutoRefresh hook - Auto-refresh polling management - * Following parameterized test pattern per project conventions + * Auto-refresh polling tests */ - import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' -import { useAutoRefresh } from './useAutoRefresh' +import { useAutoRefresh } from '../useAutoRefresh' -describe('useAutoRefresh', () => { +describe('useAutoRefresh polling', () => { beforeEach(() => { vi.useFakeTimers() }) @@ -21,7 +19,7 @@ describe('useAutoRefresh', () => { { enabled: false, expectAutoRefreshing: false }, { enabled: true, expectAutoRefreshing: true }, { enabled: undefined, expectAutoRefreshing: false }, - ])('should initialize with enabled=$enabled -> isAutoRefreshing=$expectAutoRefreshing', ({ enabled, expectAutoRefreshing }) => { + ])('initializes with enabled=$enabled -> isAutoRefreshing=$expectAutoRefreshing', ({ enabled, expectAutoRefreshing }) => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -39,7 +37,7 @@ describe('useAutoRefresh', () => { { intervalMs: 30000, expectedSeconds: 30 }, { intervalMs: 60000, expectedSeconds: 60 }, { intervalMs: 5000, expectedSeconds: 5 }, - ])('should set secondsUntilNextRefresh from intervalMs=$intervalMs', ({ intervalMs, expectedSeconds }) => { + ])('sets secondsUntilNextRefresh from intervalMs=$intervalMs', ({ intervalMs, expectedSeconds }) => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -55,7 +53,7 @@ describe('useAutoRefresh', () => { }) describe('toggleAutoRefresh', () => { - it('should toggle from disabled to enabled', () => { + it('toggles from disabled to enabled', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -75,7 +73,7 @@ describe('useAutoRefresh', () => { expect(result.current.isAutoRefreshing).toBe(true) }) - it('should toggle from enabled to disabled', () => { + it('toggles from enabled to disabled', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -102,7 +100,7 @@ describe('useAutoRefresh', () => { { initial: true, setTo: false, expected: false }, { initial: false, setTo: false, expected: false }, { initial: true, setTo: true, expected: true }, - ])('should set from $initial to $setTo', ({ initial, setTo, expected }) => { + ])('sets from $initial to $setTo', ({ initial, setTo, expected }) => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -122,7 +120,7 @@ describe('useAutoRefresh', () => { }) describe('refresh timing', () => { - it('should call onRefresh after intervalMs when enabled', async () => { + it('calls onRefresh after intervalMs when enabled', async () => { const onRefresh = vi.fn().mockResolvedValue(undefined) renderHook(() => @@ -133,23 +131,20 @@ describe('useAutoRefresh', () => { }) ) - // Not called initially expect(onRefresh).not.toHaveBeenCalled() - // Advance to just before interval act(() => { vi.advanceTimersByTime(4999) }) expect(onRefresh).not.toHaveBeenCalled() - // Advance past interval act(() => { vi.advanceTimersByTime(1) }) expect(onRefresh).toHaveBeenCalledTimes(1) }) - it('should not call onRefresh when disabled', () => { + it('does not call onRefresh when disabled', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) renderHook(() => @@ -167,7 +162,7 @@ describe('useAutoRefresh', () => { expect(onRefresh).not.toHaveBeenCalled() }) - it('should call onRefresh multiple times at interval', () => { + it('calls onRefresh multiple times at interval', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) renderHook(() => @@ -179,7 +174,7 @@ describe('useAutoRefresh', () => { ) act(() => { - vi.advanceTimersByTime(15000) // 3 intervals + vi.advanceTimersByTime(15000) }) expect(onRefresh).toHaveBeenCalledTimes(3) @@ -187,7 +182,7 @@ describe('useAutoRefresh', () => { }) describe('countdown', () => { - it('should decrement countdown every second when enabled', () => { + it('decrements countdown every second when enabled', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -211,7 +206,7 @@ describe('useAutoRefresh', () => { expect(result.current.secondsUntilNextRefresh).toBe(3) }) - it('should reset countdown after reaching zero', () => { + it('resets countdown after reaching zero', () => { const onRefresh = vi.fn().mockResolvedValue(undefined) const { result } = renderHook(() => @@ -224,45 +219,11 @@ describe('useAutoRefresh', () => { expect(result.current.secondsUntilNextRefresh).toBe(3) - // Advance 3 seconds to reach zero act(() => { vi.advanceTimersByTime(3000) }) - // Should reset to initial value expect(result.current.secondsUntilNextRefresh).toBe(3) }) }) - - describe('cleanup', () => { - it('should stop refresh when disabled after being enabled', () => { - const onRefresh = vi.fn().mockResolvedValue(undefined) - - const { result } = renderHook(() => - useAutoRefresh({ - intervalMs: 5000, - onRefresh, - enabled: true, - }) - ) - - // Advance half interval - act(() => { - vi.advanceTimersByTime(2500) - }) - - // Disable - act(() => { - result.current.setEnabled(false) - }) - - // Advance another full interval - act(() => { - vi.advanceTimersByTime(10000) - }) - - // Should not have been called (disabled before first interval completed) - expect(onRefresh).not.toHaveBeenCalled() - }) - }) })