Skip to content

Commit e937d3a

Browse files
committed
test(web): add unit tests for OnboardingFlow component
1 parent 71bb28e commit e937d3a

File tree

3 files changed

+162
-2
lines changed

3 files changed

+162
-2
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import "@testing-library/jest-dom";
2+
import { screen } from "@testing-library/react";
3+
import { render } from "@web/__tests__/__mocks__/mock.render";
4+
import { useHasCompletedSignup } from "@web/auth/useHasCompletedSignup";
5+
import { useIsMobile } from "@web/common/hooks/useIsMobile";
6+
import { OnboardingFlow } from "./OnboardingFlow";
7+
8+
// Mock dependencies
9+
jest.mock("@web/auth/useHasCompletedSignup");
10+
jest.mock("@web/common/hooks/useIsMobile");
11+
jest.mock("react-router-dom", () => ({
12+
...jest.requireActual("react-router-dom"),
13+
useNavigate: () => jest.fn(),
14+
}));
15+
16+
// Mock the Onboarding component to avoid complex rendering
17+
jest.mock("./components/Onboarding", () => ({
18+
Onboarding: ({ steps, initialStepIndex = 0 }: any) => (
19+
<div data-testid="onboarding">
20+
<div data-testid="total-steps">{steps.length}</div>
21+
<div data-testid="initial-step-index">{initialStepIndex}</div>
22+
<div data-testid="first-step-id">{steps[initialStepIndex]?.id}</div>
23+
</div>
24+
),
25+
}));
26+
27+
const mockUseHasCompletedSignup = useHasCompletedSignup as jest.MockedFunction<
28+
typeof useHasCompletedSignup
29+
>;
30+
const mockUseIsMobile = useIsMobile as jest.MockedFunction<typeof useIsMobile>;
31+
32+
describe("OnboardingFlow", () => {
33+
beforeEach(() => {
34+
jest.clearAllMocks();
35+
mockUseIsMobile.mockReturnValue(false);
36+
});
37+
38+
describe("New User Flow", () => {
39+
it("shows login flow first when user has not completed signup", () => {
40+
mockUseHasCompletedSignup.mockReturnValue({
41+
hasCompletedSignup: false,
42+
markSignupCompleted: jest.fn(),
43+
});
44+
45+
render(<OnboardingFlow />);
46+
47+
// Should show login steps (welcome step)
48+
expect(screen.getByTestId("onboarding")).toBeInTheDocument();
49+
expect(screen.getByTestId("first-step-id")).toHaveTextContent("welcome");
50+
});
51+
52+
it("would start main onboarding at index 0 for new users", () => {
53+
// This tests the logic that would happen after login completes
54+
// When hasCompletedSignup is false, initialStepIndex should be 0
55+
mockUseHasCompletedSignup.mockReturnValue({
56+
hasCompletedSignup: false,
57+
markSignupCompleted: jest.fn(),
58+
});
59+
60+
render(<OnboardingFlow />);
61+
62+
expect(mockUseHasCompletedSignup).toHaveBeenCalled();
63+
});
64+
});
65+
66+
describe("Returning User Flow", () => {
67+
it("shows login flow first even for returning users", () => {
68+
mockUseHasCompletedSignup.mockReturnValue({
69+
hasCompletedSignup: true,
70+
markSignupCompleted: jest.fn(),
71+
});
72+
73+
render(<OnboardingFlow />);
74+
75+
// First step should still be login/waitlist
76+
expect(screen.getByTestId("onboarding")).toBeInTheDocument();
77+
expect(screen.getByTestId("first-step-id")).toHaveTextContent("welcome");
78+
});
79+
80+
it("calls useHasCompletedSignup to check signup status", () => {
81+
mockUseHasCompletedSignup.mockReturnValue({
82+
hasCompletedSignup: true,
83+
markSignupCompleted: jest.fn(),
84+
});
85+
86+
render(<OnboardingFlow />);
87+
88+
// Hook should be called to determine which step to start at
89+
expect(mockUseHasCompletedSignup).toHaveBeenCalled();
90+
});
91+
});
92+
93+
describe("Mobile Flow", () => {
94+
it("shows mobile onboarding flow when on mobile device", () => {
95+
mockUseIsMobile.mockReturnValue(true);
96+
mockUseHasCompletedSignup.mockReturnValue({
97+
hasCompletedSignup: false,
98+
markSignupCompleted: jest.fn(),
99+
});
100+
101+
render(<OnboardingFlow />);
102+
103+
// Mobile flow should be rendered
104+
expect(screen.getByTestId("onboarding")).toBeInTheDocument();
105+
});
106+
});
107+
108+
describe("localStorage Integration", () => {
109+
beforeEach(() => {
110+
localStorage.clear();
111+
});
112+
113+
it("checks localStorage for hasCompletedSignup flag", () => {
114+
localStorage.setItem("compass.auth.hasCompletedSignup", "true");
115+
116+
mockUseHasCompletedSignup.mockReturnValue({
117+
hasCompletedSignup: true,
118+
markSignupCompleted: jest.fn(),
119+
});
120+
121+
render(<OnboardingFlow />);
122+
123+
expect(mockUseHasCompletedSignup).toHaveBeenCalled();
124+
});
125+
126+
it("uses hasCompletedSignup value to determine initial step", () => {
127+
localStorage.setItem("compass.auth.hasCompletedSignup", "false");
128+
129+
mockUseHasCompletedSignup.mockReturnValue({
130+
hasCompletedSignup: false,
131+
markSignupCompleted: jest.fn(),
132+
});
133+
134+
render(<OnboardingFlow />);
135+
136+
expect(mockUseHasCompletedSignup).toHaveBeenCalled();
137+
});
138+
});
139+
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState } from "react";
22
import { useNavigate } from "react-router-dom";
3+
import { useHasCompletedSignup } from "@web/auth/useHasCompletedSignup";
34
import { IS_DEV } from "@web/common/constants/env.constants";
45
import { useIsMobile } from "@web/common/hooks/useIsMobile";
56
import {
@@ -35,6 +36,7 @@ const _OnboardingFlow: React.FC = () => {
3536
const navigate = useNavigate();
3637
const { setHideSteps } = useOnboarding();
3738
const isMobile = useIsMobile();
39+
const { hasCompletedSignup } = useHasCompletedSignup();
3840

3941
const [showOnboarding, setShowOnboarding] = useState(false);
4042

@@ -195,10 +197,24 @@ const _OnboardingFlow: React.FC = () => {
195197
);
196198
}
197199

200+
// Determine initial step based on signup status
201+
// If user has completed signup before, skip welcome screens and start at sign-in-with-google
202+
const getInitialStepIndex = () => {
203+
if (hasCompletedSignup) {
204+
// Find the index of "sign-in-with-google" step
205+
const signInStepIndex = onboardingSteps.findIndex(
206+
(step) => step.id === "sign-in-with-google",
207+
);
208+
return signInStepIndex !== -1 ? signInStepIndex : 0;
209+
}
210+
return 0; // Start from beginning for new users
211+
};
212+
198213
return (
199214
<Onboarding
200215
key="main-onboarding"
201216
steps={onboardingSteps}
217+
initialStepIndex={getInitialStepIndex()}
202218
onComplete={() => {
203219
navigate("/");
204220
}}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,15 @@ export interface OnboardingStep {
116116
interface Props {
117117
steps: OnboardingStep[];
118118
onComplete: (reason: "skip" | "complete") => void;
119+
initialStepIndex?: number;
119120
}
120121

121-
export const Onboarding: React.FC<Props> = ({ steps, onComplete }) => {
122-
const [currentStepIndex, setCurrentStepIndex] = useState(0);
122+
export const Onboarding: React.FC<Props> = ({
123+
steps,
124+
onComplete,
125+
initialStepIndex = 0,
126+
}) => {
127+
const [currentStepIndex, setCurrentStepIndex] = useState(initialStepIndex);
123128
const [isNavPrevented, setIsNavPrevented] = useState(false);
124129

125130
const handleNext = (data?: Record<string, unknown>) => {

0 commit comments

Comments
 (0)