From 445781bc40afee2c64f645abcfa2575b4218aa08 Mon Sep 17 00:00:00 2001 From: nsfisis 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 --- 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(); + + 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(); + + expect(screen.getByText("Syncing...")).toBeDefined(); + }); + + it("is disabled when offline", () => { + mockUseSync.mockReturnValue({ + isOnline: false, + isSyncing: false, + sync: mockSync, + }); + + render(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + const button = screen.getByTestId("sync-button"); + expect(button.getAttribute("title")).toBeNull(); + }); +}); -- cgit v1.2.3-70-g09d2