From 0c042ac89fc0822fcbe09c48702857faa5494ae1 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 7 Dec 2025 23:34:03 +0900
Subject: feat(client): add sync status indicator component
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add SyncStatusIndicator component to display current sync state in the
UI header. The component shows online/offline status, syncing progress,
pending changes count, and sync errors.

- Create SyncProvider context to wrap SyncManager for React components
- Add SyncStatusIndicator component with visual status indicators
- Integrate indicator into HomePage header
- Add comprehensive tests for SyncStatusIndicator and SyncProvider
- Update existing tests to include SyncProvider wrapper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/components/SyncStatusIndicator.test.tsx | 160 +++++++++++++++++++++
 1 file changed, 160 insertions(+)
 create mode 100644 src/client/components/SyncStatusIndicator.test.tsx

(limited to 'src/client/components/SyncStatusIndicator.test.tsx')

diff --git a/src/client/components/SyncStatusIndicator.test.tsx b/src/client/components/SyncStatusIndicator.test.tsx
new file mode 100644
index 0000000..a607e11
--- /dev/null
+++ b/src/client/components/SyncStatusIndicator.test.tsx
@@ -0,0 +1,160 @@
+/**
+ * @vitest-environment jsdom
+ */
+import "fake-indexeddb/auto";
+import { cleanup, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SyncStatusIndicator } from "./SyncStatusIndicator";
+
+// Mock the useSync hook
+const mockUseSync = vi.fn();
+vi.mock("../stores", () => ({
+	useSync: () => mockUseSync(),
+}));
+
+// Mock the SyncStatus constant
+vi.mock("../sync", () => ({
+	SyncStatus: {
+		Idle: "idle",
+		Syncing: "syncing",
+		Error: "error",
+	},
+}));
+
+describe("SyncStatusIndicator", () => {
+	beforeEach(() => {
+		vi.clearAllMocks();
+	});
+
+	afterEach(() => {
+		cleanup();
+	});
+
+	it("displays 'Synced' when online with no pending changes", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			pendingCount: 0,
+			lastError: null,
+			status: "idle",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Synced")).toBeDefined();
+		expect(screen.getByTestId("sync-status-indicator")).toBeDefined();
+	});
+
+	it("displays 'Offline' when not online", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			isSyncing: false,
+			pendingCount: 0,
+			lastError: null,
+			status: "idle",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Offline")).toBeDefined();
+	});
+
+	it("displays 'Syncing...' when syncing", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: true,
+			pendingCount: 0,
+			lastError: null,
+			status: "syncing",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Syncing...")).toBeDefined();
+	});
+
+	it("displays pending count when there are pending changes", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			pendingCount: 5,
+			lastError: null,
+			status: "idle",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("5 pending")).toBeDefined();
+	});
+
+	it("displays 'Sync error' when there is an error", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			pendingCount: 0,
+			lastError: "Network error",
+			status: "error",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Sync error")).toBeDefined();
+	});
+
+	it("shows error message in title when there is an error", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			pendingCount: 0,
+			lastError: "Network error",
+			status: "error",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		const indicator = screen.getByTestId("sync-status-indicator");
+		expect(indicator.getAttribute("title")).toBe("Network error");
+	});
+
+	it("prioritizes offline status over other states", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			isSyncing: true,
+			pendingCount: 5,
+			lastError: "Error",
+			status: "error",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Offline")).toBeDefined();
+	});
+
+	it("prioritizes syncing status over pending and error", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: true,
+			pendingCount: 5,
+			lastError: null,
+			status: "syncing",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Syncing...")).toBeDefined();
+	});
+
+	it("prioritizes error status over pending", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			pendingCount: 5,
+			lastError: "Network error",
+			status: "error",
+		});
+
+		render(<SyncStatusIndicator />);
+
+		expect(screen.getByText("Sync error")).toBeDefined();
+	});
+});
-- 
cgit v1.2.3-70-g09d2

