/**
* Provider Composition Tests
*
* Validates that providers are nested in the correct order and that their
* dependencies are satisfied. These tests ensure that the provider hierarchy
* contract is maintained across refactors.
*
* Key invariants tested:
* - All providers mount without crashing
* - Providers are accessible from their descendant components
* - Order-dependent initialization (auth, timezone) works correctly
* - Theme persistence works across re-renders
* - Notifications and errors propagate correctly
*/
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { type ReactElement } from "react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { AuthProvider } from "../AuthProvider";
import { ThemeProvider, useThemeMode } from "../ThemeProvider";
import { TimezoneProvider } from "../TimezoneProvider";
import { useTimezone } from "../../hooks/useTimezone";
import { NotificationProvider, useNotification } from "../../services/notificationService";
import * as clientModule from "../../api/client";
// ---------------------------------------------------------------------------
// Fixture: Test component that consumes all providers
// ---------------------------------------------------------------------------
function AllProvidersConsumer(): ReactElement {
const { colorMode } = useThemeMode();
const notification = useNotification();
const timezone = useTimezone();
return (
{colorMode}
{timezone}
);
}
// ---------------------------------------------------------------------------
// Test Suite: Provider Composition
// ---------------------------------------------------------------------------
describe("Provider Composition Contract", () => {
beforeEach(() => {
sessionStorage.clear();
localStorage.clear();
vi.clearAllMocks();
vi.restoreAllMocks();
// Mock window.matchMedia for theme preference
Object.defineProperty(window, "matchMedia", {
configurable: true,
writable: true,
value: vi.fn().mockReturnValue({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}),
});
});
afterEach(() => {
vi.clearAllMocks();
});
// -----------------------------------------------------------------------
// Test 1: ThemeProvider must be the outermost provider
// -----------------------------------------------------------------------
it("ThemeProvider supplies theme to AppContents for FluentProvider", () => {
function InnerApp(): ReactElement {
const { colorMode } = useThemeMode();
return
{colorMode}
;
}
render(
,
);
const colorOutput = screen.getByTestId("color-output");
expect(colorOutput.textContent).toMatch(/light|dark/);
});
it("FluentProvider must receive theme from useThemeMode", () => {
function ConsumerComponent(): ReactElement {
return
Fluent content rendered
;
}
const { container } = render(
,
);
expect(screen.getByTestId("fluent-content")).toBeInTheDocument();
// Verify FluentProvider rendered (check for Fluent-specific class)
expect(container.querySelector("[class*='fui']")).toBeTruthy();
});
// -----------------------------------------------------------------------
// Test 2: NotificationProvider must be after FluentProvider but before
// error boundaries and content
// -----------------------------------------------------------------------
it("NotificationProvider makes useNotification available to children", () => {
function NotificationConsumer(): ReactElement {
const notification = useNotification();
return (
);
}
render(
,
);
expect(screen.getByTestId("notify-btn")).toBeInTheDocument();
});
it("throws error when useNotification is called outside NotificationProvider", () => {
function BadComponent(): ReactElement {
useNotification();
return
This should not render
;
}
expect(() => {
render(
,
);
}).toThrow("useNotification must be used within NotificationProvider");
});
// -----------------------------------------------------------------------
// Test 3: AuthProvider must be inside BrowserRouter
// -----------------------------------------------------------------------
it("AuthProvider must be inside BrowserRouter (uses useNavigate internally)", () => {
// This test verifies that AuthProvider can be placed inside BrowserRouter
// by confirming it doesn't throw when nested correctly
render(
Home} />
,
);
expect(screen.getByText("Home")).toBeInTheDocument();
});
it("throws error when AuthProvider is outside BrowserRouter", () => {
function RootComponent(): ReactElement {
return (
Content
);
}
expect(() => {
render(
,
);
}).toThrow(/useNavigate|useLocation|outside/i);
});
// -----------------------------------------------------------------------
// Test 4: TimezoneProvider must be inside authenticated context
// -----------------------------------------------------------------------
it("TimezoneProvider provides timezone to consumers", () => {
function TimezoneConsumer(): ReactElement {
const timezone = useTimezone();
return
{timezone}
;
}
render(
,
);
const tzOutput = screen.getByTestId("tz-output");
expect(tzOutput.textContent).toBeTruthy();
});
it("defaults to UTC when timezone data is loading or unavailable", async () => {
function TimezoneDisplay(): ReactElement {
const timezone = useTimezone();
return
{timezone}
;
}
render(
,
);
// Should default to UTC while loading
const tzDisplay = screen.getByTestId("tz-display");
expect(tzDisplay.textContent).toMatch(/UTC/);
});
// -----------------------------------------------------------------------
// Test 5: Provider initialization order is respected
// -----------------------------------------------------------------------
it("renders all providers without errors when properly nested", () => {
render(
,
);
expect(screen.getByTestId("theme")).toBeInTheDocument();
expect(screen.getByTestId("timezone")).toBeInTheDocument();
expect(screen.getByTestId("notify-btn")).toBeInTheDocument();
});
// -----------------------------------------------------------------------
// Test 6: Async provider initialization (AuthProvider)
// -----------------------------------------------------------------------
it("AuthProvider shows loading state while validating session", async () => {
vi.spyOn(clientModule, "setUnauthorizedHandler").mockImplementation(
(_handler: (() => void) | null) => {
// Mock implementation
},
);
sessionStorage.setItem("bangui_authenticated", "true");
render(
Authenticated content} />
,
);
// AuthProvider should eventually render authenticated content
await waitFor(() => {
expect(screen.queryByText("Authenticated content")).toBeInTheDocument();
});
});
// -----------------------------------------------------------------------
// Test 7: Theme persistence across provider remounts
// -----------------------------------------------------------------------
it("persists theme selection across re-renders", () => {
localStorage.setItem("bangui_theme", "dark");
const { rerender } = render(
,
);
expect(screen.getByTestId("theme-color")).toHaveTextContent("dark");
// Re-render should preserve theme
rerender(
,
);
expect(screen.getByTestId("theme-color")).toHaveTextContent("dark");
});
// -----------------------------------------------------------------------
// Test 8: Error boundary receives notifications
// -----------------------------------------------------------------------
it("NotificationProvider is accessible from error scenarios", () => {
// This tests that NotificationProvider is positioned correctly
// so that error boundaries and other error handlers can use it
let serviceWorks = false;
function CaptureNotification(): ReactElement {
const notification = useNotification();
// Just verify it's callable - don't call it as it mutates state
serviceWorks = typeof notification.success === "function";
return