Skip to content

Commit 8e928f8

Browse files
authored
Merge pull request #1213 from firebase/@invertase/require-display-name-react
2 parents 82b6c22 + 513b23f commit 8e928f8

File tree

7 files changed

+292
-19
lines changed

7 files changed

+292
-19
lines changed

packages/react/src/auth/forms/email-link-auth-form.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ export function EmailLinkAuthForm({ onEmailSent, onSignIn }: EmailLinkAuthFormPr
113113
>
114114
<form.AppForm>
115115
<fieldset>
116-
<form.AppField name="email">{(field) => <field.Input label="Email" type="email" />}</form.AppField>
116+
<form.AppField name="email">
117+
{(field) => <field.Input label={getTranslation(ui, "labels", "emailAddress")} type="email" />}
118+
</form.AppField>
117119
</fieldset>
118120
<Policies />
119121
<fieldset>

packages/react/src/auth/forms/forgot-password-auth-form.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export function ForgotPasswordAuthForm({ onBackToSignInClick, onPasswordSent }:
9393
>
9494
<form.AppForm>
9595
<fieldset>
96-
<form.AppField name="email">{(field) => <field.Input label="Email" type="email" />}</form.AppField>
96+
<form.AppField name="email">
97+
{(field) => <field.Input label={getTranslation(ui, "labels", "emailAddress")} type="email" />}
98+
</form.AppField>
9799
</fieldset>
98100
<Policies />
99101
<fieldset>

packages/react/src/auth/forms/sign-in-auth-form.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,14 @@ export function SignInAuthForm({ onSignIn, onForgotPasswordClick, onRegisterClic
8888
>
8989
<form.AppForm>
9090
<fieldset>
91-
<form.AppField name="email">{(field) => <field.Input label="Email" type="email" />}</form.AppField>
91+
<form.AppField name="email">
92+
{(field) => <field.Input label={getTranslation(ui, "labels", "emailAddress")} type="email" />}
93+
</form.AppField>
9294
</fieldset>
9395
<fieldset>
9496
<form.AppField name="password">
9597
{(field) => (
96-
<field.Input label="Password" type="password">
98+
<field.Input label={getTranslation(ui, "labels", "password")} type="password">
9799
{onForgotPasswordClick ? (
98100
<form.Action onClick={onForgotPasswordClick}>
99101
{getTranslation(ui, "labels", "forgotPassword")}

packages/react/src/auth/forms/sign-up-auth-form.test.tsx

Lines changed: 254 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1818
import { render, screen, fireEvent, renderHook, cleanup } from "@testing-library/react";
19-
import { SignUpAuthForm, useSignUpAuthForm, useSignUpAuthFormAction } from "./sign-up-auth-form";
19+
import { SignUpAuthForm, useSignUpAuthForm, useSignUpAuthFormAction, useRequireDisplayName } from "./sign-up-auth-form";
2020
import { act } from "react";
2121
import { createUserWithEmailAndPassword } from "@firebase-ui/core";
2222
import { createFirebaseUIProvider, createMockUI } from "~/tests/utils";
@@ -63,7 +63,8 @@ describe("useSignUpAuthFormAction", () => {
6363
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
6464
expect.any(Object),
6565
66-
"password123"
66+
"password123",
67+
undefined
6768
);
6869
});
6970

@@ -88,7 +89,8 @@ describe("useSignUpAuthFormAction", () => {
8889
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
8990
expect.any(Object),
9091
91-
"password123"
92+
"password123",
93+
undefined
9294
);
9395
});
9496

@@ -115,7 +117,35 @@ describe("useSignUpAuthFormAction", () => {
115117
});
116118
}).rejects.toThrow("unknownError");
117119

118-
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(mockUI.get(), "[email protected]", "password123");
120+
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
121+
mockUI.get(),
122+
123+
"password123",
124+
undefined
125+
);
126+
});
127+
128+
it("should return a callback which accepts email, password, and displayName", async () => {
129+
const mockCredential = { credential: true } as unknown as UserCredential;
130+
const createUserWithEmailAndPasswordMock = vi
131+
.mocked(createUserWithEmailAndPassword)
132+
.mockResolvedValue(mockCredential);
133+
const mockUI = createMockUI();
134+
135+
const { result } = renderHook(() => useSignUpAuthFormAction(), {
136+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
137+
});
138+
139+
await act(async () => {
140+
await result.current({ email: "[email protected]", password: "password123", displayName: "John Doe" });
141+
});
142+
143+
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
144+
expect.any(Object),
145+
146+
"password123",
147+
"John Doe"
148+
);
119149
});
120150
});
121151

@@ -129,8 +159,11 @@ describe("useSignUpAuthForm", () => {
129159
});
130160

