diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-01-04 17:43:59 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-01-04 19:09:58 +0900 |
| commit | f8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch) | |
| tree | b2cf350d2e2e52803ff809311effb40da767d859 /src/client/stores/sync.test.tsx | |
| parent | e1c9e5e89bb91bca2586470c786510c3e1c03826 (diff) | |
| download | kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.gz kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.zst kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.zip | |
refactor(client): migrate state management from React Context to Jotai
Replace AuthProvider and SyncProvider with Jotai atoms for more granular
state management and better performance. This migration:
- Creates atoms for auth, sync, decks, cards, noteTypes, and study state
- Uses atomFamily for parameterized state (e.g., cards by deckId)
- Introduces StoreInitializer component for subscription initialization
- Updates all components and pages to use useAtomValue/useSetAtom
- Updates all tests to use Jotai Provider with createStore pattern
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/stores/sync.test.tsx')
| -rw-r--r-- | src/client/stores/sync.test.tsx | 234 |
1 files changed, 0 insertions, 234 deletions
diff --git a/src/client/stores/sync.test.tsx b/src/client/stores/sync.test.tsx deleted file mode 100644 index 20de69d..0000000 --- a/src/client/stores/sync.test.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import "fake-indexeddb/auto"; -import { act, renderHook, waitFor } from "@testing-library/react"; -import type { ReactNode } from "react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { db } from "../db/index"; -import { SyncProvider, useSync } from "./sync"; - -// Mock fetch globally -const mockFetch = vi.fn(); -global.fetch = mockFetch; - -// Mock apiClient -vi.mock("../api/client", () => ({ - apiClient: { - getAuthHeader: vi.fn(() => ({ Authorization: "Bearer token" })), - authenticatedFetch: vi.fn((input: RequestInfo | URL, init?: RequestInit) => - mockFetch(input, init), - ), - }, -})); - -const wrapper = ({ children }: { children: ReactNode }) => ( - <SyncProvider>{children}</SyncProvider> -); - -describe("useSync", () => { - beforeEach(async () => { - vi.clearAllMocks(); - localStorage.clear(); - await db.decks.clear(); - await db.cards.clear(); - await db.reviewLogs.clear(); - - // Default mock for fetch - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - decks: [], - cards: [], - reviewLogs: [], - noteTypes: [], - noteFieldTypes: [], - notes: [], - noteFieldValues: [], - conflicts: { - decks: [], - cards: [], - noteTypes: [], - noteFieldTypes: [], - notes: [], - noteFieldValues: [], - }, - currentSyncVersion: 0, - }), - }); - }); - - afterEach(async () => { - vi.restoreAllMocks(); - localStorage.clear(); - await db.decks.clear(); - await db.cards.clear(); - await db.reviewLogs.clear(); - }); - - it("throws error when used outside SyncProvider", () => { - // Suppress console.error for this test - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - expect(() => { - renderHook(() => useSync()); - }).toThrow("useSync must be used within a SyncProvider"); - - consoleSpy.mockRestore(); - }); - - it("returns initial state", async () => { - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(result.current.isOnline).toBe(true); - expect(result.current.isSyncing).toBe(false); - expect(result.current.pendingCount).toBe(0); - expect(result.current.lastSyncAt).toBeNull(); - expect(result.current.lastError).toBeNull(); - expect(result.current.status).toBe("idle"); - }); - }); - - it("provides sync function", async () => { - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(typeof result.current.sync).toBe("function"); - }); - }); - - it("updates isSyncing during sync", async () => { - // Make the sync take some time - mockFetch.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - ok: true, - json: async () => ({ - decks: [], - cards: [], - reviewLogs: [], - conflicts: { decks: [], cards: [] }, - currentSyncVersion: 0, - }), - }), - 50, - ), - ), - ); - - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(result.current.isSyncing).toBe(false); - }); - - // Start sync - let syncPromise: Promise<unknown>; - act(() => { - syncPromise = result.current.sync(); - }); - - // Check that isSyncing becomes true - await waitFor(() => { - expect(result.current.isSyncing).toBe(true); - }); - - // Wait for sync to complete - await act(async () => { - await syncPromise; - }); - - expect(result.current.isSyncing).toBe(false); - }); - - it("updates lastSyncAt after successful sync", async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - decks: [], - cards: [], - reviewLogs: [], - noteTypes: [], - noteFieldTypes: [], - notes: [], - noteFieldValues: [], - conflicts: { - decks: [], - cards: [], - noteTypes: [], - noteFieldTypes: [], - notes: [], - noteFieldValues: [], - }, - currentSyncVersion: 1, - }), - }); - - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(result.current.lastSyncAt).toBeNull(); - }); - - await act(async () => { - await result.current.sync(); - }); - - await waitFor(() => { - expect(result.current.lastSyncAt).not.toBeNull(); - }); - }); - - it("updates lastError on sync failure", async () => { - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - json: async () => ({ error: "Server error" }), - }); - - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(result.current.lastError).toBeNull(); - }); - - await act(async () => { - await result.current.sync(); - }); - - await waitFor(() => { - expect(result.current.lastError).toBe("Server error"); - expect(result.current.status).toBe("error"); - }); - }); - - it("responds to online/offline events", async () => { - const { result } = renderHook(() => useSync(), { wrapper }); - - await waitFor(() => { - expect(result.current.isOnline).toBe(true); - }); - - // Simulate going offline - act(() => { - window.dispatchEvent(new Event("offline")); - }); - - await waitFor(() => { - expect(result.current.isOnline).toBe(false); - }); - - // Simulate going online - act(() => { - window.dispatchEvent(new Event("online")); - }); - - await waitFor(() => { - expect(result.current.isOnline).toBe(true); - }); - }); -}); |
