From f0635f9beb0ff85bbea2264a1b19160f0beed257 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 7 Dec 2025 23:41:28 +0900
Subject: feat(client): add offline mode banner indicator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Displays a prominent banner at the top of all pages when the user
is offline, informing them that changes will sync when reconnected.
Shows pending change count when applicable.

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

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

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

diff --git a/src/client/components/OfflineBanner.test.tsx b/src/client/components/OfflineBanner.test.tsx
new file mode 100644
index 0000000..41679d9
--- /dev/null
+++ b/src/client/components/OfflineBanner.test.tsx
@@ -0,0 +1,85 @@
+/**
+ * @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 { OfflineBanner } from "./OfflineBanner";
+
+// Mock the useSync hook
+const mockUseSync = vi.fn();
+vi.mock("../stores", () => ({
+	useSync: () => mockUseSync(),
+}));
+
+describe("OfflineBanner", () => {
+	beforeEach(() => {
+		vi.clearAllMocks();
+	});
+
+	afterEach(() => {
+		cleanup();
+	});
+
+	it("renders nothing when online", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: true,
+			pendingCount: 0,
+		});
+
+		render(<OfflineBanner />);
+
+		expect(screen.queryByTestId("offline-banner")).toBeNull();
+	});
+
+	it("renders banner when offline", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			pendingCount: 0,
+		});
+
+		render(<OfflineBanner />);
+
+		const banner = screen.getByTestId("offline-banner");
+		expect(banner).toBeDefined();
+		expect(
+			screen.getByText(/You're offline. Changes will sync when you reconnect./),
+		).toBeDefined();
+	});
+
+	it("displays pending count when offline with pending changes", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			pendingCount: 5,
+		});
+
+		render(<OfflineBanner />);
+
+		expect(screen.getByTestId("offline-pending-count")).toBeDefined();
+		expect(screen.getByText("(5 pending)")).toBeDefined();
+	});
+
+	it("does not display pending count when there are no pending changes", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			pendingCount: 0,
+		});
+
+		render(<OfflineBanner />);
+
+		expect(screen.queryByTestId("offline-pending-count")).toBeNull();
+	});
+
+	it("has correct accessibility attributes", () => {
+		mockUseSync.mockReturnValue({
+			isOnline: false,
+			pendingCount: 0,
+		});
+
+		render(<OfflineBanner />);
+
+		const banner = screen.getByTestId("offline-banner");
+		expect(banner.getAttribute("role")).toBe("status");
+		expect(banner.getAttribute("aria-live")).toBe("polite");
+	});
+});
-- 
cgit v1.2.3-70-g09d2

