Skip to content

Commit d379061

Browse files
committed
refactor(auth): simplify ProtectedRoute tests and enhance loading state handling
- Removed the renderWithRouter utility in ProtectedRoute tests, directly using the render function for clarity. - Added a new test case to ensure that the ProtectedRoute does not redirect when hasCompletedSignup is null, addressing the loading state scenario. - Updated ProtectedRoute component to wait for hasCompletedSignup to be determined before performing any redirects, improving user experience during authentication checks.
1 parent a1200ad commit d379061

File tree

4 files changed

+45
-32
lines changed

4 files changed

+45
-32
lines changed

packages/web/src/auth/ProtectedRoute.test.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { ReactElement } from "react";
2-
import { BrowserRouter } from "react-router-dom";
31
import "@testing-library/jest-dom";
4-
import { render, waitFor } from "@testing-library/react";
2+
import { waitFor } from "@testing-library/react";
3+
import { render } from "@web/__tests__/__mocks__/mock.render";
54
import { AUTH_FAILURE_REASONS } from "@web/common/constants/auth.constants";
65
import { ROOT_ROUTES } from "@web/common/constants/routes";
76
import { STORAGE_KEYS } from "@web/common/constants/storage.constants";
@@ -28,22 +27,6 @@ jest.mock("react-router-dom", () => ({
2827
useNavigate: () => mockNavigate,
2928
}));
3029

