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: () =>
Home
, initialEntries: ["/"], authClient, - guardComponent: () =>
Loading...
, + loadingComponent: () =>
Loading...
, }); 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: () =>
Home
, initialEntries: ["/"], authClient, - guardComponent: () =>
Loading...
, + loadingComponent: () =>
Loading...
, }); 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: () =>
Home
, initialEntries: ["/"], authClient, - guardComponent: () =>
Loading...
, + loadingComponent: () =>
Loading...
, }); - // Initially the guard should be shown + // Initially the loadingComponent should be shown expect(await screen.findByText("Loading...")).toBeDefined(); expect(screen.queryByText("Home")).toBeNull(); @@ -819,7 +827,7 @@ describe("RouterContainer with AuthProvider", () => { modules: [dashboardModule, settingsModule], initialEntries: ["/dashboard"], authClient, - guardComponent: () =>
Loading...
, + loadingComponent: () =>
Loading...
, }); expect(await screen.findByText("Dashboard")).toBeDefined(); @@ -908,10 +916,10 @@ describe("RouterContainer with AuthProvider", () => { modules: [publicModule, restrictedModule], initialEntries: ["/restricted"], authClient, - guardComponent: () =>
Auth Loading...
, + loadingComponent: () =>
Auth Loading...
, }); - // 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();