/** * Tests for Provider Order Validator * * Validates runtime checking of provider ordering through context tracking. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { render } from "@testing-library/react"; import { validateProviderPosition, validateProvidersExist, hasProvider, getCurrentProviders, createProviderTracker, useProviderValidation, } from "../providerOrderValidator"; describe("ProviderOrderValidator", () => { beforeEach(() => { vi.clearAllMocks(); }); // ----------------------------------------------------------------------- // Test 1: validateProviderPosition detects missing parent providers // ----------------------------------------------------------------------- it("throws error when provider parent is missing", () => { function InvalidComponent(): React.JSX.Element { validateProviderPosition("AuthProvider", 6); return
Should not render
; } expect(() => { render(); }).toThrow(/missing required parent providers/i); }); it("allows ThemeProvider at position 0 without parent tracking", () => { function ValidThemeComponent(): React.JSX.Element { validateProviderPosition("ThemeProvider", 0); return
Theme OK
; } const { getByTestId } = render(); expect(getByTestId("theme")).toBeInTheDocument(); }); // ----------------------------------------------------------------------- // Test 2: validateProvidersExist checks for required providers // ----------------------------------------------------------------------- it("throws error when required providers are missing", () => { function NeedsProviders(): React.JSX.Element { validateProvidersExist(["AuthProvider", "NotificationProvider"]); return
Should not render
; } expect(() => { render(); }).toThrow(/missing required providers/i); }); it("allows validation when tracking context exists with all required providers", () => { function ValidatedComponent(): React.JSX.Element { validateProvidersExist(["ThemeProvider"]); return
Valid
; } const composition = (
{createProviderTracker("ThemeProvider", 0, )}
); const { getByTestId } = render(composition); expect(getByTestId("valid")).toBeInTheDocument(); }); // ----------------------------------------------------------------------- // Test 3: hasProvider checks provider presence // ----------------------------------------------------------------------- it("returns true when provider is present", () => { let result = false; function CheckProvider(): React.JSX.Element { result = hasProvider("ThemeProvider"); return
Check done
; } render( createProviderTracker("ThemeProvider", 0, ) ); expect(result).toBe(true); }); it("returns false when provider is not present", () => { let result = true; function CheckProvider(): React.JSX.Element { result = hasProvider("TimezoneProvider"); return
Check done
; } render( createProviderTracker("ThemeProvider", 0, ) ); expect(result).toBe(false); }); // ----------------------------------------------------------------------- // Test 4: getCurrentProviders returns providers in order // ----------------------------------------------------------------------- it("returns empty array when no providers are tracked", () => { let providers: string[] = []; function CheckProviders(): React.JSX.Element { const result = getCurrentProviders(); providers = result; return
Check done
; } render(); expect(providers).toEqual([]); }); it("returns providers in correct order when tracked", () => { let providers: string[] = []; function CheckProviders(): React.JSX.Element { const result = getCurrentProviders(); providers = result; return
Check done
; } const composition = (
{createProviderTracker("ThemeProvider", 0, createProviderTracker("FluentProvider", 1, createProviderTracker("NotificationProvider", 2, ) ) )}
); render(composition); expect(providers).toEqual(["ThemeProvider", "FluentProvider", "NotificationProvider"]); }); // ----------------------------------------------------------------------- // Test 5: createProviderTracker validates positioning // ----------------------------------------------------------------------- it("throws error for invalid position in createProviderTracker", () => { function Component(): React.JSX.Element { return
Invalid
; } // The error should be thrown during render, not construction expect(() => { // Using any to test invalid position despite type checking // eslint-disable-next-line @typescript-eslint/no-explicit-any render(createProviderTracker("ThemeProvider" as any, 999, )); }).toThrow(/invalid position/i); }); it("throws error when parent provider is missing in tracker", () => { function Component(): React.JSX.Element { return
Should not render
; } expect(() => { // NotificationProvider (position 2) requires FluentProvider (position 1) // but we're skipping FluentProvider render(
{createProviderTracker("ThemeProvider", 0, createProviderTracker("NotificationProvider", 2, ) )}
); }).toThrow(/incorrect parent structure|missing/i); }); // ----------------------------------------------------------------------- // Test 6: useProviderValidation hook validates providers // ----------------------------------------------------------------------- it("logs warning in development when required providers are missing", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); function ComponentNeedsAuth(): React.JSX.Element { useProviderValidation("ComponentNeedsAuth", ["AuthProvider"]); return
Checking
; } render(); // In development, should warn about missing provider const hasWarning = warnSpy.mock.calls.some((call) => String(call[0] || "").includes("ComponentNeedsAuth") || String(call[0] || "").includes("missing") ); // Only verify warning if NODE_ENV is development if (process.env.NODE_ENV === "development") { expect(hasWarning).toBe(true); } warnSpy.mockRestore(); }); it("does not warn when all required providers are present", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); function ComponentNeedsAuth(): React.JSX.Element { useProviderValidation("ComponentNeedsAuth", ["ThemeProvider"]); return
OK
; } const composition = (
{createProviderTracker("ThemeProvider", 0, createProviderTracker("FluentProvider", 1, ) )}
); render(composition); // Should not warn about missing ThemeProvider const warnCalls = warnSpy.mock.calls; const hasThemeWarning = warnCalls.some((call) => String(call[0] || "").includes("missing") && String(call[0] || "").includes("ThemeProvider") ); expect(hasThemeWarning).toBe(false); warnSpy.mockRestore(); }); // ----------------------------------------------------------------------- // Test 7: Nested provider tracking // ----------------------------------------------------------------------- it("correctly tracks nested providers through multiple levels", () => { let capturedProviders: string[] = []; function InnerComponent(): React.JSX.Element { capturedProviders = getCurrentProviders(); return
Inner
; } const composition = (
{createProviderTracker("ThemeProvider", 0, createProviderTracker("FluentProvider", 1, createProviderTracker("NotificationProvider", 2, createProviderTracker("ErrorBoundary", 3, createProviderTracker("BrowserRouter", 4, ) ) ) ) )}
); const { getByTestId } = render(composition); expect(getByTestId("inner")).toBeInTheDocument(); expect(capturedProviders).toEqual([ "ThemeProvider", "FluentProvider", "NotificationProvider", "ErrorBoundary", "BrowserRouter", ]); }); // ----------------------------------------------------------------------- // Test 8: Validation errors are descriptive // ----------------------------------------------------------------------- it("provides helpful error messages for missing providers", () => { let errorCaught = false; function Component(): React.JSX.Element { try { validateProvidersExist(["AuthProvider", "TimezoneProvider"]); } catch (error) { if (error instanceof Error) { expect(error.message).toMatch(/missing required providers/i); errorCaught = true; } } return
Error caught
; } render(); expect(errorCaught).toBe(true); }); it("provides helpful error messages for incorrect positioning", () => { let errorCaught = false; function Component(): React.JSX.Element { try { validateProviderPosition("AuthProvider", 6); } catch (error) { if (error instanceof Error) { expect(error.message).toMatch(/missing required parent/i); errorCaught = true; } } return
Error caught
; } render(); expect(errorCaught).toBe(true); }); // ----------------------------------------------------------------------- // Test 9: Invalid position numbers are rejected // ----------------------------------------------------------------------- it("throws error for negative position", () => { function Component(): React.JSX.Element { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any validateProviderPosition("InvalidProvider" as any, -1); } catch (error) { if (error instanceof Error) { expect(error.message).toMatch(/invalid expected position/i); } } return
Error caught
; } render(); }); it("throws error for position beyond sequence length", () => { function Component(): React.JSX.Element { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any validateProviderPosition("InvalidProvider" as any, 999); } catch (error) { if (error instanceof Error) { expect(error.message).toMatch(/invalid expected position/i); } } return
Error caught
; } render(); }); });