From 445781bc40afee2c64f645abcfa2575b4218aa08 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 7 Dec 2025 23:37:31 +0900
Subject: feat(client): add manual sync button
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add SyncButton component that allows users to manually trigger
data synchronization. The button is disabled when offline or
when sync is already in progress.

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

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

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

diff --git a/src/client/components/SyncButton.test.tsx b/src/client/components/SyncButton.test.tsx
new file mode 100644
index 0000000..c399284
--- /dev/null
+++ b/src/client/components/SyncButton.test.tsx
@@ -0,0 +1,145 @@
+/**
+ * @vitest-environment jsdom
+ */
+import "fake-indexeddb/auto";
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SyncButton } from "./SyncButton";
+
+// Mock the useSync hook
+const mockSync = vi.fn();
+const mockUseSync = vi.fn();
+vi.mock("../stores", () => ({
+	useSync: () => mockUseSync(),
+}));
+
+describe("SyncButton", () => {
+	beforeEach(() => {
+		vi.clearAllMocks();
+		mockSync.mockResolvedValue({ success: true });
+	});
+
+	afterEach(() => {
+		cleanup();
+	});
+
+	it("renders sync button", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		expect(screen.getByTestId("sync-button")).toBeDefined();
+		expect(screen.getByText("Sync")).toBeDefined();
+	});
+
+	it("displays 'Syncing...' when syncing", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: true,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		expect(screen.getByText("Syncing...")).toBeDefined();
+	});
+
+	it("is disabled when offline", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		expect(button).toHaveProperty("disabled", true);
+	});
+
+	it("is disabled when syncing", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: true,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		expect(button).toHaveProperty("disabled", true);
+	});
+
+	it("is enabled when online and not syncing", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		expect(button).toHaveProperty("disabled", false);
+	});
+
+	it("calls sync when clicked", async () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		fireEvent.click(button);
+
+		expect(mockSync).toHaveBeenCalledTimes(1);
+	});
+
+	it("does not call sync when clicked while disabled", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		fireEvent.click(button);
+
+		expect(mockSync).not.toHaveBeenCalled();
+	});
+
+	it("shows tooltip when offline", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		expect(button.getAttribute("title")).toBe("Cannot sync while offline");
+	});
+
+	it("does not show tooltip when online", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			isSyncing: false,
+			sync: mockSync,
+		});
+
+		render(<SyncButton />);
+
+		const button = screen.getByTestId("sync-button");
+		expect(button.getAttribute("title")).toBeNull();
+	});
+});
-- 
cgit v1.2.3-70-g09d2

