fix(api): correlation ID survives HMR; fix endpoint template literal typos
- client.ts: store correlation ID in sessionStorage so HMR (module re-eval) does not generate a new ID mid-session; add clearSessionCorrelationId() - endpoints.ts: fix 3 template literal trailing-quote bugs (missing ')' chars); replace template literals with string concat for encodeURIComponent calls - AuthProvider.tsx: call clearSessionCorrelationId() on logout - App.tsx: reorder ThemeProvider import before AuthProvider per PROVIDER_ORDER.md; indent Routes inside AuthProvider to match expected tree structure - Tasks.md: update task status - providerTreeOrder.test.tsx: add integration tests for provider nesting order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -56,7 +56,7 @@ import React, {
|
||||
} from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as authApi from "../api/auth";
|
||||
import { setUnauthorizedHandler, resetLogoutState } from "../api/client";
|
||||
import { setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
|
||||
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
|
||||
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
|
||||
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
||||
@@ -215,6 +215,7 @@ export function AuthProvider({
|
||||
// Always clear local state even if the API call fails (e.g. expired session).
|
||||
sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
|
||||
setIsAuthenticated(false);
|
||||
clearSessionCorrelationId();
|
||||
|
||||
// Broadcast logout event to other tabs for immediate sync
|
||||
try {
|
||||
|
||||
219
frontend/src/providers/__tests__/providerTreeOrder.test.tsx
Normal file
219
frontend/src/providers/__tests__/providerTreeOrder.test.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Integration Tests for Provider Tree Order (Issue #54)
|
||||
*
|
||||
* Verifies that the provider tree is correctly nested per PROVIDER_ORDER.md.
|
||||
* These tests complement compile-time checks by validating runtime structure.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { createProviderComposition } from "../providerComposition";
|
||||
import { ThemeProvider } from "../ThemeProvider";
|
||||
import { AuthProvider } from "../AuthProvider";
|
||||
import { TimezoneProvider } from "../TimezoneProvider";
|
||||
import { NavigationCancellationProvider } from "../NavigationCancellationProvider";
|
||||
import * as clientModule from "../../api/client";
|
||||
import * as authModule from "../../api/auth";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: Simple component to verify each provider's context is accessible
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function AuthConsumer(): React.JSX.Element {
|
||||
return <div data-testid="auth-available">Auth available</div>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test Suite: Provider Tree Order Integration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Provider Tree Order Integration", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockReturnValue({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
}),
|
||||
});
|
||||
|
||||
vi.spyOn(clientModule, "setUnauthorizedHandler").mockImplementation(
|
||||
(_handler: (() => void) | null) => {},
|
||||
);
|
||||
|
||||
// Mock session validation to prevent auth errors
|
||||
vi.spyOn(authModule, "validateSession").mockResolvedValue({
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 1: createProviderComposition renders providers without crashing
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("builder creates composition that renders without errors", async () => {
|
||||
const composition = createProviderComposition()
|
||||
.withTheme({ children: undefined })
|
||||
.withFluent({} as any)
|
||||
.withNotification()
|
||||
.withErrorBoundary()
|
||||
.withBrowserRouter()
|
||||
.withNavigationCancellation()
|
||||
.withAuth()
|
||||
.build(<AuthConsumer />);
|
||||
|
||||
render(composition);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("auth-available")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 2: App provider structure matches PROVIDER_ORDER.md
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("App providers nested as documented in PROVIDER_ORDER.md", async () => {
|
||||
function Inner(): React.JSX.Element {
|
||||
return <div data-testid="inner">Inner</div>;
|
||||
}
|
||||
|
||||
// Recreate the core provider nesting from App.tsx
|
||||
const composition = (
|
||||
<ThemeProvider>
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<NavigationCancellationProvider>
|
||||
<AuthProvider>
|
||||
<TimezoneProvider>
|
||||
<Inner />
|
||||
</TimezoneProvider>
|
||||
</AuthProvider>
|
||||
</NavigationCancellationProvider>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
render(composition);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("inner")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 3: Incorrect provider nesting (BrowserRouter missing) is detected
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("throws when AuthProvider is outside BrowserRouter", async () => {
|
||||
// AuthProvider uses useNavigate() which requires BrowserRouter context.
|
||||
// If placed outside BrowserRouter, it should throw.
|
||||
|
||||
function BadlyNested(): React.JSX.Element {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<div>Should fail</div>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
expect(() => render(<BadlyNested />)).toThrow();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 4: Provider order matters - FluentProvider needs theme from ThemeProvider
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("verify that ThemeProvider and FluentProvider order matters", async () => {
|
||||
// FluentProvider needs theme from ThemeProvider via useThemeMode().
|
||||
// This test verifies they work together when correctly nested.
|
||||
|
||||
function FluentContent(): React.JSX.Element {
|
||||
return <div data-testid="fluent-content">Fluent content</div>;
|
||||
}
|
||||
|
||||
const composition = createProviderComposition()
|
||||
.withTheme({ children: undefined })
|
||||
.withFluent({} as any)
|
||||
.withNotification()
|
||||
.withErrorBoundary()
|
||||
.withBrowserRouter()
|
||||
.withNavigationCancellation()
|
||||
.withAuth()
|
||||
.build(<FluentContent />);
|
||||
|
||||
render(composition);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("fluent-content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 5: buildWithTimezone adds TimezoneProvider after AuthProvider
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("buildWithTimezone() includes TimezoneProvider for protected routes", async () => {
|
||||
function TimezoneContent(): React.JSX.Element {
|
||||
return <div data-testid="timezone-content">Timezone content</div>;
|
||||
}
|
||||
|
||||
const composition = createProviderComposition()
|
||||
.withTheme({ children: undefined })
|
||||
.withFluent({} as any)
|
||||
.withNotification()
|
||||
.withErrorBoundary()
|
||||
.withBrowserRouter()
|
||||
.withNavigationCancellation()
|
||||
.withAuth()
|
||||
.withTimezone({ children: <TimezoneContent /> })
|
||||
.buildWithTimezone();
|
||||
|
||||
// Just verify it renders without throwing - ordering is enforced by builder
|
||||
render(composition);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("timezone-content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 6: All providers render without errors (nesting depth check)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
it("composition with all providers renders correctly", async () => {
|
||||
function AllProvidersContent(): React.JSX.Element {
|
||||
return <div data-testid="all-providers">All providers rendered</div>;
|
||||
}
|
||||
|
||||
const composition = createProviderComposition()
|
||||
.withTheme({ children: undefined })
|
||||
.withFluent({} as any)
|
||||
.withNotification()
|
||||
.withErrorBoundary()
|
||||
.withBrowserRouter()
|
||||
.withNavigationCancellation()
|
||||
.withAuth()
|
||||
.withTimezone({ children: <AllProvidersContent /> })
|
||||
.buildWithTimezone();
|
||||
|
||||
render(composition);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("all-providers")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user