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();
});
});
});