131161
it("should allow the form to be submitted", async () => {
162+
const mockCredential = { credential: true } as unknown as UserCredential;
132163
const mockUI = createMockUI();
133-
const createUserWithEmailAndPasswordMock = vi.mocked(createUserWithEmailAndPassword);
164+
const createUserWithEmailAndPasswordMock = vi
165+
.mocked(createUserWithEmailAndPassword)
166+
.mockResolvedValue(mockCredential);
134167

135168
const { result } = renderHook(() => useSignUpAuthForm(), {
136169
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
@@ -139,13 +172,19 @@ describe("useSignUpAuthForm", () => {
139172
act(() => {
140173
result.current.setFieldValue("email", "[email protected]");
141174
result.current.setFieldValue("password", "password123");
175+
// Don't set displayName - let it be undefined (optional)
142176
});
143177

144178
await act(async () => {
145179
await result.current.handleSubmit();
146180
});
147181

148-
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(mockUI.get(), "[email protected]", "password123");
182+
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
183+
mockUI.get(),
184+
185+
"password123",
186+
undefined
187+
);
149188
});
150189

151190
it("should not allow the form to be submitted if the form is invalid", async () => {
@@ -167,18 +206,50 @@ describe("useSignUpAuthForm", () => {
167206
expect(result.current.getFieldMeta("email")!.errors[0].length).toBeGreaterThan(0);
168207
expect(createUserWithEmailAndPasswordMock).not.toHaveBeenCalled();
169208
});
209+
210+
it("should allow the form to be submitted with displayName", async () => {
211+
const mockUI = createMockUI();
212+
const createUserWithEmailAndPasswordMock = vi.mocked(createUserWithEmailAndPassword);
213+
214+
const { result } = renderHook(() => useSignUpAuthForm(), {
215+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
216+
});
217+
218+
act(() => {
219+
result.current.setFieldValue("email", "[email protected]");
220+
result.current.setFieldValue("password", "password123");
221+
result.current.setFieldValue("displayName", "John Doe");
222+
});
223+
224+
await act(async () => {
225+
await result.current.handleSubmit();
226+
});
227+
228+
expect(createUserWithEmailAndPasswordMock).toHaveBeenCalledWith(
229+
mockUI.get(),
230+
231+
"password123",
232+
"John Doe"
233+
);
234+
});
170235
});
171236

172237
describe("<SignUpAuthForm />", () => {
173238
beforeEach(() => {
174239
vi.clearAllMocks();
175240
});
176241

242+
afterEach(() => {
243+
cleanup();
244+
});
245+
177246
it("should render the form correctly", () => {
178247
const mockUI = createMockUI({
179248
locale: registerLocale("test", {
180249
labels: {
181250
createAccount: "createAccount",
251+
emailAddress: "emailAddress",
252+
password: "password",
182253
},
183254
}),
184255
});
@@ -193,9 +264,9 @@ describe("<SignUpAuthForm />", () => {
193264
const form = container.querySelectorAll("form.fui-form");
194265
expect(form.length).toBe(1);
195266

196-
// Make sure we have an email and password input
197-
expect(screen.getByRole("textbox", { name: /email/i })).toBeInTheDocument();
198-
expect(screen.getByRole("textbox", { name: /password/i })).toBeInTheDocument();
267+
// Make sure we have an email and password input with translated labels
268+
expect(screen.getByRole("textbox", { name: /emailAddress/ })).toBeInTheDocument();
269+
expect(screen.getByRole("textbox", { name: /password/ })).toBeInTheDocument();
199270

200271
// Ensure the "Create Account" button is present and is a submit button
201272
const createAccountButton = screen.getByRole("button", { name: "createAccount" });
@@ -256,4 +327,178 @@ describe("<SignUpAuthForm />", () => {
256327

257328
expect(screen.getByText("Please enter a valid email address")).toBeInTheDocument();
258329
});
330+
331+
it("should render displayName field when requireDisplayName behavior is enabled", () => {
332+
const mockUI = createMockUI({
333+
locale: registerLocale("test", {
334+
labels: {
335+
createAccount: "createAccount",
336+
emailAddress: "emailAddress",
337+
password: "password",
338+
displayName: "displayName",
339+
},
340+
}),
341+
behaviors: [
342+
{
343+
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
344+
},
345+
],
346+
});
347+
348+
const { container } = render(
349+
<FirebaseUIProvider ui={mockUI}>
350+
<SignUpAuthForm />
351+
</FirebaseUIProvider>
352+
);
353+
354+
// There should be only one form
355+
const form = container.querySelectorAll("form.fui-form");
356+
expect(form.length).toBe(1);
357+
358+
// Make sure we have all three inputs with translated labels
359+
expect(screen.getByRole("textbox", { name: /emailAddress/ })).toBeInTheDocument();
360+
expect(screen.getByRole("textbox", { name: /password/ })).toBeInTheDocument();
361+
expect(screen.getByRole("textbox", { name: /displayName/ })).toBeInTheDocument();
362+
363+
// Ensure the "Create Account" button is present and is a submit button
364+
const createAccountButton = screen.getByRole("button", { name: "createAccount" });
365+
expect(createAccountButton).toBeInTheDocument();
366+
expect(createAccountButton).toHaveAttribute("type", "submit");
367+
});
368+
369+
it("should not render displayName field when requireDisplayName behavior is not enabled", () => {
370+
const mockUI = createMockUI({
371+
locale: registerLocale("test", {
372+
labels: {
373+
createAccount: "createAccount",
374+
emailAddress: "emailAddress",
375+
password: "password",
376+
displayName: "displayName",
377+
},
378+
}),
379+
behaviors: [], // Explicitly set empty behaviors array
380+
});
381+
382+
const { container } = render(
383+
<FirebaseUIProvider ui={mockUI}>
384+
<SignUpAuthForm />
385+
</FirebaseUIProvider>
386+
);
387+
388+
const form = container.querySelectorAll("form.fui-form");
389+
expect(form.length).toBe(1);
390+
391+
expect(screen.getByRole("textbox", { name: /email/ })).toBeInTheDocument();
392+
expect(screen.getByRole("textbox", { name: /password/ })).toBeInTheDocument();
393+
expect(screen.queryByRole("textbox", { name: /displayName/ })).not.toBeInTheDocument();
394+
});
395+
396+
it("should trigger displayName validation errors when the form is blurred and requireDisplayName is enabled", () => {
397+
const mockUI = createMockUI({
398+
locale: registerLocale("test", {
399+
errors: {
400+
displayNameRequired: "Please provide a display name",
401+
},
402+
labels: {
403+
displayName: "displayName",
404+
},
405+
}),
406+
behaviors: [
407+
{
408+
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
409+
},
410+
],
411+
});
412+
413+
const { container } = render(
414+
<FirebaseUIProvider ui={mockUI}>
415+
<SignUpAuthForm />
416+
</FirebaseUIProvider>
417+
);
418+
419+
const form = container.querySelector("form.fui-form");
420+
expect(form).toBeInTheDocument();
421+
422+
const displayNameInput = screen.getByRole("textbox", { name: /displayName/ });
423+
expect(displayNameInput).toBeInTheDocument();
424+
425+
act(() => {
426+
fireEvent.blur(displayNameInput);
427+
});
428+
429+
expect(screen.getByText("Please provide a display name")).toBeInTheDocument();
430+
});
431+
432+
it("should not trigger displayName validation when requireDisplayName is not enabled", () => {
433+
const mockUI = createMockUI({
434+
locale: registerLocale("test", {
435+
errors: {
436+
displayNameRequired: "Please provide a display name",
437+
},
438+
labels: {
439+
displayName: "displayName",
440+
},
441+
}),
442+
});
443+
444+
const { container } = render(
445+
<FirebaseUIProvider ui={mockUI}>
446+
<SignUpAuthForm />
447+
</FirebaseUIProvider>
448+
);
449+
450+
const form = container.querySelector("form.fui-form");
451+
expect(form).toBeInTheDocument();
452+
453+
// Display name field should not be present
454+
expect(screen.queryByRole("textbox", { name: "displayName" })).not.toBeInTheDocument();
455+
});
456+
});
457+
458+
describe("useRequireDisplayName", () => {
459+
beforeEach(() => {
460+
vi.clearAllMocks();
461+
});
462+
463+
afterEach(() => {
464+
cleanup();
465+
});
466+
467+
it("should return true when requireDisplayName behavior is enabled", () => {
468+
const mockUI = createMockUI({
469+
behaviors: [
470+
{
471+
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
472+
},
473+
],
474+
});
475+
476+
const { result } = renderHook(() => useRequireDisplayName(), {
477+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
478+
});
479+
480+
expect(result.current).toBe(true);
481+
});
482+
483+
it("should return false when requireDisplayName behavior is not enabled", () => {
484+
const mockUI = createMockUI({
485+
behaviors: [],
486+
});
487+
488+
const { result } = renderHook(() => useRequireDisplayName(), {
489+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
490+
});
491+
492+
expect(result.current).toBe(false);
493+
});
494+
495+
it("should return false when behaviors array is empty", () => {
496+
const mockUI = createMockUI();
497+
498+
const { result } = renderHook(() => useRequireDisplayName(), {
499+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
500+
});
501+
502+
expect(result.current).toBe(false);
503+
});
259504
});

0 commit comments

Comments
 (0)