Skip to content

Commit d3fc8c8

Browse files
committed
fix(react): Fix tests by using separate schema + waitFor async submissions
1 parent 5b57d08 commit d3fc8c8

File tree

8 files changed

+220
-24
lines changed

8 files changed

+220
-24
lines changed

packages/core/src/schemas.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createMockUI } from "~/tests/utils";
33
import {
44
createEmailLinkAuthFormSchema,
55
createForgotPasswordAuthFormSchema,
6+
createMultiFactorPhoneAuthAssertionNumberFormSchema,
67
createPhoneAuthNumberFormSchema,
78
createPhoneAuthVerifyFormSchema,
89
createSignInAuthFormSchema,
@@ -309,3 +310,77 @@ describe("createPhoneAuthVerifyFormSchema", () => {
309310
).toBe(true);
310311
});
311312
});
313+
314+
describe("createMultiFactorPhoneAuthAssertionNumberFormSchema", () => {
315+
it("should create a multi-factor phone auth assertion number form schema and show missing phone number error", () => {
316+
const testLocale = registerLocale("test", {
317+
errors: {
318+
missingPhoneNumber: "createMultiFactorPhoneAuthAssertionNumberFormSchema + missingPhoneNumber",
319+
},
320+
});
321+
322+
const mockUI = createMockUI({
323+
locale: testLocale,
324+
});
325+
326+
const schema = createMultiFactorPhoneAuthAssertionNumberFormSchema(mockUI);
327+
328+
const result = schema.safeParse({
329+
phoneNumber: "",
330+
});
331+
332+
expect(result.success).toBe(false);
333+
expect(result.error).toBeDefined();
334+
expect(result.error?.issues[0]?.message).toBe(
335+
"createMultiFactorPhoneAuthAssertionNumberFormSchema + missingPhoneNumber"
336+
);
337+
});
338+
339+
it("should create a multi-factor phone auth assertion number form schema and show an error if the phone number is too long", () => {
340+
const testLocale = registerLocale("test", {
341+
errors: {
342+
invalidPhoneNumber: "createMultiFactorPhoneAuthAssertionNumberFormSchema + invalidPhoneNumber",
343+
},
344+
});
345+
346+
const mockUI = createMockUI({
347+
locale: testLocale,
348+
});
349+
350+
const schema = createMultiFactorPhoneAuthAssertionNumberFormSchema(mockUI);
351+
352+
const result = schema.safeParse({
353+
phoneNumber: "12345678901",
354+
});
355+
356+
expect(result.success).toBe(false);
357+
expect(result.error).toBeDefined();
358+
expect(result.error?.issues[0]?.message).toBe(
359+
"createMultiFactorPhoneAuthAssertionNumberFormSchema + invalidPhoneNumber"
360+
);
361+
});
362+
363+
it("should accept valid phone number without requiring displayName", () => {
364+
const testLocale = registerLocale("test", {
365+
errors: {
366+
missingPhoneNumber: "missing",
367+
invalidPhoneNumber: "invalid",
368+
},
369+
});
370+
371+
const mockUI = createMockUI({
372+
locale: testLocale,
373+
});
374+
375+
const schema = createMultiFactorPhoneAuthAssertionNumberFormSchema(mockUI);
376+
377+
const result = schema.safeParse({
378+
phoneNumber: "1234567890",
379+
});
380+
381+
expect(result.success).toBe(true);
382+
if (result.success) {
383+
expect(result.data).toEqual({ phoneNumber: "1234567890" });
384+
}
385+
});
386+
});

packages/core/src/schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export function createMultiFactorPhoneAuthNumberFormSchema(ui: FirebaseUI) {
8080
});
8181
}
8282

83+
export function createMultiFactorPhoneAuthAssertionNumberFormSchema(ui: FirebaseUI) {
84+
return createPhoneAuthNumberFormSchema(ui);
85+
}
86+
8387
export function createMultiFactorPhoneAuthVerifyFormSchema(ui: FirebaseUI) {
8488
return createPhoneAuthVerifyFormSchema(ui);
8589
}

packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.test.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
18-
import { render, screen, renderHook, cleanup } from "@testing-library/react";
18+
import { render, screen, renderHook, cleanup, fireEvent, waitFor } from "@testing-library/react";
1919
import {
2020
SmsMultiFactorAssertionForm,
2121
useSmsMultiFactorAssertionPhoneFormAction,
@@ -299,7 +299,7 @@ describe("<SmsMultiFactorAssertionForm />", () => {
299299

300300
const mockHint = {
301301
factorId: "phone" as const,
302-
phoneNumber: "+1234567890",
302+
phoneNumber: "+123456789", // Max 10 chars for schema validation
303303
uid: "test-uid",
304304
enrollmentTime: "2023-01-01T00:00:00Z",
305305
};
@@ -319,27 +319,27 @@ describe("<SmsMultiFactorAssertionForm />", () => {
319319
})
320320
);
321321

322-
// Step 1: Send code
323-
const sendCodeButton = screen.getByRole("button", { name: "sendCode" });
322+
const sendCodeForm = screen.getByRole("button", { name: "sendCode" }).closest("form");
324323
await act(async () => {
325-
sendCodeButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
324+
fireEvent.submit(sendCodeForm!);
326325
});
327326

328-
// Now verify form should be rendered; enter code and submit
329-
const codeInput = await screen.findByRole("textbox", { name: /verificationCode/i });
330-
const verifyButton = screen.getByRole("button", { name: "verifyCode" });
327+
const codeInput = await waitFor(() => screen.findByRole("textbox", { name: /verificationCode/i }));
328+
const form = codeInput.closest("form");
331329

332330
await act(async () => {
333-
(codeInput as HTMLInputElement).value = "123456";
334-
codeInput.dispatchEvent(new Event("input", { bubbles: true }));
331+
fireEvent.change(codeInput, { target: { value: "123456" } });
335332
});
336333

337334
await act(async () => {
338-
verifyButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
335+
fireEvent.submit(form!);
336+
});
337+
338+
await waitFor(() => {
339+
expect(verifyPhoneNumber).toHaveBeenCalled();
340+
expect(signInWithMultiFactorAssertion).toHaveBeenCalled();
339341
});
340342

341-
expect(verifyPhoneNumber).toHaveBeenCalled();
342-
expect(signInWithMultiFactorAssertion).toHaveBeenCalled();
343343
expect(onSuccessMock).toHaveBeenCalledTimes(1);
344344
expect(onSuccessMock).toHaveBeenCalledWith(
345345
expect.objectContaining({ user: expect.objectContaining({ uid: "sms-cred-user" }) })

packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { signInWithMultiFactorAssertion, FirebaseUIError, getTranslation, verifyPhoneNumber } from "@firebase-ui/core";
1111
import { form } from "~/components/form";
1212
import {
13-
useMultiFactorPhoneAuthNumberFormSchema,
13+
useMultiFactorPhoneAuthAssertionNumberFormSchema,
1414
useMultiFactorPhoneAuthVerifyFormSchema,
1515
useRecaptchaVerifier,
1616
useUI,
@@ -43,7 +43,7 @@ export function useSmsMultiFactorAssertionPhoneForm({
4343
onSuccess,
4444
}: UseSmsMultiFactorAssertionPhoneForm) {
4545
const action = useSmsMultiFactorAssertionPhoneFormAction();
46-
const schema = useMultiFactorPhoneAuthNumberFormSchema();
46+
const schema = useMultiFactorPhoneAuthAssertionNumberFormSchema();
4747

4848
return form.useAppForm({
4949
defaultValues: {

packages/react/src/auth/forms/mfa/totp-multi-factor-assertion-form.test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
19-
import { render, screen, renderHook, cleanup } from "@testing-library/react";
19+
import { render, screen, renderHook, cleanup, fireEvent, waitFor } from "@testing-library/react";
2020
import {
2121
TotpMultiFactorAssertionForm,
2222
useTotpMultiFactorAssertionFormAction,
@@ -234,20 +234,20 @@ describe("<TotpMultiFactorAssertionForm />", () => {
234234
);
235235

236236
const input = screen.getByRole("textbox", { name: /verificationCode/i });
237-
const submit = screen.getByRole("button", { name: "verifyCode" });
237+
const form = input.closest("form");
238238

239239
await act(async () => {
240-
// Fill input and submit form
241-
(input as HTMLInputElement).value = "123456";
242-
// Dispatch input event if needed by validators
243-
input.dispatchEvent(new Event("input", { bubbles: true }));
240+
fireEvent.change(input, { target: { value: "123456" } });
244241
});
245242

246243
await act(async () => {
247-
submit.dispatchEvent(new MouseEvent("click", { bubbles: true }));
244+
fireEvent.submit(form!);
245+
});
246+
247+
await waitFor(() => {
248+
expect(signInWithMultiFactorAssertion).toHaveBeenCalled();
248249
});
249250

250-
expect(signInWithMultiFactorAssertion).toHaveBeenCalled();
251251
expect(onSuccessMock).toHaveBeenCalledTimes(1);
252252
expect(onSuccessMock).toHaveBeenCalledWith(
253253
expect.objectContaining({ user: expect.objectContaining({ uid: "totp-cred-user" }) })

packages/react/src/auth/screens/oauth-screen.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import { describe, it, expect, vi, afterEach } from "vitest";
17-
import { render, screen, cleanup } from "@testing-library/react";
17+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
1818
import { OAuthScreen } from "~/auth/screens/oauth-screen";
1919
import { CreateFirebaseUIProvider, createMockUI } from "~/tests/utils";
2020
import { registerLocale } from "@firebase-ui/translations";

packages/react/src/hooks.test.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
useSignUpAuthFormSchema,
2525
useForgotPasswordAuthFormSchema,
2626
useEmailLinkAuthFormSchema,
27+
useMultiFactorPhoneAuthAssertionNumberFormSchema,
2728
usePhoneAuthNumberFormSchema,
2829
usePhoneAuthVerifyFormSchema,
2930
useRecaptchaVerifier,
@@ -712,6 +713,116 @@ describe("usePhoneAuthVerifyFormSchema", () => {
712713
});
713714
});
714715

716+
describe("useMultiFactorPhoneAuthAssertionNumberFormSchema", () => {
717+
beforeEach(() => {
718+
vi.clearAllMocks();
719+
cleanup();
720+
});
721+
722+
it("returns schema with default English error messages", () => {
723+
const mockUI = createMockUI();
724+
725+
const { result } = renderHook(() => useMultiFactorPhoneAuthAssertionNumberFormSchema(), {
726+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
727+
});
728+
729+
const schema = result.current;
730+
731+
const phoneResult = schema.safeParse({ phoneNumber: "invalid-phone" });
732+
expect(phoneResult.success).toBe(false);
733+
if (!phoneResult.success) {
734+
expect(phoneResult.error.issues[0]!.message).toBe(enUs.translations.errors!.invalidPhoneNumber);
735+
}
736+
});
737+
738+
it("returns schema with custom error messages when locale changes", () => {
739+
const customTranslations = {
740+
errors: {
741+
invalidPhoneNumber: "Por favor ingresa un número de teléfono válido",
742+
},
743+
};
744+
745+
const customLocale = registerLocale("es-ES", customTranslations);
746+
const mockUI = createMockUI({ locale: customLocale });
747+
748+
const { result } = renderHook(() => useMultiFactorPhoneAuthAssertionNumberFormSchema(), {
749+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
750+
});
751+
752+
const schema = result.current;
753+
754+
const phoneResult = schema.safeParse({ phoneNumber: "invalid-phone" });
755+
expect(phoneResult.success).toBe(false);
756+
if (!phoneResult.success) {
757+
expect(phoneResult.error.issues[0]!.message).toBe("Por favor ingresa un número de teléfono válido");
758+
}
759+
});
760+
761+
it("returns stable reference when UI hasn't changed", () => {
762+
const mockUI = createMockUI();
763+
764+
const { result, rerender } = renderHook(() => useMultiFactorPhoneAuthAssertionNumberFormSchema(), {
765+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
766+
});
767+
768+
const initialSchema = result.current;
769+
770+
rerender();
771+
772+
expect(result.current).toBe(initialSchema);
773+
});
774+
775+
it("returns new schema when locale changes", () => {
776+
const mockUI = createMockUI();
777+
778+
const { result, rerender } = renderHook(() => useMultiFactorPhoneAuthAssertionNumberFormSchema(), {
779+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
780+
});
781+
782+
const initialSchema = result.current;
783+
784+
const customTranslations = {
785+
errors: {
786+
invalidPhoneNumber: "Custom phone error",
787+
},
788+
};
789+
const customLocale = registerLocale("fr-FR", customTranslations);
790+
791+
act(() => {
792+
mockUI.get().setLocale(customLocale);
793+
});
794+
795+
rerender();
796+
797+
expect(result.current).not.toBe(initialSchema);
798+
799+
const phoneResult = result.current.safeParse({ phoneNumber: "invalid-phone" });
800+
expect(phoneResult.success).toBe(false);
801+
802+
if (!phoneResult.success) {
803+
expect(phoneResult.error.issues[0]!.message).toBe("Custom phone error");
804+
}
805+
});
806+
807+
it("accepts valid phone number without requiring displayName", () => {
808+
const mockUI = createMockUI();
809+
810+
const { result } = renderHook(() => useMultiFactorPhoneAuthAssertionNumberFormSchema(), {
811+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
812+
});
813+
814+
const schema = result.current;
815+
816+
const phoneResult = schema.safeParse({ phoneNumber: "1234567890" });
817+
expect(phoneResult.success).toBe(true);
818+
if (phoneResult.success) {
819+
expect(phoneResult.data).toEqual({ phoneNumber: "1234567890" });
820+
// Should not have displayName field
821+
expect(phoneResult.data).not.toHaveProperty("displayName");
822+
}
823+
});
824+
});
825+
715826
describe("useRedirectError", () => {
716827
beforeEach(() => {
717828
vi.clearAllMocks();

packages/react/src/hooks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
createEmailLinkAuthFormSchema,
2121
createForgotPasswordAuthFormSchema,
2222
createMultiFactorPhoneAuthNumberFormSchema,
23+
createMultiFactorPhoneAuthAssertionNumberFormSchema,
2324
createMultiFactorPhoneAuthVerifyFormSchema,
2425
createMultiFactorTotpAuthNumberFormSchema,
2526
createMultiFactorTotpAuthVerifyFormSchema,
@@ -92,6 +93,11 @@ export function useMultiFactorPhoneAuthNumberFormSchema() {
9293
return useMemo(() => createMultiFactorPhoneAuthNumberFormSchema(ui), [ui]);
9394
}
9495

96+
export function useMultiFactorPhoneAuthAssertionNumberFormSchema() {
97+
const ui = useUI();
98+
return useMemo(() => createMultiFactorPhoneAuthAssertionNumberFormSchema(ui), [ui]);
99+
}
100+
95101
export function useMultiFactorPhoneAuthVerifyFormSchema() {
96102
const ui = useUI();
97103
return useMemo(() => createMultiFactorPhoneAuthVerifyFormSchema(ui), [ui]);

0 commit comments

Comments
 (0)