From f8e4be9b36a16969ac53bd9ce12ce8064be10196 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Jan 2026 17:43:59 +0900 Subject: refactor(client): migrate state management from React Context to Jotai MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/client/stores/sync.test.tsx | 234 ---------------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 src/client/stores/sync.test.tsx (limited to 'src/client/stores/sync.test.tsx') 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 }) => ( - {children} -); - -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; - 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); - }); - }); -}); -- cgit v1.2.3-70-g09d2