Skip to content

Commit acbc1b2

Browse files
authored
Merge pull request #7822 from Couchers-org/web/task/display-name-validation
Add validation to display name input
2 parents 4afe746 + 65d942f commit acbc1b2

4 files changed

Lines changed: 229 additions & 4 deletions

File tree

app/web/features/auth/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@
106106
"name": {
107107
"field_label": "Name",
108108
"empty_error": "Please enter your name",
109-
"required_error": "This field can't be empty."
109+
"required_error": "This field can't be empty",
110+
"invalid_characters_error": "Name can only contain letters, spaces, hyphens, and apostrophes",
111+
"min_length_error": "Name must be at least 2 characters",
112+
"max_length_error": "Name must be no more than 100 characters"
110113
},
111114
"email": {
112115
"field_label": "Email",

app/web/features/auth/signup/BasicForm.test.tsx

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,211 @@ describe("basic signup form", () => {
157157
mockConsoleError();
158158
await assertErrorAlert("Permission denied");
159159
});
160+
161+
describe("name validation", () => {
162+
it("rejects names shorter than 2 characters", async () => {
163+
render(<BasicForm />, { wrapper });
164+
165+
const user = userEvent.setup();
166+
const nameInput = await screen.findByLabelText(
167+
t("auth:basic_form.name.field_label"),
168+
);
169+
170+
await user.type(nameInput, "A");
171+
await user.tab(); // Trigger blur to validate
172+
173+
await waitFor(() => {
174+
expect(
175+
screen.getByText(t("auth:basic_form.name.min_length_error")),
176+
).toBeInTheDocument();
177+
});
178+
179+
await user.type(
180+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
181+
"test@example.com",
182+
);
183+
await user.click(
184+
await screen.findByRole("button", { name: t("global:continue") }),
185+
);
186+
187+
await waitFor(() => {
188+
expect(startSignupMock).not.toHaveBeenCalled();
189+
});
190+
});
191+
192+
it("rejects names longer than 100 characters", async () => {
193+
render(<BasicForm />, { wrapper });
194+
195+
const user = userEvent.setup();
196+
const nameInput = await screen.findByLabelText(
197+
t("auth:basic_form.name.field_label"),
198+
);
199+
200+
const longName = "A".repeat(101);
201+
await user.type(nameInput, longName);
202+
await user.tab(); // Trigger blur to validate
203+
204+
await waitFor(() => {
205+
expect(
206+
screen.getByText(t("auth:basic_form.name.max_length_error")),
207+
).toBeInTheDocument();
208+
});
209+
210+
await user.type(
211+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
212+
"test@example.com",
213+
);
214+
await user.click(
215+
await screen.findByRole("button", { name: t("global:continue") }),
216+
);
217+
218+
await waitFor(() => {
219+
expect(startSignupMock).not.toHaveBeenCalled();
220+
});
221+
});
222+
223+
it("rejects names with invalid characters like !@#$", async () => {
224+
render(<BasicForm />, { wrapper });
225+
226+
const user = userEvent.setup();
227+
const nameInput = await screen.findByLabelText(
228+
t("auth:basic_form.name.field_label"),
229+
);
230+
231+
await user.type(nameInput, "John!@#$");
232+
await user.tab(); // Trigger blur to validate
233+
234+
await waitFor(() => {
235+
expect(
236+
screen.getByText(t("auth:basic_form.name.invalid_characters_error")),
237+
).toBeInTheDocument();
238+
});
239+
240+
await user.type(
241+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
242+
"test@example.com",
243+
);
244+
await user.click(
245+
await screen.findByRole("button", { name: t("global:continue") }),
246+
);
247+
248+
await waitFor(() => {
249+
expect(startSignupMock).not.toHaveBeenCalled();
250+
});
251+
});
252+
253+
it("accepts names with hyphens and apostrophes", async () => {
254+
startSignupMock.mockResolvedValue(stateAfterStart);
255+
render(<BasicForm />, { wrapper });
256+
257+
const user = userEvent.setup();
258+
259+
await user.type(
260+
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
261+
"Anne-Marie O'Connor",
262+
);
263+
await user.type(
264+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
265+
"anne@example.com",
266+
);
267+
268+
await user.click(
269+
await screen.findByRole("button", { name: t("global:continue") }),
270+
);
271+
272+
await waitFor(() => {
273+
expect(startSignupMock).toHaveBeenCalledWith(
274+
"Anne-Marie O'Connor",
275+
"anne@example.com",
276+
undefined,
277+
);
278+
});
279+
});
280+
281+
it("accepts names with Unicode characters (Cyrillic)", async () => {
282+
startSignupMock.mockResolvedValue(stateAfterStart);
283+
render(<BasicForm />, { wrapper });
284+
285+
const user = userEvent.setup();
286+
287+
await user.type(
288+
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
289+
"Иван Иванов",
290+
);
291+
await user.type(
292+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
293+
"ivan@example.com",
294+
);
295+
296+
await user.click(
297+
await screen.findByRole("button", { name: t("global:continue") }),
298+
);
299+
300+
await waitFor(() => {
301+
expect(startSignupMock).toHaveBeenCalledWith(
302+
"Иван Иванов",
303+
"ivan@example.com",
304+
undefined,
305+
);
306+
});
307+
});
308+
309+
it("accepts names at minimum length (2 characters)", async () => {
310+
startSignupMock.mockResolvedValue(stateAfterStart);
311+
render(<BasicForm />, { wrapper });
312+
313+
const user = userEvent.setup();
314+
315+
await user.type(
316+
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
317+
"Li",
318+
);
319+
await user.type(
320+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
321+
"li@example.com",
322+
);
323+
324+
await user.click(
325+
await screen.findByRole("button", { name: t("global:continue") }),
326+
);
327+
328+
await waitFor(() => {
329+
expect(startSignupMock).toHaveBeenCalledWith(
330+
"Li",
331+
"li@example.com",
332+
undefined,
333+
);
334+
});
335+
});
336+
337+
it("rejects names with only leading/trailing spaces that result in less than 2 characters", async () => {
338+
render(<BasicForm />, { wrapper });
339+
340+
const user = userEvent.setup();
341+
const nameInput = await screen.findByLabelText(
342+
t("auth:basic_form.name.field_label"),
343+
);
344+
345+
await user.type(nameInput, " A ");
346+
await user.tab(); // Trigger blur to validate
347+
348+
await waitFor(() => {
349+
expect(
350+
screen.getByText(t("auth:basic_form.name.min_length_error")),
351+
).toBeInTheDocument();
352+
});
353+
354+
await user.type(
355+
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
356+
"test@example.com",
357+
);
358+
await user.click(
359+
await screen.findByRole("button", { name: t("global:continue") }),
360+
);
361+
362+
await waitFor(() => {
363+
expect(startSignupMock).not.toHaveBeenCalled();
364+
});
365+
});
366+
});
160367
});