31-
/**
32-
* Utility function to wrap a component in BrowserRouter with v7 future props
33-
*/
34-
const renderWithRouter = (component: ReactElement) => {
35-
return render(
36-
<BrowserRouter
37-
future={{
38-
v7_startTransition: true,
39-
v7_relativeSplatPath: true,
40-
}}
41-
>
42-
{component}
43-
</BrowserRouter>,
44-
);
45-
};
46-
4730
describe("ProtectedRoute", () => {
4831
beforeEach(() => {
4932
jest.clearAllMocks();
@@ -64,7 +47,7 @@ describe("ProtectedRoute", () => {
6447
markSignupCompleted: jest.fn(),
6548
});
6649

67-
renderWithRouter(
50+
render(
6851
<ProtectedRoute>
6952
<div>Protected Content</div>
7053
</ProtectedRoute>,
@@ -87,7 +70,7 @@ describe("ProtectedRoute", () => {
8770
markSignupCompleted: jest.fn(),
8871
});
8972

90-
renderWithRouter(
73+
render(
9174
<ProtectedRoute>
9275
<div>Protected Content</div>
9376
</ProtectedRoute>,
@@ -112,7 +95,7 @@ describe("ProtectedRoute", () => {
11295
markSignupCompleted: jest.fn(),
11396
});
11497

115-
renderWithRouter(
98+
render(
11699
<ProtectedRoute>
117100
<div>Protected Content</div>
118101
</ProtectedRoute>,
@@ -137,7 +120,7 @@ describe("ProtectedRoute", () => {
137120
markSignupCompleted: jest.fn(),
138121
});
139122

140-
const { getByText } = renderWithRouter(
123+
const { getByText } = render(
141124
<ProtectedRoute>
142125
<div>Protected Content</div>
143126
</ProtectedRoute>,
@@ -149,6 +132,31 @@ describe("ProtectedRoute", () => {
149132

150133
expect(mockNavigate).not.toHaveBeenCalled();
151134
});
135+
136+
it("does not redirect when hasCompletedSignup is null (loading state)", async () => {
137+
mockUseAuthCheck.mockReturnValue({
138+
isAuthenticated: false,
139+
isCheckingAuth: false,
140+
isGoogleTokenActive: false,
141+
isSessionActive: false,
142+
});
143+
mockUseHasCompletedSignup.mockReturnValue({
144+
hasCompletedSignup: null,
145+
markSignupCompleted: jest.fn(),
146+
});
147+
148+
render(
149+
<ProtectedRoute>
150+
<div>Protected Content</div>
151+
</ProtectedRoute>,
152+
);
153+
154+
// Wait a bit to ensure redirect doesn't happen
155+
await new Promise((resolve) => setTimeout(resolve, 100));
156+
157+
// Should not redirect while loading
158+
expect(mockNavigate).not.toHaveBeenCalled();
159+
});
152160
});
153161

154162
describe("localStorage Integration", () => {
@@ -166,7 +174,7 @@ describe("ProtectedRoute", () => {
166174
markSignupCompleted: jest.fn(),
167175
});
168176

169-
renderWithRouter(
177+
render(
170178
<ProtectedRoute>
171179
<div>Protected Content</div>
172180
</ProtectedRoute>,
@@ -194,7 +202,7 @@ describe("ProtectedRoute", () => {
194202
markSignupCompleted: jest.fn(),
195203
});
196204

197-
renderWithRouter(
205+
render(
198206
<ProtectedRoute>
199207
<div>Protected Content</div>
200208
</ProtectedRoute>,

packages/web/src/auth/ProtectedRoute.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const ProtectedRoute = ({ children }: { children: ReactNode }) => {
1616
useEffect(() => {
1717
const handleAuthCheck = () => {
1818
if (isAuthenticated === false) {
19+
// Wait for hasCompletedSignup to be determined (not null) before redirecting
20+
if (hasCompletedSignup === null) {
21+
return;
22+
}
23+
1924
// Check if user has completed signup to determine redirect destination
2025
if (hasCompletedSignup === true) {
2126
// User has completed signup before, redirect to /login

packages/web/src/views/Onboarding/OnboardingFlow.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { OnboardingFlow } from "./OnboardingFlow";
1010

1111
// Mock navigate function
1212
const mockNavigate = jest.fn();
13-
const mockLocation = { pathname: ROOT_ROUTES.LOGIN };
13+
let mockLocationPathname = ROOT_ROUTES.LOGIN;
1414

1515
// Mock dependencies
1616
jest.mock("@web/auth/useHasCompletedSignup");
@@ -19,7 +19,7 @@ jest.mock("@web/common/hooks/useIsMobile");
1919
jest.mock("react-router-dom", () => ({
2020
...jest.requireActual("react-router-dom"),
2121
useNavigate: () => mockNavigate,
22-
useLocation: () => mockLocation,
22+
useLocation: () => ({ pathname: mockLocationPathname }),
2323
}));
2424

2525
// Mock the Onboarding component to avoid complex rendering
@@ -67,7 +67,7 @@ describe("OnboardingFlow", () => {
6767
isSessionActive: false,
6868
});
6969
// Reset location to /login by default
70-
mockLocation.pathname = ROOT_ROUTES.LOGIN;
70+
mockLocationPathname = ROOT_ROUTES.LOGIN;
7171
});
7272

7373
describe("New User Flow", () => {
@@ -240,7 +240,7 @@ describe("OnboardingFlow", () => {
240240
describe("Route-based Behavior", () => {
241241
describe("/onboarding route", () => {
242242
beforeEach(() => {
243-
mockLocation.pathname = ROOT_ROUTES.ONBOARDING;
243+
mockLocationPathname = ROOT_ROUTES.ONBOARDING;
244244
});
245245

246246
it("ignores localStorage and always starts from beginning", () => {
@@ -323,7 +323,7 @@ describe("OnboardingFlow", () => {
323323

324324
describe("/login route", () => {
325325
beforeEach(() => {
326-
mockLocation.pathname = ROOT_ROUTES.LOGIN;
326+
mockLocationPathname = ROOT_ROUTES.LOGIN;
327327
});
328328

329329
it("preserves localStorage check behavior", () => {

packages/web/src/views/Onboarding/OnboardingFlow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import { FC, useEffect, useState } from "react";
22
import { useLocation, useNavigate } from "react-router-dom";
33
import { useAuthCheck } from "@web/auth/useAuthCheck";
44
import { useHasCompletedSignup } from "@web/auth/useHasCompletedSignup";
@@ -37,7 +37,7 @@ import { ReminderIntroOne } from "./steps/reminder/ReminderIntroOne";
3737
import { ReminderIntroTwo } from "./steps/reminder/ReminderIntroTwo";
3838
import { TasksToday } from "./steps/tasks/TasksToday/TasksToday";
3939

40-
const _OnboardingFlow: React.FC = () => {
40+
const _OnboardingFlow: FC = () => {
4141
const navigate = useNavigate();
4242
const location = useLocation();
4343
const { setHideSteps } = useOnboarding();

0 commit comments

Comments
 (0)