import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { SyncStatus } from "@/components/SyncStatus"; describe("SyncStatus", () => { beforeEach(() => { // Mock Date.now for consistent time-based tests vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-26T12:00:00Z")); }); afterEach(() => { vi.useRealTimers(); }); describe("synced state", () => { it("shows green indicator when synced", () => { const lastSync = new Date("2026-02-26T11:58:00Z"); // 2 minutes ago render(); const indicator = screen.getByTestId("sync-indicator"); expect(indicator).toHaveClass("bg-green-500"); }); it("shows relative time for recent sync", () => { const lastSync = new Date("2026-02-26T11:58:00Z"); // 2 minutes ago render(); expect(screen.getByText(/synced 2m ago/i)).toBeInTheDocument(); }); it("shows seconds for very recent sync", () => { const lastSync = new Date("2026-02-26T11:59:30Z"); // 30 seconds ago render(); expect(screen.getByText(/synced.*30s ago/i)).toBeInTheDocument(); }); it("shows 'just now' for immediate sync", () => { const lastSync = new Date("2026-02-26T12:00:00Z"); // right now render(); expect(screen.getByText(/synced just now/i)).toBeInTheDocument(); }); }); describe("syncing state", () => { it("shows spinner when syncing", () => { render(); expect(screen.getByTestId("sync-spinner")).toBeInTheDocument(); }); it("shows 'Syncing...' text", () => { render(); expect(screen.getByText("Syncing...")).toBeInTheDocument(); }); it("does not show indicator dot when syncing", () => { render(); expect(screen.queryByTestId("sync-indicator")).not.toBeInTheDocument(); }); }); describe("stale state", () => { it("shows amber indicator when explicitly stale", () => { const lastSync = new Date("2026-02-26T11:40:00Z"); // 20 minutes ago render(); const indicator = screen.getByTestId("sync-indicator"); expect(indicator).toHaveClass("bg-amber-500"); }); it("auto-detects stale when synced status but time > 15 minutes", () => { const lastSync = new Date("2026-02-26T11:40:00Z"); // 20 minutes ago render(); // Should show amber (stale) even though status is "synced" const indicator = screen.getByTestId("sync-indicator"); expect(indicator).toHaveClass("bg-amber-500"); }); it("shows 'Last sync' text for stale", () => { const lastSync = new Date("2026-02-26T11:40:00Z"); // 20 minutes ago render(); expect(screen.getByText(/last sync 20m ago/i)).toBeInTheDocument(); }); it("shows refresh button when stale", () => { const lastSync = new Date("2026-02-26T11:40:00Z"); render( {}} />); expect(screen.getByRole("button", { name: /refresh/i })).toBeInTheDocument(); }); }); describe("error state", () => { it("shows red indicator for error", () => { render(); const indicator = screen.getByTestId("sync-indicator"); expect(indicator).toHaveClass("bg-red-500"); }); it("shows error message", () => { render(); expect(screen.getByText(/lore command failed/)).toBeInTheDocument(); }); it("shows retry button", () => { render( {}} />); expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument(); }); it("calls onRetry when retry button clicked", async () => { // Use real timers for this test since userEvent needs them vi.useRealTimers(); const user = userEvent.setup(); const onRetry = vi.fn(); render(); await user.click(screen.getByRole("button", { name: /retry/i })); expect(onRetry).toHaveBeenCalledTimes(1); // Restore fake timers for subsequent tests vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-26T12:00:00Z")); }); it("shows generic message when no error provided", () => { render(); expect(screen.getByText(/sync failed/i)).toBeInTheDocument(); }); }); describe("offline state", () => { it("shows gray indicator when offline", () => { render(); const indicator = screen.getByTestId("sync-indicator"); expect(indicator).toHaveClass("bg-gray-400"); }); it("shows 'lore unavailable' message", () => { render(); expect(screen.getByText(/lore unavailable/i)).toBeInTheDocument(); }); it("does not show retry button when offline", () => { render( {}} />); // Offline doesn't get a retry button (user needs to fix lore first) expect(screen.queryByRole("button", { name: /retry/i })).not.toBeInTheDocument(); }); }); describe("relative time formatting", () => { it("formats hours correctly", () => { const lastSync = new Date("2026-02-26T10:00:00Z"); // 2 hours ago render(); expect(screen.getByText(/2h ago/i)).toBeInTheDocument(); }); it("formats days correctly", () => { const lastSync = new Date("2026-02-24T12:00:00Z"); // 2 days ago render(); expect(screen.getByText(/2d ago/i)).toBeInTheDocument(); }); }); describe("no onRetry callback", () => { it("hides retry button when no onRetry provided for error", () => { render(); expect(screen.queryByRole("button", { name: /retry/i })).not.toBeInTheDocument(); }); it("hides refresh button when no onRetry provided for stale", () => { const lastSync = new Date("2026-02-26T11:40:00Z"); render(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); }); });