feat(#415): theme fix, AuthDivider, SessionExpiryWarning components
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
ci/woodpecker/push/api Pipeline was successful

- AUTH-014: Fix theme storage key (jarvis-theme -> mosaic-theme)
- AUTH-016: Create AuthDivider component with customizable text
- AUTH-019: Create SessionExpiryWarning floating banner (PDA-friendly, blue)
- Fix lint errors in LoginForm, OAuthButton from parallel agents
- Sync pnpm-lock.yaml for recharts dependency

Refs #415

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 11:37:31 -06:00
parent 9623a3be97
commit 81b5204258
13 changed files with 899 additions and 4 deletions

View File

@@ -0,0 +1,120 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import { ThemeProvider, useTheme } from "./ThemeProvider";
function ThemeConsumer(): React.JSX.Element {
const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
return (
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="resolved">{resolvedTheme}</span>
<button
onClick={() => {
setTheme("light");
}}
>
Set Light
</button>
<button
onClick={() => {
setTheme("dark");
}}
>
Set Dark
</button>
<button
onClick={() => {
toggleTheme();
}}
>
Toggle
</button>
</div>
);
}
describe("ThemeProvider", (): void => {
let mockMatchMedia: ReturnType<typeof vi.fn>;
beforeEach((): void => {
localStorage.clear();
document.documentElement.classList.remove("light", "dark");
mockMatchMedia = vi.fn().mockReturnValue({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
Object.defineProperty(window, "matchMedia", {
writable: true,
value: mockMatchMedia,
});
});
afterEach((): void => {
vi.restoreAllMocks();
});
it("should use 'mosaic-theme' as storage key", (): void => {
localStorage.setItem("mosaic-theme", "light");
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
);
expect(screen.getByTestId("theme")).toHaveTextContent("light");
});
it("should NOT read from old 'jarvis-theme' storage key", (): void => {
localStorage.setItem("jarvis-theme", "light");
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
);
// Should default to system, not read from jarvis-theme
expect(screen.getByTestId("theme")).toHaveTextContent("system");
});
it("should store theme under 'mosaic-theme' key", (): void => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
);
act(() => {
screen.getByText("Set Light").click();
});
expect(localStorage.getItem("mosaic-theme")).toBe("light");
expect(localStorage.getItem("jarvis-theme")).toBeNull();
});
it("should render children", (): void => {
render(
<ThemeProvider>
<div data-testid="child">Hello</div>
</ThemeProvider>
);
expect(screen.getByTestId("child")).toBeInTheDocument();
});
it("should throw when useTheme is used outside provider", (): void => {
// Suppress console.error for expected error
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
// Intentionally empty
});
expect(() => {
render(<ThemeConsumer />);
}).toThrow("useTheme must be used within a ThemeProvider");
consoleSpy.mockRestore();
});
});

View File

@@ -13,7 +13,7 @@ interface ThemeContextValue {
const ThemeContext = createContext<ThemeContextValue | null>(null);
const STORAGE_KEY = "jarvis-theme";
const STORAGE_KEY = "mosaic-theme";
function getSystemTheme(): "light" | "dark" {
if (typeof window === "undefined") return "dark";