/**
* @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" })),
},
}));
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: [],
conflicts: { decks: [], cards: [] },
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: [],
conflicts: { decks: [], cards: [] },
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);
});
});
});