aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/stores/sync.test.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-01-04 17:43:59 +0900
committernsfisis <nsfisis@gmail.com>2026-01-04 19:09:58 +0900
commitf8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch)
treeb2cf350d2e2e52803ff809311effb40da767d859 /src/client/stores/sync.test.tsx
parente1c9e5e89bb91bca2586470c786510c3e1c03826 (diff)
downloadkioku-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.tsx234
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);
- });
- });
-});