- Add new provider composition system with validation - Create providerComposition.tsx for centralized provider management - Implement providerOrderValidator.tsx to ensure correct provider order - Add comprehensive tests for provider composition - Create custom ESLint rules in frontend/eslint-rules/ - Update ESLint configuration - Update architecture and tasks documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
/**
|
|
* 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 <div>Should not render</div>;
|
|
}
|
|
|
|
expect(() => {
|
|
render(<InvalidComponent />);
|
|
}).toThrow(/missing required parent providers/i);
|
|
});
|
|
|
|
it("allows ThemeProvider at position 0 without parent tracking", () => {
|
|
function ValidThemeComponent(): React.JSX.Element {
|
|
validateProviderPosition("ThemeProvider", 0);
|
|
return <div data-testid="theme">Theme OK</div>;
|
|
}
|
|
|
|
const { getByTestId } = render(<ValidThemeComponent />);
|
|
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 <div>Should not render</div>;
|
|
}
|
|
|
|
expect(() => {
|
|
render(<NeedsProviders />);
|
|
}).toThrow(/missing required providers/i);
|
|
});
|
|
|
|
it("allows validation when tracking context exists with all required providers", () => {
|
|
function ValidatedComponent(): React.JSX.Element {
|
|
validateProvidersExist(["ThemeProvider"]);
|
|
return <div data-testid="valid">Valid</div>;
|
|
}
|
|
|
|
const composition = (
|
|
<div>
|
|
{createProviderTracker("ThemeProvider", 0, <ValidatedComponent />)}
|
|
</div>
|
|
);
|
|
|
|
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 <div>Check done</div>;
|
|
}
|
|
|
|
render(
|
|
createProviderTracker("ThemeProvider", 0, <CheckProvider />)
|
|
);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false when provider is not present", () => {
|
|
let result = true;
|
|
|
|
function CheckProvider(): React.JSX.Element {
|
|
result = hasProvider("TimezoneProvider");
|
|
return <div>Check done</div>;
|
|
}
|
|
|
|
render(
|
|
createProviderTracker("ThemeProvider", 0, <CheckProvider />)
|
|
);
|
|
|
|
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 <div>Check done</div>;
|
|
}
|
|
|
|
render(<CheckProviders />);
|
|
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 <div>Check done</div>;
|
|
}
|
|
|
|
const composition = (
|
|
<div>
|
|
{createProviderTracker("ThemeProvider", 0,
|
|
createProviderTracker("FluentProvider", 1,
|
|
createProviderTracker("NotificationProvider", 2, <CheckProviders />)
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
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 <div>Invalid</div>;
|
|
}
|
|
|
|
// 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, <Component />));
|
|
}).toThrow(/invalid position/i);
|
|
});
|
|
|
|
it("throws error when parent provider is missing in tracker", () => {
|
|
function Component(): React.JSX.Element {
|
|
return <div>Should not render</div>;
|
|
}
|
|
|
|
expect(() => {
|
|
// NotificationProvider (position 2) requires FluentProvider (position 1)
|
|
// but we're skipping FluentProvider
|
|
render(
|
|
<div>
|
|
{createProviderTracker("ThemeProvider", 0,
|
|
createProviderTracker("NotificationProvider", 2, <Component />)
|
|
)}
|
|
</div>
|
|
);
|
|
}).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 <div>Checking</div>;
|
|
}
|
|
|
|
render(<ComponentNeedsAuth />);
|
|
|
|
// 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 <div data-testid="ok">OK</div>;
|
|
}
|
|
|
|
const composition = (
|
|
<div>
|
|
{createProviderTracker("ThemeProvider", 0,
|
|
createProviderTracker("FluentProvider", 1, <ComponentNeedsAuth />)
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
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 <div data-testid="inner">Inner</div>;
|
|
}
|
|
|
|
const composition = (
|
|
<div>
|
|
{createProviderTracker("ThemeProvider", 0,
|
|
createProviderTracker("FluentProvider", 1,
|
|
createProviderTracker("NotificationProvider", 2,
|
|
createProviderTracker("ErrorBoundary", 3,
|
|
createProviderTracker("BrowserRouter", 4, <InnerComponent />)
|
|
)
|
|
)
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
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 <div>Error caught</div>;
|
|
}
|
|
|
|
render(<Component />);
|
|
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 <div>Error caught</div>;
|
|
}
|
|
|
|
render(<Component />);
|
|
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 <div>Error caught</div>;
|
|
}
|
|
|
|
render(<Component />);
|
|
});
|
|
|
|
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 <div>Error caught</div>;
|
|
}
|
|
|
|
render(<Component />);
|
|
});
|
|
});
|