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:
2026-05-01 17:33:56 +02:00
parent 4f7316c484
commit c988b4b8b6
9 changed files with 1580 additions and 50 deletions

View 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 />);
});
});