app/web/features/auth/signup/BasicForm.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,26 @@ export default function BasicForm({
8787
<StyledTextField
8888
id="name"
8989
{...register("name", {
90+
required: t("auth:basic_form.name.required_error"),
91+
minLength: {
92+
value: 2,
93+
message: t("auth:basic_form.name.min_length_error"),
94+
},
95+
maxLength: {
96+
value: 100,
97+
message: t("auth:basic_form.name.max_length_error"),
98+
},
9099
pattern: {
91-
message: t("auth:basic_form.name.empty_error"),
100+
message: t("auth:basic_form.name.invalid_characters_error"),
92101
value: nameValidationPattern,
93102
},
94-
required: t("auth:basic_form.name.required_error"),
103+
validate: (value) => {
104+
const trimmed = value.trim();
105+
if (trimmed.length < 2) {
106+
return t("auth:basic_form.name.min_length_error");
107+
}
108+
return true;
109+
},
95110
})}
96111
fullWidth
97112
name="name"

app/web/utils/validation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// taken from backend
2-
export const nameValidationPattern = /\S+/;
2+
export const nameValidationPattern = /^[\p{L}\s'-]+$/u;
33
export const usernameValidationPattern = /^[a-z][0-9a-z_]*[a-z0-9]$/i;
44
export const validatePassword = (password: string) => {
55
return password.length >= 8 && password.length < 256;

0 commit comments

Comments
 (0)