Skip to content

Commit 6037bdc

Browse files
committed
wip: React tests
1 parent 495671a commit 6037bdc

19 files changed

+2154
-4
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { vi } from "vitest";
2+
3+
// Define TranslationStrings type to match core package
4+
export type TranslationStrings = Record<string, string>;
5+
6+
// Implement FirebaseUIError with the same interface as the real implementation
7+
export class FirebaseUIError extends Error {
8+
code: string;
9+
constructor(
10+
error: any,
11+
_translations?: Partial<Record<string, Partial<TranslationStrings>>>,
12+
_language?: string
13+
) {
14+
// Extract error code from the error object
15+
const errorCode =
16+
typeof error === "string" ? error : error?.code || "unknown";
17+
18+
// For simplicity in tests, we'll use a direct message if provided as a string
19+
// or extract from translations if provided
20+
let errorMessage = `Error: ${errorCode}`;
21+
22+
if (
23+
typeof error === "string" &&
24+
arguments.length > 1 &&
25+
typeof arguments[1] === "string"
26+
) {
27+
// Handle case where first arg is code and second is message (for test convenience)
28+
errorMessage = arguments[1];
29+
}
30+
31+
super(errorMessage);
32+
this.name = "FirebaseUIError";
33+
this.code = errorCode;
34+
}
35+
}
36+
37+
// Authentication functions
38+
export const fuiSignInWithEmailAndPassword = vi.fn();
39+
export const fuiSignInWithEmailLink = vi.fn();
40+
export const fuiSignInWithPhone = vi.fn();
41+
export const fuiSignInWithOAuth = vi.fn();
42+
export const fuiResetPassword = vi.fn();
43+
export const fuiCreateUserWithEmailAndPassword = vi.fn();
44+
45+
// Country data for phone authentication
46+
export const countryData = [
47+
{ code: "US", name: "United States", dialCode: "+1", emoji: "🇺🇸" },
48+
{ code: "GB", name: "United Kingdom", dialCode: "+44", emoji: "🇬🇧" },
49+
{ code: "DE", name: "Germany", dialCode: "+49", emoji: "🇩🇪" },
50+
{ code: "FR", name: "France", dialCode: "+33", emoji: "🇫🇷" },
51+
{ code: "JP", name: "Japan", dialCode: "+81", emoji: "🇯🇵" },
52+
];
53+
54+
// Translation helpers
55+
export const getTranslation = vi.fn((section, key) => `${section}.${key}`);
56+
export const populateTranslation = vi.fn((text, data) => {
57+
if (!data) return text;
58+
let result = text;
59+
Object.entries(data).forEach(([key, value]) => {
60+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
61+
});
62+
return result;
63+
});

