Refactor provider composition and ESLint configuration
- 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>
This commit is contained in:
359
frontend/src/providers/__tests__/providerOrderValidator.test.tsx
Normal file
359
frontend/src/providers/__tests__/providerOrderValidator.test.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 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 />);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user