diff --git a/src/__tests__/ThemeProvider.test.tsx b/src/__tests__/ThemeProvider.test.tsx
new file mode 100644
index 0000000..f8a1231
--- /dev/null
+++ b/src/__tests__/ThemeProvider.test.tsx
@@ -0,0 +1,120 @@
+import React from "react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
+import { ThemeProvider, useTheme } from "@/contexts/ThemeContext";
+import ThemeToggle from "@/components/ThemeToggle";
+
+function TestConsumer() {
+ const { theme, resolvedTheme, setTheme, cycleTheme } = useTheme();
+ return (
+
+ {theme}
+ {resolvedTheme}
+
+
+
+
+
+ );
+}
+
+describe("ThemeProvider & ThemeToggle", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ document.documentElement.removeAttribute("data-theme");
+ document.documentElement.classList.remove("dark");
+
+ // Mock matchMedia
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: query.includes("dark"),
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("initializes with default system mode and applies data-theme attribute", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("system");
+ expect(screen.getByTestId("resolved-theme").textContent).toBe("dark");
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
+ });
+
+ it("persists theme change to localStorage and sets data-theme attribute", () => {
+ render(
+
+
+
+ );
+
+ const lightButton = screen.getByText("Set Light");
+ act(() => {
+ fireEvent.click(lightButton);
+ });
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("light");
+ expect(screen.getByTestId("resolved-theme").textContent).toBe("light");
+ expect(document.documentElement.getAttribute("data-theme")).toBe("light");
+ expect(localStorage.getItem("split-theme")).toBe("light");
+ });
+
+ it("restores theme from localStorage on initial render", () => {
+ localStorage.setItem("split-theme", "light");
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("light");
+ expect(document.documentElement.getAttribute("data-theme")).toBe("light");
+ });
+
+ it("ThemeToggle cycles through all 3 modes: light -> dark -> system", () => {
+ localStorage.setItem("split-theme", "light");
+
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("light");
+
+ const toggle = screen.getByRole("button", { name: /switch to dark mode/i });
+ act(() => {
+ fireEvent.click(toggle);
+ });
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("dark");
+
+ act(() => {
+ fireEvent.click(toggle);
+ });
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("system");
+
+ act(() => {
+ fireEvent.click(toggle);
+ });
+
+ expect(screen.getByTestId("current-theme").textContent).toBe("light");
+ });
+});
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a319e5f..4714ebf 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -31,6 +31,8 @@ const themeBootstrap = `
var stored = window.localStorage.getItem("split-theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var isDark = stored === "dark" || (!stored && prefersDark) || (stored === "system" && prefersDark);
+ var resolved = isDark ? "dark" : "light";
+ document.documentElement.setAttribute("data-theme", resolved);
if (isDark) {
document.documentElement.classList.add("dark");
} else {
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index a602c6e..a5a99d5 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -110,8 +110,8 @@ export default function Navbar() {
-
+
{/* New Invoice CTA — hidden below lg */}