Skip to content

Commit 0196fd9

Browse files
test: add unit tests for auth form validation (#121) (#138)
Co-authored-by: Ona <no-reply@ona.com>
1 parent f8a08fd commit 0196fd9

2 files changed

Lines changed: 434 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import "@testing-library/jest-dom/vitest";
3+
import { render, screen, waitFor } from "@testing-library/react";
4+
import userEvent from "@testing-library/user-event";
5+
6+
// Mock next/navigation
7+
const mockPush = vi.fn();
8+
vi.mock("next/navigation", () => ({
9+
useRouter: () => ({ push: mockPush }),
10+
}));
11+
12+
// Mock next/link as a simple anchor
13+
vi.mock("next/link", () => ({
14+
default: ({
15+
href,
16+
children,
17+
...props
18+
}: {
19+
href: string;
20+
children: React.ReactNode;
21+
}) => (
22+
<a href={href} {...props}>
23+
{children}
24+
</a>
25+
),
26+
}));
27+
28+
// Mock OAuthButtons to avoid tooltip provider dependency
29+
vi.mock("@/components/auth/oauth-buttons", () => ({
30+
OAuthButtons: () => <div data-testid="oauth-buttons" />,
31+
}));
32+
33+
// Supabase mock state
34+
const mockSignInWithPassword = vi.fn();
35+
const mockGetUser = vi.fn();
36+
const mockFrom = vi.fn();
37+
38+
vi.mock("@/lib/supabase/client", () => ({
39+
createClient: () => ({
40+
auth: {
41+
signInWithPassword: mockSignInWithPassword,
42+
getUser: mockGetUser,
43+
},
44+
from: mockFrom,
45+
}),
46+
}));
47+
48+
import SignInPage from "./page";
49+
50+
beforeEach(() => {
51+
vi.clearAllMocks();
52+
});
53+
54+
describe("SignInPage", () => {
55+
it("renders email and password fields", () => {
56+
render(<SignInPage />);
57+
58+
expect(screen.getByLabelText("Email")).toBeInTheDocument();
59+
expect(screen.getByLabelText("Password")).toBeInTheDocument();
60+
expect(
61+
screen.getByRole("button", { name: /sign in/i }),
62+
).toBeInTheDocument();
63+
});
64+
65+
it("email field has type=email and is required", () => {
66+
render(<SignInPage />);
67+
const input = screen.getByLabelText("Email");
68+
expect(input).toBeRequired();
69+
expect(input).toHaveAttribute("type", "email");
70+
});
71+
72+
it("password field has minLength=6 and is required", () => {
73+
render(<SignInPage />);
74+
const input = screen.getByLabelText("Password");
75+
expect(input).toBeRequired();
76+
expect(input).toHaveAttribute("type", "password");
77+
expect(input).toHaveAttribute("minLength", "6");
78+
});
79+
80+
it("shows error message when sign-in fails", async () => {
81+
mockSignInWithPassword.mockResolvedValue({
82+
error: { message: "Invalid login credentials" },
83+
});
84+
85+
const user = userEvent.setup();
86+
render(<SignInPage />);
87+
88+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
89+
await user.type(screen.getByLabelText("Password"), "wrongpass");
90+
91+
const form = screen.getByRole("button", { name: /sign in/i })
92+
.closest("form")!;
93+
form.requestSubmit();
94+
95+
await waitFor(() => {
96+
expect(
97+
screen.getByText("Invalid login credentials"),
98+
).toBeInTheDocument();
99+
});
100+
});
101+
102+
it("calls supabase.auth.signInWithPassword with correct parameters", async () => {
103+
mockSignInWithPassword.mockResolvedValue({ error: null });
104+
mockGetUser.mockResolvedValue({
105+
data: { user: { id: "user-1" } },
106+
});
107+
108+
const mockMaybeSingle = vi.fn().mockResolvedValue({
109+
data: { workspace_id: "ws-1", workspaces: { slug: "jane" } },
110+
});
111+
const mockLimit = vi.fn().mockReturnValue({ maybeSingle: mockMaybeSingle });
112+
const mockEq = vi.fn().mockReturnValue({ limit: mockLimit });
113+
const mockSelect = vi.fn().mockReturnValue({ eq: mockEq });
114+
mockFrom.mockReturnValue({ select: mockSelect });
115+
116+
const user = userEvent.setup();
117+
render(<SignInPage />);
118+
119+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
120+
await user.type(screen.getByLabelText("Password"), "password123");
121+
122+
const form = screen.getByRole("button", { name: /sign in/i })
123+
.closest("form")!;
124+
form.requestSubmit();
125+
126+
await waitFor(() => {
127+
expect(mockSignInWithPassword).toHaveBeenCalledWith({
128+
email: "jane@example.com",
129+
password: "password123",
130+
});
131+
});
132+
});
133+
134+
it("redirects to workspace after successful sign-in", async () => {
135+
mockSignInWithPassword.mockResolvedValue({ error: null });
136+
mockGetUser.mockResolvedValue({
137+
data: { user: { id: "user-1" } },
138+
});
139+
140+
const mockMaybeSingle = vi.fn().mockResolvedValue({
141+
data: { workspace_id: "ws-1", workspaces: { slug: "jane" } },
142+
});
143+
const mockLimit = vi.fn().mockReturnValue({ maybeSingle: mockMaybeSingle });
144+
const mockEq = vi.fn().mockReturnValue({ limit: mockLimit });
145+
const mockSelect = vi.fn().mockReturnValue({ eq: mockEq });
146+
mockFrom.mockReturnValue({ select: mockSelect });
147+
148+
const user = userEvent.setup();
149+
render(<SignInPage />);
150+
151+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
152+
await user.type(screen.getByLabelText("Password"), "password123");
153+
154+
const form = screen.getByRole("button", { name: /sign in/i })
155+
.closest("form")!;
156+
form.requestSubmit();
157+
158+
await waitFor(() => {
159+
expect(mockPush).toHaveBeenCalledWith("/jane");
160+
});
161+
});
162+
163+
it("redirects to root when no workspace found after sign-in", async () => {
164+
mockSignInWithPassword.mockResolvedValue({ error: null });
165+
mockGetUser.mockResolvedValue({
166+
data: { user: { id: "user-1" } },
167+
});
168+
169+
const mockMaybeSingle = vi.fn().mockResolvedValue({ data: null });
170+
const mockLimit = vi.fn().mockReturnValue({ maybeSingle: mockMaybeSingle });
171+
const mockEq = vi.fn().mockReturnValue({ limit: mockLimit });
172+
const mockSelect = vi.fn().mockReturnValue({ eq: mockEq });
173+
mockFrom.mockReturnValue({ select: mockSelect });
174+
175+
const user = userEvent.setup();
176+
render(<SignInPage />);
177+
178+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
179+
await user.type(screen.getByLabelText("Password"), "password123");
180+
181+
const form = screen.getByRole("button", { name: /sign in/i })
182+
.closest("form")!;
183+
form.requestSubmit();
184+
185+
await waitFor(() => {
186+
expect(mockPush).toHaveBeenCalledWith("/");
187+
});
188+
});
189+
190+
it("disables submit button while loading", async () => {
191+
// Never resolve to keep loading state
192+
mockSignInWithPassword.mockReturnValue(new Promise(() => {}));
193+
194+
const user = userEvent.setup();
195+
render(<SignInPage />);
196+
197+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
198+
await user.type(screen.getByLabelText("Password"), "password123");
199+
200+
const submitButton = screen.getByRole("button", { name: /sign in/i });
201+
const form = submitButton.closest("form")!;
202+
form.requestSubmit();
203+
204+
await waitFor(() => {
205+
expect(
206+
screen.getByRole("button", { name: /signing in/i }),
207+
).toBeDisabled();
208+
});
209+
});
210+
});

0 commit comments

Comments
 (0)