/** * 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
Captured notification service
; } render( , ); expect(serviceWorks).toBe(true); }); // ----------------------------------------------------------------------- // Test 9: Full app provider stack renders without errors // ----------------------------------------------------------------------- it("complete App provider stack mounts successfully", async () => { // Mock the session validation vi.spyOn(clientModule, "setUnauthorizedHandler").mockImplementation( (_handler: (() => void) | null) => { // Mock implementation }, ); // Note: This tests basic mounting; full App integration is tested in App.test.tsx render( Dashboard} /> , ); await waitFor(() => { expect(screen.queryByText("Dashboard")).toBeInTheDocument(); }); }); }); // --------------------------------------------------------------------------- // Helper Components // --------------------------------------------------------------------------- function ThemeDisplay(): ReactElement { const { colorMode } = useThemeMode(); return {colorMode}; }