diff --git a/.changeset/auth-loading-component.md b/.changeset/auth-loading-component.md
new file mode 100644
index 00000000..43e02078
--- /dev/null
+++ b/.changeset/auth-loading-component.md
@@ -0,0 +1,35 @@
+---
+"@tailor-platform/app-shell": minor
+---
+
+Split `AuthProvider`'s guard slot into two props so the loading state and the unauthenticated state can render different UI, and protect against flashes by default when `autoLogin` is enabled.
+
+- **New:** `loadingComponent` — rendered while the initial auth check is in progress (`isReady === false`). Use this for a loading spinner or skeleton.
+- **Changed:** `guardComponent` — now only rendered after the auth check has completed and the user is not authenticated (`isReady && !isAuthenticated`). Use this for a sign-in screen.
+- **New default:** when `autoLogin` is enabled and a slot is omitted, the protected tree is hidden during that state (instead of briefly rendering children before the redirect). When `autoLogin` is off, children continue to render in the omitted slot's state — preserving public-app and `useAuthSuspense` patterns.
+
+Previously `guardComponent` was rendered for the union of "not ready" and "not authenticated," which caused sign-in screens wired into that slot to flash on every reload before the session was known.
+
+```tsx
+// Before — guardComponent doubled as loading + unauthenticated
+}>
+ …
+
+
+// After — use the slot that matches your intent
+}
+ guardComponent={() => }
+>
+ …
+
+
+// Or, with autoLogin, omit both slots and rely on the default
+// flash protection (the tree stays hidden until the user is signed in):
+
+ …
+
+```
+
+**Migration:** if you were passing a loading UI to `guardComponent`, rename the prop to `loadingComponent`. If you were passing a sign-in screen, keep it on `guardComponent` — it will no longer flash before the auth check resolves. If you relied on children rendering before auth resolved with `autoLogin` on, pass an explicit `loadingComponent` (e.g., `loadingComponent={() => }` is unusual; more typically you want a spinner there).
diff --git a/docs/concepts/authentication.md b/docs/concepts/authentication.md
index b3757c5e..96442088 100644
--- a/docs/concepts/authentication.md
+++ b/docs/concepts/authentication.md
@@ -25,7 +25,7 @@ const authClient = createAuthClient({
});
const App = () => (
- }>
+ }>
@@ -46,9 +46,27 @@ Find the above values in Tailor Console:
The above code will:
- Automatically redirect unauthenticated users to the login page (if `autoLogin` is true)
-- Show the `guardComponent` while loading or when unauthenticated
+- Show the `loadingComponent` while the initial auth check is in progress
- Handle token management and session persistence automatically
+### Default flash protection
+
+When `autoLogin` is enabled, `AuthProvider` hides the protected tree by default whenever the auth state is unresolved — that is, during the initial check (`!isReady`) and during the brief window between "ready, unauthenticated" and the login redirect. Providing `loadingComponent` and/or `guardComponent` simply replaces that hidden region with your UI; you do not need to set them to avoid a flash.
+
+When `autoLogin` is off, children render in those windows by default. This preserves the "public app" pattern (where some content is meant to be visible regardless of auth) and the [`useAuthSuspense`](#suspense-compatible-hook) pattern (where a `` boundary inside your tree owns the loading UI).
+
+If you want to render a sign-in screen instead of redirecting (i.e., without `autoLogin`), pass it via `guardComponent` — it is only rendered after the initial auth check has completed and the user is not authenticated, so it does not flash on reload:
+
+```tsx
+}
+ guardComponent={() => }
+>
+ {/* ... */}
+
+```
+
## Authentication Hook
Use the `useAuth` hook to access authentication state and methods:
@@ -171,11 +189,12 @@ function App() {
## `AuthProvider` Props
-| Prop | Type | Required | Description |
-| ---------------- | ----------------------- | -------- | ----------------------------------------------------- |
-| `client` | `EnhancedAuthClient` | Yes | Auth client created with `createAuthClient` |
-| `autoLogin` | `boolean` | No | Automatically redirect unauthenticated users to login |
-| `guardComponent` | `() => React.ReactNode` | No | Rendered while loading or when not authenticated |
+| Prop | Type | Required | Description |
+| ------------------ | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `client` | `EnhancedAuthClient` | Yes | Auth client created with `createAuthClient` |
+| `autoLogin` | `boolean` | No | Automatically redirect unauthenticated users to login |
+| `loadingComponent` | `() => React.ReactNode` | No | Rendered while the initial auth check is in progress (`isReady === false`). When omitted: children are hidden if `autoLogin` is on, otherwise children render. |
+| `guardComponent` | `() => React.ReactNode` | No | Rendered when the auth check has completed and the user is not authenticated. When omitted: children are hidden if `autoLogin` is on, otherwise children render. |
## Integration with AppShell
diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts
index ca59c13a..5262806d 100644
--- a/e2e/tests/auth.spec.ts
+++ b/e2e/tests/auth.spec.ts
@@ -83,6 +83,68 @@ test.describe("AuthProvider", () => {
});
});
+ test("OAuth callback does not flash the auth guard or trip React rules of hooks", async ({
+ page,
+ }) => {
+ // Regression for two interacting bugs:
+ // 1. AuthGuard used to render guardComponent during a pending OAuth
+ // callback when the auth client surfaced a stale isReady &&
+ // !isAuthenticated state — flashing the sign-in screen the user
+ // just returned from.
+ // 2. AuthGuard invoked the slots as plain function calls, which
+ // inlined slot hooks into AuthGuard's own hook scope. Because
+ // the slots render conditionally, any slot that called a hook
+ // (this app's calls useAuth) would change AuthGuard's
+ // hook order across renders → "React has detected a change in
+ // the order of Hooks called by AuthGuard".
+ const consoleErrors: string[] = [];
+ page.on("console", (msg) => {
+ if (msg.type() === "error") consoleErrors.push(msg.text());
+ });
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
+
+ await page.goto("/");
+ await page.getByTestId("login-button").click();
+
+ const email = process.env.E2E_USER_EMAIL;
+ const password = process.env.E2E_USER_PASSWORD;
+
+ if (!email || !password) {
+ test.skip(true, "E2E_USER_EMAIL and E2E_USER_PASSWORD must be set");
+ return;
+ }
+
+ await page.waitForURL(/idp\.erp\.dev\/.*\/signin/);
+ await page.getByLabel(/email/i).fill(email);
+ await page.locator("#password").fill(password);
+ await page.getByRole("button", { name: /sign in|log in|submit/i }).click();
+
+ await page.waitForURL("http://localhost:3100/**");
+ await expect(page.getByTestId("authenticated-content")).toBeVisible({
+ timeout: 10000,
+ });
+
+ // Callback blackout: after we land back on the app, the sign-in
+ // screen must not be in the DOM at all — the AuthProvider should
+ // have rendered nothing during the callback exchange and then
+ // transitioned directly to authenticated content.
+ await expect(page.getByTestId("auth-guard")).toHaveCount(0);
+
+ // Hooks isolation: the slot (AuthGuard, which uses useAuth) was
+ // rendered at least once during the unauthenticated state. If the
+ // slot were invoked as a plain function call instead of as its own
+ // fiber, the transition out of !isAuthenticated would log a hook-
+ // order warning here.
+ const reactErrors = consoleErrors.filter(
+ (e) =>
+ e.includes("order of Hooks") ||
+ e.includes("Invalid hook call") ||
+ e.includes("Rendered fewer hooks") ||
+ e.includes("Rendered more hooks"),
+ );
+ expect(reactErrors, `Unexpected React hook errors:\n${reactErrors.join("\n")}`).toEqual([]);
+ });
+
test("maintains session on page reload after login", async ({ page }) => {
await page.goto("/");
await page.getByTestId("login-button").click();
@@ -114,4 +176,72 @@ test.describe("AuthProvider", () => {
});
await expect(page.getByTestId("auth-status")).toHaveText("Logged in");
});
+
+ test("reloading an authenticated session does not flash the auth guard", async ({ page }) => {
+ // Regression for the AuthGuard flash on reload: while the initial
+ // auth check is in flight (`!isReady`), the guardComponent slot
+ // must not render — even for a single frame — when the user has
+ // a valid persisted session. We catch a flash by installing a
+ // MutationObserver via addInitScript (which runs before any page
+ // render), so we observe the very first DOM mutations after
+ // reload.
+ const email = process.env.E2E_USER_EMAIL;
+ const password = process.env.E2E_USER_PASSWORD;
+
+ if (!email || !password) {
+ test.skip(true, "E2E_USER_EMAIL and E2E_USER_PASSWORD must be set");
+ return;
+ }
+
+ // The init script re-runs on every navigation and reload, so the
+ // sentinel starts `false` on the reloaded page regardless of what
+ // happened on previous navigations.
+ await page.addInitScript(() => {
+ (window as unknown as { __authGuardEverSeen: boolean }).__authGuardEverSeen = false;
+ const check = () => {
+ if (document.querySelector("[data-testid='auth-guard']")) {
+ (window as unknown as { __authGuardEverSeen: boolean }).__authGuardEverSeen = true;
+ }
+ };
+ const startObserving = () => {
+ if (document.body) {
+ new MutationObserver(check).observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ check();
+ } else {
+ requestAnimationFrame(startObserving);
+ }
+ };
+ startObserving();
+ });
+
+ // Log in first to establish a persisted session.
+ await page.goto("/");
+ await page.getByTestId("login-button").click();
+ await page.waitForURL(/idp\.erp\.dev\/.*\/signin/);
+ await page.getByLabel(/email/i).fill(email);
+ await page.locator("#password").fill(password);
+ await page.getByRole("button", { name: /sign in|log in|submit/i }).click();
+ await page.waitForURL("http://localhost:3100/**");
+ await expect(page.getByTestId("authenticated-content")).toBeVisible({
+ timeout: 10000,
+ });
+
+ // Reload — the init script reinstalls a fresh observer on the new
+ // document before any React render, so any moment the auth guard
+ // appears (even for one frame) sets the sentinel.
+ await page.reload();
+ await expect(page.getByTestId("authenticated-content")).toBeVisible({
+ timeout: 10000,
+ });
+
+ const guardSeenOnReload = await page.evaluate(
+ () => (window as unknown as { __authGuardEverSeen: boolean }).__authGuardEverSeen,
+ );
+ expect(guardSeenOnReload, "auth-guard appeared during reload of authenticated session").toBe(
+ false,
+ );
+ });
});
diff --git a/packages/core/src/contexts/auth-context.test.tsx b/packages/core/src/contexts/auth-context.test.tsx
index f77e69b5..eff1ae36 100644
--- a/packages/core/src/contexts/auth-context.test.tsx
+++ b/packages/core/src/contexts/auth-context.test.tsx
@@ -224,7 +224,7 @@ describe("AuthProvider", () => {
expect(screen.getByText("Test Content")).toBeDefined();
});
- it("should show guard component when not ready", () => {
+ it("should show loadingComponent when not ready", () => {
const state = {
isAuthenticated: false,
error: null,
@@ -233,16 +233,35 @@ describe("AuthProvider", () => {
const mockClient = createMockAuthClient(state);
render(
-
+
Protected Content
,
);
- // Guard is now rendered directly by AuthProvider
expect(screen.getByText("Loading...")).toBeDefined();
expect(screen.queryByText("Protected Content")).toBeNull();
});
+ it("should not show guardComponent while not ready", () => {
+ // Regression: guardComponent is for unauthenticated users only. Showing
+ // it during the !isReady state would flash a sign-in screen on every
+ // reload before the session is known.
+ const state = {
+ isAuthenticated: false,
+ error: null,
+ isReady: false,
+ };
+ const mockClient = createMockAuthClient(state);
+
+ render(
+
+
Protected Content
+ ,
+ );
+
+ expect(screen.queryByText("Please log in")).toBeNull();
+ });
+
it("should show guard component when not authenticated", async () => {
const state = {
isAuthenticated: false,
@@ -262,6 +281,67 @@ describe("AuthProvider", () => {
expect(screen.queryByText("Protected Content")).toBeNull();
});
+ it("should hide children during !isReady when autoLogin is enabled and no loadingComponent is set", () => {
+ // Default flash protection: with autoLogin a redirect is imminent
+ // once the check resolves, so AuthProvider hides children during the
+ // !isReady window even when no loadingComponent is provided.
+ const state = {
+ isAuthenticated: false,
+ error: null,
+ isReady: false,
+ };
+ const mockClient = createMockAuthClient(state);
+
+ render(
+
+
Protected Content
+ ,
+ );
+
+ expect(screen.queryByText("Protected Content")).toBeNull();
+ });
+
+ it("should hide children when autoLogin is enabled, ready, and unauthenticated with no guardComponent", () => {
+ // Default flash protection: the autoLogin redirect is about to fire,
+ // so the protected tree stays hidden until it does.
+ const state = {
+ isAuthenticated: false,
+ error: null,
+ isReady: true,
+ };
+ const mockClient = createMockAuthClient(state, {
+ login: vi.fn().mockResolvedValue(undefined),
+ });
+
+ render(
+
+
Protected Content
+ ,
+ );
+
+ expect(screen.queryByText("Protected Content")).toBeNull();
+ });
+
+ it("should render children during !isReady when autoLogin is off and no loadingComponent is set", () => {
+ // Preserves the useAuthSuspense pattern: when the consumer is not
+ // using autoLogin, a boundary inside the children may
+ // own the loading UI, so AuthProvider must not suppress the tree.
+ const state = {
+ isAuthenticated: false,
+ error: null,
+ isReady: false,
+ };
+ const mockClient = createMockAuthClient(state);
+
+ render(
+
+
Public Content
+ ,
+ );
+
+ expect(screen.getByText("Public Content")).toBeDefined();
+ });
+
it("should show children when authenticated", async () => {
const state = {
isAuthenticated: true,
@@ -287,6 +367,66 @@ describe("AuthProvider", () => {
});
expect(screen.queryByText("Please log in")).toBeNull();
});
+
+ it("should isolate slot hooks from AuthGuard's hook scope", async () => {
+ // Regression: previously the slots were invoked as plain function
+ // calls (`guardComponent()`), inlining any hooks they used into
+ // AuthGuard's own hook list. Because the calls were conditional on
+ // auth state, the hook order changed across renders the moment a
+ // consumer passed a slot that used a hook (e.g. a sign-in screen
+ // calling `useAuth`). The fix renders slots via `createElement` so
+ // each gets its own fiber with an isolated hook scope.
+ let authEventListener: ((event: { type: string; data?: unknown }) => void) | undefined;
+ const mockAddEventListener = vi.fn(
+ (listener: (event: { type: string; data?: unknown }) => void) => {
+ authEventListener = listener;
+ return () => {};
+ },
+ );
+
+ let currentState = {
+ isAuthenticated: false,
+ error: null as string | null,
+ isReady: true,
+ };
+ const mockClient = createMockAuthClient(undefined, {
+ addEventListener: mockAddEventListener,
+ getState: vi.fn(() => currentState),
+ });
+
+ const SignInUsingHook = () => {
+ const { login } = useAuth();
+ return ;
+ };
+
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ render(
+
+
Protected
+ ,
+ );
+
+ expect(screen.getByText("Sign In")).toBeDefined();
+
+ // Transition to authenticated — guardComponent stops rendering.
+ // Without isolated hook scope, this re-render is where React would
+ // warn that AuthGuard's hook order has changed.
+ currentState = { isAuthenticated: true, error: null, isReady: true };
+ act(() => {
+ authEventListener?.({ type: "auth_state_changed", data: {} });
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Protected")).toBeDefined();
+ });
+
+ const hookOrderErrors = errorSpy.mock.calls.filter(
+ ([msg]) => typeof msg === "string" && msg.includes("order of Hooks"),
+ );
+ expect(hookOrderErrors).toEqual([]);
+ errorSpy.mockRestore();
+ });
});
describe("authentication flow", () => {
@@ -354,9 +494,14 @@ describe("AuthProvider", () => {
expect(mockHandleCallback).toHaveBeenCalled();
});
- it("should render guarded children while callback is pending", async () => {
+ it("should not render guardComponent during a pending callback", () => {
+ // Regression: with guardComponent set but no loadingComponent, a
+ // pending OAuth callback that lands in a stale `isReady &&
+ // !isAuthenticated` state would otherwise let AuthGuard render the
+ // sign-in screen — flashing the very UI the user just returned
+ // from. The callback blackout must suppress guardComponent too.
const state = {
- isAuthenticated: true,
+ isAuthenticated: false,
error: null,
isReady: true,
};
@@ -370,10 +515,58 @@ describe("AuthProvider", () => {
,
);
+ expect(screen.queryByText("Please log in")).toBeNull();
+ expect(screen.queryByText("Protected Content")).toBeNull();
+ });
+
+ it("should render nothing during a pending callback when only guardComponent is set and auth is not ready", () => {
+ // Companion to the regression above: the !isReady case is the
+ // other half of the callback blackout. guardComponent must not
+ // render here either (and there is no children fallback to flash).
+ const state = {
+ isAuthenticated: false,
+ error: null,
+ isReady: false,
+ };
+ const mockClient = createMockAuthClient(state, {
+ getCallbackStatusSnapshot: vi.fn(() => "pending" as const),
+ });
+
+ render(
+
+
Protected Content
+ ,
+ );
+
+ expect(screen.queryByText("Please log in")).toBeNull();
+ expect(screen.queryByText("Protected Content")).toBeNull();
+ });
+
+ it("should render children while callback is pending when loadingComponent is set", async () => {
+ // When the consumer provides a loadingComponent, AuthProvider trusts
+ // that slot to handle transitions and does not suppress children
+ // during the callback exchange. With auth already ready+authenticated
+ // (e.g., a stale tab revisiting a callback URL) the protected tree
+ // should remain visible.
+ const state = {
+ isAuthenticated: true,
+ error: null,
+ isReady: true,
+ };
+ const mockClient = createMockAuthClient(state, {
+ getCallbackStatusSnapshot: vi.fn(() => "pending" as const),
+ });
+
+ render(
+
+
Protected Content
+ ,
+ );
+
await waitFor(() => {
expect(screen.getByText("Protected Content")).toBeDefined();
});
- expect(screen.queryByText("Please log in")).toBeNull();
+ expect(screen.queryByText("Loading...")).toBeNull();
});
it("should be authenticated when logged in", async () => {
diff --git a/packages/core/src/contexts/auth-context.tsx b/packages/core/src/contexts/auth-context.tsx
index 2cb86b7d..86a08a25 100644
--- a/packages/core/src/contexts/auth-context.tsx
+++ b/packages/core/src/contexts/auth-context.tsx
@@ -1,5 +1,6 @@
import {
createContext,
+ createElement,
useContext,
useSyncExternalStore,
useCallback,
@@ -270,21 +271,48 @@ const isCurrentOAuthCallbackUrl = () => {
};
/**
- * Guard component that shows a fallback UI while auth is not ready or
- * not authenticated. Defined here so that the router layer does not
- * need to depend on useAuth.
+ * Renders the appropriate fallback for the current auth state:
+ * - `loadingComponent` while the initial auth check is in progress
+ * - `guardComponent` once the check has resolved but the user is not
+ * authenticated
+ *
+ * When a slot is missing, the fallback depends on `hideUnresolved`:
+ * - `hideUnresolved=true` (set by AuthProvider when `autoLogin` is on):
+ * render `null` so protected UI does not flash before the session is
+ * known or before the auto-login redirect happens.
+ * - `hideUnresolved=false`: render children. This preserves the
+ * `useAuthSuspense` pattern (where a `` boundary inside the
+ * children tree owns the loading UI) and the "public app" pattern
+ * (children render regardless of auth state).
+ *
+ * Defined here so the router layer does not need to depend on useAuth.
*/
const AuthGuard = ({
+ loadingComponent,
guardComponent,
+ hideUnresolved,
children,
}: {
- guardComponent: () => React.ReactNode;
+ loadingComponent?: () => React.ReactNode;
+ guardComponent?: () => React.ReactNode;
+ hideUnresolved: boolean;
children: React.ReactNode;
}) => {
const { isReady, isAuthenticated } = useAuth();
- if (!isReady || !isAuthenticated) {
- return guardComponent();
+ // Render each slot via createElement so it becomes its own fiber with
+ // an isolated hook scope. Calling `loadingComponent()` / `guardComponent()`
+ // directly would inline any hooks they use into AuthGuard's render — and
+ // because the calls are conditional, that violates React's rules of
+ // hooks the moment a consumer passes a slot that uses one (e.g. a
+ // sign-in screen calling `useAuth`).
+ if (!isReady) {
+ if (loadingComponent) return createElement(loadingComponent);
+ return hideUnresolved ? null : children;
+ }
+ if (!isAuthenticated) {
+ if (guardComponent) return createElement(guardComponent);
+ return hideUnresolved ? null : children;
}
return children;
};
@@ -302,13 +330,41 @@ type AuthProviderProps = {
autoLogin?: boolean;
/**
- * Guard UI component to show when loading or unauthenticated.
+ * Loading UI component shown while the initial authentication check is
+ * in progress (i.e., `authState.isReady === false`).
+ *
+ * When provided, AuthProvider renders this component in place of children
+ * until the auth client has resolved the initial session. After the check
+ * completes, either children or `guardComponent` is rendered depending on
+ * whether the user is authenticated.
+ *
+ * **Default when omitted:**
+ * - If `autoLogin` is `true`, children are hidden during the initial
+ * check so protected UI does not flash on reload.
+ * - Otherwise, children render — preserving patterns like
+ * {@link useAuthSuspense} where a `` boundary inside the
+ * children owns the loading UI, and public-app cases where content
+ * should show regardless of auth state.
+ */
+ loadingComponent?: () => React.ReactNode;
+
+ /**
+ * Guard UI component shown to unauthenticated users.
+ *
+ * When provided, AuthProvider renders this component once the initial
+ * auth check has completed (`isReady === true`) and the user is not
+ * authenticated. Typical use case: a sign-in screen.
*
- * When provided, AuthProvider renders this component directly while auth
- * is not ready or the user is not authenticated. Children are hidden until
- * auth resolves to an authenticated state.
+ * Note: this is NOT shown while the initial auth check is still in
+ * progress — use `loadingComponent` for that state. Otherwise, a
+ * sign-in screen would flash on every reload before the session is
+ * known.
*
- * If not provided, children are rendered regardless of auth state.
+ * **Default when omitted:**
+ * - If `autoLogin` is `true`, children are hidden while unauthenticated
+ * so protected UI does not flash before the login redirect happens.
+ * - Otherwise, children render — the "public app" case where the
+ * consumer wants to show content regardless of auth state.
*/
guardComponent?: () => React.ReactNode;
};
@@ -487,10 +543,14 @@ export const AuthProvider = (props: React.PropsWithChildren)
});
}, [client, ensureAuthInitialized]);
- // While handling an OAuth callback, keep unguarded children hidden until
- // the callback settles. Guarded trees already wait on auth state instead.
- const resolvedChildren =
- callbackStatus === "pending" && props.guardComponent == null ? null : props.children;
+ // While handling an OAuth callback, render nothing until it settles —
+ // bypassing AuthGuard entirely so neither children nor `guardComponent`
+ // can flash during the exchange. (A stale `isReady && !isAuthenticated`
+ // state would otherwise let AuthGuard render the sign-in screen here,
+ // which is exactly the flash we want to prevent.) Providing
+ // `loadingComponent` opts the consumer out of this blackout — they've
+ // taken responsibility for what to show during transitions.
+ const isCallbackBlackout = callbackStatus === "pending" && props.loadingComponent == null;
const authContextValue = useMemo(
() => ({
@@ -505,10 +565,14 @@ export const AuthProvider = (props: React.PropsWithChildren)
return (
- {props.guardComponent ? (
- {resolvedChildren}
- ) : (
- resolvedChildren
+ {isCallbackBlackout ? null : (
+
+ {props.children}
+
)}
);
diff --git a/packages/core/src/routing/router.test.tsx b/packages/core/src/routing/router.test.tsx
index 5373c55c..baa098e9 100644
--- a/packages/core/src/routing/router.test.tsx
+++ b/packages/core/src/routing/router.test.tsx
@@ -30,6 +30,7 @@ const renderWithConfig = ({
contextData = {},
authClient,
autoLogin,
+ loadingComponent,
guardComponent,
}: {
modules?: Array;
@@ -40,6 +41,7 @@ const renderWithConfig = ({
contextData?: ContextData;
authClient?: EnhancedAuthClient;
autoLogin?: boolean;
+ loadingComponent?: () => React.ReactNode;
guardComponent?: () => React.ReactNode;
}) => {
// Convert rootComponent into a root module (path="") so the test mirrors
@@ -78,7 +80,12 @@ const renderWithConfig = ({
return render(
authClient ? (
-
+
{tree}
) : (
@@ -537,11 +544,11 @@ describe("RouterContainer with AuthProvider", () => {
rootComponent: () =>
,
});
expect(await screen.findByText("Loading...")).toBeDefined();
- // With guardComponent now applied in AuthProvider (not the router),
+ // With loadingComponent now applied in AuthProvider (not the router),
// RouterContainer is not mounted until auth is ready.
expect(createMemoryRouterSpy).toHaveBeenCalledTimes(0);
@@ -658,7 +665,7 @@ describe("RouterContainer with AuthProvider", () => {
expect(mockLogin).not.toHaveBeenCalled();
});
- it("shows guard component when not ready", async () => {
+ it("shows loadingComponent when not ready", async () => {
const authClient = createMockAuthClient({
isAuthenticated: false,
error: null,
@@ -670,7 +677,7 @@ describe("RouterContainer with AuthProvider", () => {
rootComponent: () =>
,
});
expect(await screen.findByText("Loading...")).toBeDefined();
@@ -719,10 +726,11 @@ describe("RouterContainer with AuthProvider", () => {
expect(screen.queryByText("Please log in")).toBeNull();
});
- it("transitions from guard to children when auth state changes", async () => {
+ it("transitions from loadingComponent to children when auth state changes", async () => {
// Mutable snapshot; initially not ready, not authenticated.
- // Mount-time initialization runs outside the router loader, so the guard
- // remains visible until the auth client publishes a ready state.
+ // Mount-time initialization runs outside the router loader, so the
+ // loadingComponent remains visible until the auth client publishes a
+ // ready state.
let snapshot = {
isAuthenticated: false,
error: null as string | null,
@@ -758,10 +766,10 @@ describe("RouterContainer with AuthProvider", () => {
rootComponent: () =>
,
});
- // Auth guard should be shown first (auth not ready)
+ // loadingComponent should be shown first (auth not ready)
expect(await screen.findByText("Auth Loading...")).toBeDefined();
expect(screen.queryByText("Public Page")).toBeNull();
expect(screen.queryByText("Restricted Page")).toBeNull();