Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/web/features/auth/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@
"name": {
"field_label": "Name",
"empty_error": "Please enter your name",
"required_error": "This field can't be empty."
"required_error": "This field can't be empty",
"invalid_characters_error": "Name can only contain letters, numbers, spaces, hyphens, and apostrophes",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"invalid_characters_error": "Name can only contain letters, numbers, spaces, hyphens, and apostrophes",
"invalid_characters_error": "Name can only contain letters, spaces, hyphens, and apostrophes",

Let's do this and remove the numbers!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nabramow thanks for reviewing! Just to be clear, you're saying we should remove 'numbers, ' from the string here and continue to allow them in the validation?
or remove the numbers from the validation (but then i guess keep the string here as it) 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndyMcGN Also remove them from the allowed characters in the validation!

We should probably also do that on backend too. Not sure if you're able to do backend? If not I will make a ticket for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, will take a look at the backend now

"min_length_error": "Name must be at least 2 characters",
"max_length_error": "Name must be no more than 100 characters"
},
"email": {
"field_label": "Email",
Expand Down
207 changes: 207 additions & 0 deletions app/web/features/auth/signup/BasicForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,211 @@ describe("basic signup form", () => {
mockConsoleError();
await assertErrorAlert("Permission denied");
});

describe("name validation", () => {
it("rejects names shorter than 2 characters", async () => {
render(<BasicForm />, { wrapper });

const user = userEvent.setup();
const nameInput = await screen.findByLabelText(
t("auth:basic_form.name.field_label"),
);

await user.type(nameInput, "A");
await user.tab(); // Trigger blur to validate

await waitFor(() => {
expect(
screen.getByText(t("auth:basic_form.name.min_length_error")),
).toBeInTheDocument();
});

await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"test@example.com",
);
await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).not.toHaveBeenCalled();
});
});

it("rejects names longer than 100 characters", async () => {
render(<BasicForm />, { wrapper });

const user = userEvent.setup();
const nameInput = await screen.findByLabelText(
t("auth:basic_form.name.field_label"),
);

const longName = "A".repeat(101);
await user.type(nameInput, longName);
await user.tab(); // Trigger blur to validate

await waitFor(() => {
expect(
screen.getByText(t("auth:basic_form.name.max_length_error")),
).toBeInTheDocument();
});

await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"test@example.com",
);
await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).not.toHaveBeenCalled();
});
});

it("rejects names with invalid characters like !@#$", async () => {
render(<BasicForm />, { wrapper });

const user = userEvent.setup();
const nameInput = await screen.findByLabelText(
t("auth:basic_form.name.field_label"),
);

await user.type(nameInput, "John!@#$");
await user.tab(); // Trigger blur to validate

await waitFor(() => {
expect(
screen.getByText(t("auth:basic_form.name.invalid_characters_error")),
).toBeInTheDocument();
});

await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"test@example.com",
);
await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).not.toHaveBeenCalled();
});
});

it("accepts names with hyphens and apostrophes", async () => {
startSignupMock.mockResolvedValue(stateAfterStart);
render(<BasicForm />, { wrapper });

const user = userEvent.setup();

await user.type(
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
"Anne-Marie O'Connor",
);
await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"anne@example.com",
);

await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).toHaveBeenCalledWith(
"Anne-Marie O'Connor",
"anne@example.com",
undefined,
);
});
});

it("accepts names with Unicode characters (Cyrillic)", async () => {
startSignupMock.mockResolvedValue(stateAfterStart);
render(<BasicForm />, { wrapper });

const user = userEvent.setup();

await user.type(
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
"Иван Иванов",
);
await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"ivan@example.com",
);

await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).toHaveBeenCalledWith(
"Иван Иванов",
"ivan@example.com",
undefined,
);
});
});

it("accepts names at minimum length (2 characters)", async () => {
startSignupMock.mockResolvedValue(stateAfterStart);
render(<BasicForm />, { wrapper });

const user = userEvent.setup();

await user.type(
await screen.findByLabelText(t("auth:basic_form.name.field_label")),
"Li",
);
await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"li@example.com",
);

await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).toHaveBeenCalledWith(
"Li",
"li@example.com",
undefined,
);
});
});

it("rejects names with only leading/trailing spaces that result in less than 2 characters", async () => {
render(<BasicForm />, { wrapper });

const user = userEvent.setup();
const nameInput = await screen.findByLabelText(
t("auth:basic_form.name.field_label"),
);

await user.type(nameInput, " A ");
await user.tab(); // Trigger blur to validate

await waitFor(() => {
expect(
screen.getByText(t("auth:basic_form.name.min_length_error")),
).toBeInTheDocument();
});

await user.type(
await screen.findByLabelText(t("auth:basic_form.email.field_label")),
"test@example.com",
);
await user.click(
await screen.findByRole("button", { name: t("global:continue") }),
);

await waitFor(() => {
expect(startSignupMock).not.toHaveBeenCalled();
});
});
});
});
19 changes: 17 additions & 2 deletions app/web/features/auth/signup/BasicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,26 @@ export default function BasicForm({
<StyledTextField
id="name"
{...register("name", {
required: t("auth:basic_form.name.required_error"),
minLength: {
value: 2,
message: t("auth:basic_form.name.min_length_error"),
},
maxLength: {
value: 100,
message: t("auth:basic_form.name.max_length_error"),
},
pattern: {
message: t("auth:basic_form.name.empty_error"),
message: t("auth:basic_form.name.invalid_characters_error"),
value: nameValidationPattern,
},
required: t("auth:basic_form.name.required_error"),
validate: (value) => {
const trimmed = value.trim();
if (trimmed.length < 2) {
return t("auth:basic_form.name.min_length_error");
}
return true;
},
})}
fullWidth
name="name"
Expand Down
2 changes: 1 addition & 1 deletion app/web/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// taken from backend
export const nameValidationPattern = /\S+/;
export const nameValidationPattern = /^[\p{L}\p{N}\s'-]+$/u;
export const usernameValidationPattern = /^[a-z][0-9a-z_]*[a-z0-9]$/i;
export const validatePassword = (password: string) => {
return password.length >= 8 && password.length < 256;
Expand Down