packages/firebaseui-react/tests/tsconfig.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111
"vitest/globals",
1212
"node",
1313
"@testing-library/jest-dom"
14-
]
14+
],
15+
"baseUrl": "..",
16+
"paths": {
17+
"@firebase-ui/core": [
18+
"../firebaseui-core/src/index.ts"
19+
],
20+
"@firebase-ui/core/*": [
21+
"../firebaseui-core/src/*"
22+
],
23+
"~/*": [
24+
"src/*"
25+
]
26+
}
1527
}
1628
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { render, screen, fireEvent, act } from "@testing-library/react";
3+
import { EmailLinkForm } from "../../../../src/auth/forms/email-link-form";
4+
5+
// Mock Firebase UI Core
6+
vi.mock("@firebase-ui/core", () => {
7+
const FirebaseUIError = vi.fn();
8+
FirebaseUIError.prototype.message = "Test error message";
9+
10+
return {
11+
FirebaseUIError: class FirebaseUIError {
12+
message: string;
13+
code?: string;
14+
15+
constructor({ code, message }: { code: string; message: string }) {
16+
this.code = code;
17+
this.message = message;
18+
}
19+
},
20+
fuiCompleteEmailLinkSignIn: vi.fn(),
21+
fuiSendSignInLinkToEmail: vi.fn(),
22+
createEmailLinkFormSchema: () => ({
23+
email: {
24+
validate: (value: string) => {
25+
if (!value) return "Email is required";
26+
return undefined;
27+
},
28+
},
29+
}),
30+
getTranslation: (section: string, key: string) => {
31+
const defaultTranslations: Record<string, Record<string, string>> = {
32+
labels: {
33+
emailAddress: "Email",
34+
sendSignInLink: "sendSignInLink",
35+
},
36+
messages: {
37+
signInLinkSent: "Sign-in link sent!",
38+
},
39+
errors: {
40+
unknownError: "An unknown error occurred",
41+
},
42+
};
43+
44+
return defaultTranslations[section]?.[key] || `${section}.${key}`;
45+
},
46+
};
47+
});
48+
49+
import {
50+
FirebaseUIError,
51+
fuiSendSignInLinkToEmail,
52+
fuiCompleteEmailLinkSignIn,
53+
} from "@firebase-ui/core";
54+
55+
// Mock hooks
56+
vi.mock("../../../../src/hooks", () => ({
57+
useAuth: vi.fn(() => ({})),
58+
useFirebaseUIAuth: vi.fn(() => ({})),
59+
useFirebaseUIConfig: vi.fn(() => ({
60+
config: {
61+
signInOptions: ["email"],
62+
callbacks: {
63+
signInSuccessWithAuthResult: vi.fn(),
64+
},
65+
},
66+
})),
67+
useConfig: vi.fn(() => ({
68+
language: "en",
69+
signInOptions: ["email"],
70+
callbacks: {
71+
signInSuccessWithAuthResult: vi.fn(),
72+
},
73+
enableAutoUpgradeAnonymous: false,
74+
enableHandleExistingCredential: false,
75+
})),
76+
useTranslations: vi.fn(() => ({
77+
signInWithEmail: "Sign in with email",
78+
emailLinkSignInMessage: "Check your email for sign-in link",
79+
})),
80+
}));
81+
82+
// Mock form
83+
vi.mock("@tanstack/react-form", () => ({
84+
useForm: () => {
85+
const formState = {
86+
87+
};
88+
89+
return {
90+
Field: ({ name, children }: any) => {
91+
// Create a mock field with the required methods and state management
92+
const field = {
93+
name,
94+
handleBlur: vi.fn(),
95+
handleChange: vi.fn((value: string) => {
96+
formState[name as keyof typeof formState] = value;
97+
}),
98+
state: {
99+
value: formState[name as keyof typeof formState] || "",
100+
meta: { isTouched: false, errors: [] },
101+
},
102+
};
103+
104+
return children(field);
105+
},
106+
handleSubmit: vi.fn().mockImplementation(async () => {
107+
// Call the onSubmit handler with the form state
108+
await (global as any).formOnSubmit?.({ value: formState });
109+
}),
110+
};
111+
},
112+
}));
113+
114+
// Mock components
115+
vi.mock("../../../../src/components/field-info", () => ({
116+
FieldInfo: () => <div data-testid="field-info" />,
117+
}));
118+
119+
vi.mock("../../../../src/components/terms-and-privacy", () => ({
120+
TermsAndPrivacy: () => (
121+
<div data-testid="terms-and-privacy">Terms & Privacy</div>
122+
),
123+
}));
124+
125+
vi.mock("../../../../src/components/button", () => ({
126+
Button: ({
127+
children,
128+
onClick,
129+
type,
130+
...rest
131+
}: {
132+
children: React.ReactNode;
133+
onClick?: () => void;
134+
type?: "submit" | "reset" | "button";
135+
[key: string]: any;
136+
}) => (
137+
<button onClick={onClick} type={type} data-testid="submit-button" {...rest}>
138+
{children}
139+
</button>
140+
),
141+
}));
142+
143+
// Mock react useState to control state in tests
144+
vi.mock("react", async () => {
145+
const actual = (await vi.importActual("react")) as typeof import("react");
146+
return {
147+
...actual,
148+
useState: vi.fn().mockImplementation((initialValue) => {
149+
const [state, setState] = actual.useState(initialValue);
150+
return [state, setState];
151+
}),
152+
};
153+
});
154+
155+
const mockSendSignInLink = vi.mocked(fuiSendSignInLinkToEmail);
156+
const mockCompleteEmailLink = vi.mocked(fuiCompleteEmailLinkSignIn);
157+
158+
describe("EmailLinkForm", () => {
159+
beforeEach(() => {
160+
vi.clearAllMocks();
161+
// Reset the global state
162+
(global as any).formOnSubmit = null;
163+
});
164+
165+
it("renders the email link form", () => {
166+
render(<EmailLinkForm />);
167+
168+
expect(screen.getByLabelText("Email")).toBeInTheDocument();
169+
expect(screen.getByText("sendSignInLink")).toBeInTheDocument();
170+
});
171+
172+
it("attempts to complete email link sign-in on load", () => {
173+
mockCompleteEmailLink.mockResolvedValue(null);
174+
175+
render(<EmailLinkForm />);
176+
177+
expect(mockCompleteEmailLink).toHaveBeenCalled();
178+
});
179+
180+
it("submits the form and sends sign-in link to email", async () => {
181+
mockSendSignInLink.mockResolvedValue(undefined);
182+
183+
const { container } = render(<EmailLinkForm />);
184+
185+
// Get the form element
186+
const form = container.getElementsByClassName(
187+
"fui-form"
188+
)[0] as HTMLFormElement;
189+
190+
// Set up the form submit handler
191+
(global as any).formOnSubmit = async ({
192+
value,
193+
}: {
194+
value: { email: string };
195+
}) => {
196+
await fuiSendSignInLinkToEmail(
197+
expect.anything(),
198+
value.email,
199+
expect.anything()
200+
);
201+
};
202+
203+
// Submit the form
204+
await act(async () => {
205+
fireEvent.submit(form);
206+
});
207+
208+
expect(mockSendSignInLink).toHaveBeenCalledWith(
209+
expect.anything(),
210+
211+
expect.anything()
212+
);
213+
});
214+
215+
it("handles error when sending email link fails", async () => {
216+
// Mock the error that will be thrown
217+
const mockError = new FirebaseUIError({
218+
code: "auth/invalid-email",
219+
message: "Invalid email",
220+
});
221+
mockSendSignInLink.mockRejectedValue(mockError);
222+
223+
render(<EmailLinkForm />);
224+
225+
// Get the form element
226+
const form = screen.getByRole("form");
227+
228+
// Submit the form
229+
await act(async () => {
230+
fireEvent.submit(form);
231+
});
232+
233+
// Verify the error message is displayed
234+
const errorElement = screen.getByText("Invalid email");
235+
expect(errorElement).toHaveClass("fui-form__error");
236+
});
237+
238+
it("handles success when email is sent", async () => {
239+
mockSendSignInLink.mockResolvedValue(undefined);
240+
241+
const { container } = render(<EmailLinkForm />);
242+
243+
// Get the form element
244+
const form = container.getElementsByClassName(
245+
"fui-form"
246+
)[0] as HTMLFormElement;
247+
248+
// Set up the form submit handler
249+
(global as any).formOnSubmit = async ({
250+
value,
251+
}: {
252+
value: { email: string };
253+
}) => {
254+
await fuiSendSignInLinkToEmail(
255+
expect.anything(),
256+
value.email,
257+
expect.anything()
258+
);
259+
};
260+
261+
// Submit the form
262+
await act(async () => {
263+
fireEvent.submit(form);
264+
});
265+
266+
expect(mockSendSignInLink).toHaveBeenCalledWith(
267+
expect.anything(),
268+
269+
expect.anything()
270+
);
271+
272+
// Verify success message is displayed
273+
expect(
274+
screen.getByText("Sign-in link sent!", { exact: false })
275+
).toBeInTheDocument();
276+
});
277+
});

0 commit comments

Comments
 (0)