Skip to content

Commit 158279a

Browse files
committed
feat(angular): Add SMS/TOTP assertion components
1 parent e4de63c commit 158279a

File tree

6 files changed

+887
-13
lines changed

6 files changed

+887
-13
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import { render, screen, fireEvent, waitFor } from "@testing-library/angular";
17+
import { TestBed } from "@angular/core/testing";
18+
import { PhoneMultiFactorGenerator } from "firebase/auth";
19+
20+
import {
21+
SmsMultiFactorAssertionFormComponent,
22+
SmsMultiFactorAssertionPhoneFormComponent,
23+
SmsMultiFactorAssertionVerifyFormComponent,
24+
} from "./sms-multi-factor-assertion-form";
25+
26+
import {
27+
verifyPhoneNumber,
28+
signInWithMultiFactorAssertion,
29+
FirebaseUIError,
30+
} from "../../../tests/test-helpers";
31+
32+
describe("<fui-sms-multi-factor-assertion-form>", () => {
33+
beforeEach(() => {
34+
const { injectTranslation, injectUI, injectMultiFactorPhoneAuthNumberFormSchema, injectMultiFactorPhoneAuthVerifyFormSchema } = require("../../../provider");
35+
36+
injectTranslation.mockImplementation((category: string, key: string) => {
37+
const mockTranslations: Record<string, Record<string, string>> = {
38+
labels: {
39+
phoneNumber: "Phone Number",
40+
sendCode: "Send Code",
41+
verificationCode: "Verification Code",
42+
verifyCode: "Verify Code",
43+
},
44+
errors: {
45+
unknownError: "An unknown error occurred",
46+
},
47+
};
48+
return () => mockTranslations[category]?.[key] || `${category}.${key}`;
49+
});
50+
51+
injectUI.mockImplementation(() => {
52+
return () => ({
53+
auth: {},
54+
});
55+
});
56+
57+
injectMultiFactorPhoneAuthNumberFormSchema.mockReturnValue(() => {
58+
const { z } = require("zod");
59+
return z.object({
60+
phoneNumber: z.string().min(1, "Phone number is required"),
61+
});
62+
});
63+
64+
injectMultiFactorPhoneAuthVerifyFormSchema.mockReturnValue(() => {
65+
const { z } = require("zod");
66+
return z.object({
67+
verificationCode: z.string().min(1, "Verification code is required"),
68+
});
69+
});
70+
71+
// Mock FirebaseUI Core functions
72+
verifyPhoneNumber.mockResolvedValue("test-verification-id");
73+
signInWithMultiFactorAssertion.mockResolvedValue({});
74+
75+
// Mock Firebase Auth classes
76+
const { PhoneAuthProvider, PhoneMultiFactorGenerator } = require("firebase/auth");
77+
PhoneAuthProvider.credential = jest.fn().mockReturnValue({});
78+
PhoneMultiFactorGenerator.assertion = jest.fn().mockReturnValue({});
79+
});
80+
81+
it("renders phone form initially", async () => {
82+
const mockHint = {
83+
factorId: PhoneMultiFactorGenerator.FACTOR_ID,
84+
displayName: "Phone",
85+
phoneNumber: "+1234567890",
86+
};
87+
88+
await render(SmsMultiFactorAssertionFormComponent, {
89+
componentInputs: {
90+
hint: mockHint,
91+
},
92+
imports: [SmsMultiFactorAssertionFormComponent],
93+
});
94+
95+
expect(screen.getByLabelText("Phone Number")).toBeInTheDocument();
96+
expect(screen.getByDisplayValue("+1234567890")).toBeInTheDocument();
97+
expect(screen.getByRole("button", { name: "Send Code" })).toBeInTheDocument();
98+
});
99+
100+
it("switches to verify form after phone submission", async () => {
101+
const mockHint = {
102+
factorId: PhoneMultiFactorGenerator.FACTOR_ID,
103+
displayName: "Phone",
104+
phoneNumber: "+1234567890",
105+
};
106+
107+
const { fixture } = await render(SmsMultiFactorAssertionFormComponent, {
108+
componentInputs: {
109+
hint: mockHint,
110+
},
111+
imports: [SmsMultiFactorAssertionFormComponent],
112+
});
113+
114+
// Initially shows phone form
115+
expect(screen.getByLabelText("Phone Number")).toBeInTheDocument();
116+
117+
// Submit the phone form
118+
fireEvent.click(screen.getByRole("button", { name: "Send Code" }));
119+
120+
// Wait for the form to switch
121+
await waitFor(() => {
122+
expect(screen.getByLabelText("Verification Code")).toBeInTheDocument();
123+
});
124+
125+
expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument();
126+
expect(screen.queryByLabelText("Phone Number")).not.toBeInTheDocument();
127+
});
128+
129+
it("emits onSuccess when verification is successful", async () => {
130+
const mockHint = {
131+
factorId: PhoneMultiFactorGenerator.FACTOR_ID,
132+
displayName: "Phone",
133+
phoneNumber: "+1234567890",
134+
};
135+
136+
const { fixture } = await render(SmsMultiFactorAssertionFormComponent, {
137+
componentInputs: {
138+
hint: mockHint,
139+
},
140+
imports: [SmsMultiFactorAssertionFormComponent],
141+
});
142+
143+
const onSuccessSpy = jest.fn();
144+
fixture.componentInstance.onSuccess.subscribe(onSuccessSpy);
145+
146+
// Submit phone form to get to verification form
147+
fireEvent.click(screen.getByRole("button", { name: "Send Code" }));
148+
149+
await waitFor(() => {
150+
expect(screen.getByLabelText("Verification Code")).toBeInTheDocument();
151+
});
152+
153+
// Fill in verification code and submit
154+
fireEvent.change(screen.getByLabelText("Verification Code"), {
155+
target: { value: "123456" },
156+
});
157+
fireEvent.click(screen.getByRole("button", { name: "Verify Code" }));
158+
159+
await waitFor(() => {
160+
expect(onSuccessSpy).toHaveBeenCalled();
161+
});
162+
});
163+
});
164+
165+
describe("<fui-sms-multi-factor-assertion-phone-form>", () => {
166+
beforeEach(() => {
167+
const { injectTranslation, injectUI, injectMultiFactorPhoneAuthNumberFormSchema } = require("../../../provider");
168+
169+
injectTranslation.mockImplementation((category: string, key: string) => {
170+
const mockTranslations: Record<string, Record<string, string>> = {
171+
labels: {
172+
phoneNumber: "Phone Number",
173+
sendCode: "Send Code",
174+
},
175+
errors: {
176+
unknownError: "An unknown error occurred",
177+
},
178+
};
179+
return () => mockTranslations[category]?.[key] || `${category}.${key}`;
180+
});
181+
182+
injectUI.mockImplementation(() => {
183+
return () => ({
184+
auth: {},
185+
});
186+
});
187+
188+
injectMultiFactorPhoneAuthNumberFormSchema.mockReturnValue(() => {
189+
const { z } = require("zod");
190+
return z.object({
191+
phoneNumber: z.string().min(1, "Phone number is required"),
192+
});
193+
});
194+
195+
// Mock FirebaseUI Core functions
196+
verifyPhoneNumber.mockResolvedValue("test-verification-id");
197+
});
198+
199+
it("renders phone form with phone number from hint", async () => {
200+
const mockHint = {
201+
factorId: PhoneMultiFactorGenerator.FACTOR_ID,
202+
displayName: "Phone",
203+
phoneNumber: "+1234567890",
204+
};
205+
206+
await render(SmsMultiFactorAssertionPhoneFormComponent, {
207+
componentInputs: {
208+
hint: mockHint,
209+
},
210+
imports: [SmsMultiFactorAssertionPhoneFormComponent],
211+
});
212+
213+
const phoneInput = screen.getByLabelText("Phone Number");
214+
expect(phoneInput).toBeInTheDocument();
215+
expect(phoneInput).toHaveValue("+1234567890");
216+
});
217+
218+
it("emits onSubmit when form is submitted", async () => {
219+
const mockHint = {
220+
factorId: PhoneMultiFactorGenerator.FACTOR_ID,
221+
displayName: "Phone",
222+
phoneNumber: "+1234567890",
223+
};
224+
225+
const { fixture } = await render(SmsMultiFactorAssertionPhoneFormComponent, {
226+
componentInputs: {
227+
hint: mockHint,
228+
},
229+
imports: [SmsMultiFactorAssertionPhoneFormComponent],
230+
});
231+
232+
const onSubmitSpy = jest.fn();
233+
fixture.componentInstance.onSubmit.subscribe(onSubmitSpy);
234+
235+
fireEvent.click(screen.getByRole("button", { name: "Send Code" }));
236+
237+
await waitFor(() => {
238+
expect(onSubmitSpy).toHaveBeenCalledWith("test-verification-id");
239+
});
240+
});
241+
});
242+
243+
describe("<fui-sms-multi-factor-assertion-verify-form>", () => {
244+
beforeEach(() => {
245+
const { injectTranslation, injectUI, injectMultiFactorPhoneAuthVerifyFormSchema } = require("../../../provider");
246+
247+
injectTranslation.mockImplementation((category: string, key: string) => {
248+
const mockTranslations: Record<string, Record<string, string>> = {
249+
labels: {
250+
verificationCode: "Verification Code",
251+
verifyCode: "Verify Code",
252+
},
253+
errors: {
254+
unknownError: "An unknown error occurred",
255+
},
256+
};
257+
return () => mockTranslations[category]?.[key] || `${category}.${key}`;
258+
});
259+
260+
injectUI.mockImplementation(() => {
261+
return () => ({
262+
auth: {},
263+
});
264+
});
265+
266+
injectMultiFactorPhoneAuthVerifyFormSchema.mockReturnValue(() => {
267+
const { z } = require("zod");
268+
return z.object({
269+
verificationCode: z.string().min(1, "Verification code is required"),
270+
});
271+
});
272+
273+
// Mock FirebaseUI Core functions
274+
signInWithMultiFactorAssertion.mockResolvedValue({});
275+
276+
// Mock Firebase Auth classes
277+
const { PhoneAuthProvider, PhoneMultiFactorGenerator } = require("firebase/auth");
278+
PhoneAuthProvider.credential = jest.fn().mockReturnValue({});
279+
PhoneMultiFactorGenerator.assertion = jest.fn().mockReturnValue({});
280+
});
281+
282+
it("renders verification form", async () => {
283+
await render(SmsMultiFactorAssertionVerifyFormComponent, {
284+
componentInputs: {
285+
verificationId: "test-verification-id",
286+
},
287+
imports: [SmsMultiFactorAssertionVerifyFormComponent],
288+
});
289+
290+
expect(screen.getByLabelText("Verification Code")).toBeInTheDocument();
291+
expect(screen.getByRole("button", { name: "Verify Code" })).toBeInTheDocument();
292+
});
293+
294+
it("emits onSuccess when verification is successful", async () => {
295+
const { fixture } = await render(SmsMultiFactorAssertionVerifyFormComponent, {
296+
componentInputs: {
297+
verificationId: "test-verification-id",
298+
},
299+
imports: [SmsMultiFactorAssertionVerifyFormComponent],
300+
});
301+
302+
const onSuccessSpy = jest.fn();
303+
fixture.componentInstance.onSuccess.subscribe(onSuccessSpy);
304+
305+
// Fill in verification code and submit
306+
fireEvent.change(screen.getByLabelText("Verification Code"), {
307+
target: { value: "123456" },
308+
});
309+
fireEvent.click(screen.getByRole("button", { name: "Verify Code" }));
310+
311+
await waitFor(() => {
312+
expect(onSuccessSpy).toHaveBeenCalled();
313+
});
314+
});
315+
});

0 commit comments

Comments
 (0)