Skip to content

Commit 78a5e3c

Browse files
BC-10341 - Registration Step: password (#3955)
Adds the password step for the registration of external persons. * add password validation rules (required, min length 8; at least one: uppercase, lowercase letter, special char and number; passwords must match) * create password registration step * add validation rules for password * add password validation on continue * adjust layout for different screensizes, on display sizes <600px: password fields and first+last name are not in a row (column) * fix layout shift caused by multiline error messages * focus step headings on continue and on back button * focus first invalid inputs on continue button * fix cutoff of bullet points on IPad --------- Co-authored-by: Sebastian Mueller <[email protected]>
1 parent c617a69 commit 78a5e3c

File tree

12 files changed

+711
-24
lines changed

12 files changed

+711
-24
lines changed

src/locales/de.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default {
8888
"common.labels.partial": "teilweise",
8989
"common.labels.password.new": "Neues Passwort",
9090
"common.labels.password": "Passwort",
91+
"common.labels.password.confirmation": "Passwort wiederholen",
9192
"common.labels.readmore": "Weiterlesen",
9293
"common.labels.register": "Registrieren",
9394
"common.labels.registration": "Registrierung",
@@ -1530,6 +1531,29 @@ export default {
15301531
"pages.registrationExternalMembers.steps.declarationOfConsent.title": "Einwilligungserklärung",
15311532
"pages.registrationExternalMembers.steps.confirmationCode.title": "Bestätigungscode",
15321533
"pages.registrationExternalMembers.steps.registration.title": "Registrierung",
1534+
"pages.registrationExternalMembers.steps.password.firstParagraph":
1535+
"Die {instance} bewahrt die Nutzenden-Daten sicher auf und gibt sie nicht an Dritte weiter. Die Verarbeitung der Daten erfolgt entsprechend der hohen gesetzlichen Datenschutz-Anforderungen.",
1536+
"pages.registrationExternalMembers.steps.password.secondParagraph":
1537+
"Die folgenden Daten hat eine Lehrkraft oder ein Schul-Admin eingetragen (falls Anpassungen notwendig sind, bitte an die entsprechende Person wenden):",
1538+
"pages.registrationExternalMembers.steps.password.setPassword": "Bitte ein Passwort vergeben",
1539+
"pages.registrationExternalMembers.steps.password.instructions.minLengthWithLowerAndUpperCase":
1540+
"mindestens 8 Zeichen, einen Groß- und einen Kleinbuchstaben",
1541+
"pages.registrationExternalMembers.steps.password.instructions.numberAndSpecialCharacter":
1542+
"davon jeweils mindestens eine Zahl und ein Sonderzeichen",
1543+
"pages.registrationExternalMembers.steps.password.instructions.allowedSpecialCharacters":
1544+
"erlaube Sonderzeichen sind: ! § $ % / ( ) = ? \\ ; : , . # + * ~ -",
1545+
"pages.registrationExternalMembers.steps.password.validation.required": "Bitte ein Passwort eingeben.",
1546+
"pages.registrationExternalMembers.steps.password.validation.minLength":
1547+
"Das Passwort muss mindestens 8 Zeichen lang sein.",
1548+
"pages.registrationExternalMembers.steps.password.validation.upperCase":
1549+
"Das Passwort muss mindestens einen Großbuchstaben enthalten.",
1550+
"pages.registrationExternalMembers.steps.password.validation.lowerCase":
1551+
"Das Passwort muss mindestens einen Kleinbuchstaben enthalten.",
1552+
"pages.registrationExternalMembers.steps.password.validation.number":
1553+
"Das Passwort muss mindestens eine Zahl enthalten.",
1554+
"pages.registrationExternalMembers.steps.password.validation.specialCharacter":
1555+
"Das Passwort muss mindestens ein Sonderzeichen enthalten.",
1556+
"pages.registrationExternalMembers.steps.password.validation.passwordsMatch": "Die Passwörter stimmen nicht überein.",
15331557
"pages.registrationExternalMembers.steps.registration.heading": "Registrierung erfolgreich",
15341558
"pages.registrationExternalMembers.steps.language.heading": "Bitte Sprache wählen",
15351559
"pages.registrationExternalMembers.steps.welcome.heading":

src/locales/en.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default {
8686
"common.labels.partial": "partial",
8787
"common.labels.password.new": "New password",
8888
"common.labels.password": "Password",
89+
"common.labels.password.confirmation": "Confirm password",
8990
"common.labels.readmore": "Read more",
9091
"common.labels.register": "Register",
9192
"common.labels.registration": "Registration",
@@ -1508,6 +1509,29 @@ export default {
15081509
"pages.registrationExternalMembers.steps.declarationOfConsent.title": "Declaration of consent",
15091510
"pages.registrationExternalMembers.steps.confirmationCode.title": "Confirmation code",
15101511
"pages.registrationExternalMembers.steps.registration.title": "Registration",
1512+
"pages.registrationExternalMembers.steps.password.firstParagraph":
1513+
"The {instance} stores user data securely and does not share it with third parties. Data is processed in accordance with strict legal data protection requirements.",
1514+
"pages.registrationExternalMembers.steps.password.secondParagraph":
1515+
"The following data has been entered by a teacher or school administrator (if adjustments are necessary, please contact the relevant person):",
1516+
"pages.registrationExternalMembers.steps.password.setPassword": "Please set a password",
1517+
"pages.registrationExternalMembers.steps.password.instructions.minLengthWithLowerAndUpperCase":
1518+
"at least 8 characters, one uppercase and one lowercase letter",
1519+
"pages.registrationExternalMembers.steps.password.instructions.numberAndSpecialCharacter":
1520+
"at least one number and one special character",
1521+
"pages.registrationExternalMembers.steps.password.instructions.allowedSpecialCharacters":
1522+
"allowed special characters are: ! § $ % / ( ) = ? \\ ; : , . # + * ~ -",
1523+
"pages.registrationExternalMembers.steps.password.validation.required": "Please enter a password.",
1524+
"pages.registrationExternalMembers.steps.password.validation.minLength":
1525+
"The password must be at least 8 characters long.",
1526+
"pages.registrationExternalMembers.steps.password.validation.upperCase":
1527+
"The password must contain at least one uppercase letter.",
1528+
"pages.registrationExternalMembers.steps.password.validation.lowerCase":
1529+
"The password must contain at least one lowercase letter.",
1530+
"pages.registrationExternalMembers.steps.password.validation.number":
1531+
"The password must contain at least one number.",
1532+
"pages.registrationExternalMembers.steps.password.validation.specialCharacter":
1533+
"The password must contain at least one special character.",
1534+
"pages.registrationExternalMembers.steps.password.validation.passwordsMatch": "The passwords do not match.",
15111535
"pages.registrationExternalMembers.steps.registration.heading": "Registration successful",
15121536
"pages.registrationExternalMembers.steps.language.heading": "Please select your language",
15131537
"pages.registrationExternalMembers.steps.welcome.heading":

src/locales/es.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default {
8888
"common.labels.partial": "parcial",
8989
"common.labels.password.new": "Nueva contraseña",
9090
"common.labels.password": "Contraseña",
91+
"common.labels.password.confirmation": "Confirmar contraseña",
9192
"common.labels.readmore": "Leer más",
9293
"common.labels.register": "Registrarse",
9394
"common.labels.registration": "Registro",
@@ -1541,6 +1542,29 @@ export default {
15411542
"pages.registrationExternalMembers.steps.declarationOfConsent.title": "Declaración de consentimiento",
15421543
"pages.registrationExternalMembers.steps.confirmationCode.title": "Código de confirmación",
15431544
"pages.registrationExternalMembers.steps.registration.title": "Registro",
1545+
"pages.registrationExternalMembers.steps.password.firstParagraph":
1546+
"La {instance} almacena los datos de los usuarios de forma segura y no los comparte con terceros. El tratamiento de los datos se realiza conforme a los altos requisitos legales de protección de datos.",
1547+
"pages.registrationExternalMembers.steps.password.secondParagraph":
1548+
"Los siguientes datos han sido introducidos por un docente o un administrador escolar (si es necesario realizar ajustes, por favor, dirígete a la persona correspondiente):",
1549+
"pages.registrationExternalMembers.steps.password.setPassword": "Por favor, establece una contraseña",
1550+
"pages.registrationExternalMembers.steps.password.instructions.minLengthWithLowerAndUpperCase":
1551+
"mínimo 8 caracteres, una letra mayúscula y una minúscula",
1552+
"pages.registrationExternalMembers.steps.password.instructions.numberAndSpecialCharacter":
1553+
"debe contener al menos un número y un carácter especial",
1554+
"pages.registrationExternalMembers.steps.password.instructions.allowedSpecialCharacters":
1555+
"los caracteres especiales permitidos son: ! § $ % / ( ) = ? \\ ; : , . # + * ~ -",
1556+
"pages.registrationExternalMembers.steps.password.validation.required": "Por favor, introduce una contraseña.",
1557+
"pages.registrationExternalMembers.steps.password.validation.minLength":
1558+
"La contraseña debe tener al menos 8 caracteres.",
1559+
"pages.registrationExternalMembers.steps.password.validation.upperCase":
1560+
"La contraseña debe contener al menos una letra mayúscula.",
1561+
"pages.registrationExternalMembers.steps.password.validation.lowerCase":
1562+
"La contraseña debe contener al menos una letra minúscula.",
1563+
"pages.registrationExternalMembers.steps.password.validation.number":
1564+
"La contraseña debe contener al menos un número.",
1565+
"pages.registrationExternalMembers.steps.password.validation.specialCharacter":
1566+
"La contraseña debe contener al menos un carácter especial.",
1567+
"pages.registrationExternalMembers.steps.password.validation.passwordsMatch": "Las contraseñas no coinciden.",
15441568
"pages.registrationExternalMembers.steps.registration.heading": "Registro exitoso",
15451569
"pages.registrationExternalMembers.steps.language.heading": "Por favor seleccione su idioma",
15461570
"pages.registrationExternalMembers.steps.welcome.heading":

src/locales/uk.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default {
8888
"common.labels.partial": "частковий",
8989
"common.labels.password.new": "Новий пароль",
9090
"common.labels.password": "Пароль",
91+
"common.labels.password.confirmation": "Підтвердження пароля",
9192
"common.labels.readmore": "Додаткові відомості",
9293
"common.labels.register": "Реєстрація",
9394
"common.labels.registration": "Реєстрація",
@@ -1517,6 +1518,28 @@ export default {
15171518
"pages.registrationExternalMembers.steps.declarationOfConsent.title": "Декларація про згоду",
15181519
"pages.registrationExternalMembers.steps.confirmationCode.title": "Код підтвердження",
15191520
"pages.registrationExternalMembers.steps.registration.title": "Реєстрація",
1521+
"pages.registrationExternalMembers.steps.password.firstParagraph":
1522+
"{instance} надійно зберігає дані користувачів і не передає їх третім особам. Обробка даних здійснюється відповідно до високих законодавчих вимог щодо захисту даних.",
1523+
"pages.registrationExternalMembers.steps.password.secondParagraph":
1524+
"Наступні дані були введені вчителем або адміністратором школи (якщо потрібні зміни, будь ласка, зверніться до відповідної особи):",
1525+
"pages.registrationExternalMembers.steps.password.setPassword": "Будь ласка, задайте пароль",
1526+
"pages.registrationExternalMembers.steps.password.instructions.minLengthWithLowerAndUpperCase":
1527+
"мінімум 8 символів, одна велика та одна мала буква",
1528+
"pages.registrationExternalMembers.steps.password.instructions.numberAndSpecialCharacter":
1529+
"мінімум одна цифра та один спеціальний символ",
1530+
"pages.registrationExternalMembers.steps.password.instructions.allowedSpecialCharacters":
1531+
"дозволені спеціальні символи: ! § $ % / ( ) = ? \\ ; : , . # + * ~ -",
1532+
"pages.registrationExternalMembers.steps.password.validation.required": "Будь ласка, введіть пароль.",
1533+
"pages.registrationExternalMembers.steps.password.validation.minLength":
1534+
"Пароль повинен містити щонайменше 8 символів.",
1535+
"pages.registrationExternalMembers.steps.password.validation.upperCase":
1536+
"Пароль повинен містити щонайменше одну велику літеру.",
1537+
"pages.registrationExternalMembers.steps.password.validation.lowerCase":
1538+
"Пароль повинен містити щонайменше одну малу літеру.",
1539+
"pages.registrationExternalMembers.steps.password.validation.number": "Пароль повинен містити щонайменше одну цифру.",
1540+
"pages.registrationExternalMembers.steps.password.validation.specialCharacter":
1541+
"Пароль повинен містити щонайменше один спеціальний символ.",
1542+
"pages.registrationExternalMembers.steps.password.validation.passwordsMatch": "Паролі не співпадають.",
15201543
"pages.registrationExternalMembers.steps.registration.heading": "Реєстрація успішна",
15211544
"pages.registrationExternalMembers.steps.language.heading": "Будь ласка, виберіть свою мову",
15221545
"pages.registrationExternalMembers.steps.welcome.heading":

src/modules/data/room/registration/registration.composable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useI18n } from "vue-i18n";
55
export const useRegistration = () => {
66
const i18n = useI18n();
77
const selectedLanguage = ref<LanguageType | undefined>(undefined);
8+
const password = ref<string>("");
89

910
const initializeLanguage = () => {
1011
const match = document.cookie.match(/(?:^|;\s*)USER_LANG=([^;]*)/);
@@ -26,5 +27,5 @@ export const useRegistration = () => {
2627
i18n.locale.value = value;
2728
};
2829

29-
return { selectedLanguage, setCookie, setSelectedLanguage, initializeLanguage };
30+
return { selectedLanguage, setCookie, setSelectedLanguage, initializeLanguage, password };
3031
};

src/modules/feature/room/registration/Registration.unit.ts

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import Registration from "./Registration.vue";
2+
import LanguageSelection from "./steps/LanguageSelection.vue";
3+
import Password from "./steps/Password.vue";
24
import { LanguageType } from "@/serverApi/v3";
5+
import { createTestEnvStore } from "@@/tests/test-utils";
36
import { createTestingVuetify } from "@@/tests/test-utils/setup";
47
import { useRegistration } from "@data-room";
8+
import { createTestingPinia } from "@pinia/testing";
9+
import { flushPromises } from "@vue/test-utils";
10+
import { setActivePinia } from "pinia";
511
import { nextTick, ref } from "vue";
12+
import { VStepper, VStepperItem } from "vuetify/components";
613

714
vi.mock("vue-i18n", () => ({
815
useI18n: () => ({
@@ -14,17 +21,24 @@ vi.mock("@data-room/registration/registration.composable");
1421
const useRegistrationMock = vi.mocked(useRegistration);
1522

1623
describe("Registration.vue", () => {
24+
beforeEach(() => {
25+
setActivePinia(createTestingPinia());
26+
createTestEnvStore();
27+
});
1728
const setup = () => {
1829
useRegistrationMock.mockReturnValue({
1930
selectedLanguage: ref(LanguageType.De),
31+
password: ref(""),
2032
setCookie: vi.fn(),
2133
setSelectedLanguage: vi.fn(),
2234
initializeLanguage: vi.fn(),
2335
});
2436

2537
const wrapper = mount(Registration, {
38+
attachTo: document.body,
2639
global: {
2740
plugins: [createTestingVuetify()],
41+
stubs: { Welcome: true },
2842
},
2943
});
3044

@@ -41,15 +55,15 @@ describe("Registration.vue", () => {
4155
describe("Stepper Component", () => {
4256
it("should have stepper component", () => {
4357
const { wrapper } = setup();
44-
const stepperComponent = wrapper.findComponent({ name: "VStepper" });
58+
const stepperComponent = wrapper.findComponent(VStepper);
4559
expect(stepperComponent.exists()).toBe(true);
4660
expect(stepperComponent.props("modelValue")).toBe(1);
4761
});
4862

4963
it("should have 6 steps", () => {
5064
const { wrapper } = setup();
5165

52-
const stepComponents = wrapper.findAllComponents({ name: "VStepperItem" });
66+
const stepComponents = wrapper.findAllComponents(VStepperItem);
5367
expect(stepComponents.length).toBe(6);
5468
});
5569

@@ -63,8 +77,9 @@ describe("Registration.vue", () => {
6377
describe("when step is 1", () => {
6478
it("should have only 'Continue' button", () => {
6579
const { wrapper } = setup();
80+
6681
const backButton = wrapper.find("[data-testid='registration-back-button']");
67-
const nextButton = wrapper.find("[data-testid='registiration-continue-button']");
82+
const nextButton = wrapper.find("[data-testid='registration-continue-button']");
6883

6984
expect(backButton.exists()).toBe(false);
7085
expect(nextButton.exists()).toBe(true);
@@ -74,7 +89,7 @@ describe("Registration.vue", () => {
7489
describe("when step is greater than 1", () => {
7590
it("should have 'Back' and 'Continue' buttons", async () => {
7691
const { wrapper } = setup();
77-
const nextButton = wrapper.find("[data-testid='registiration-continue-button']");
92+
const nextButton = wrapper.find("[data-testid='registration-continue-button']");
7893

7994
await nextButton.trigger("click");
8095

@@ -83,12 +98,61 @@ describe("Registration.vue", () => {
8398
expect(nextButton.exists()).toBe(true);
8499
});
85100
});
101+
102+
describe("back button", () => {
103+
it("should go to previous step on click", async () => {
104+
const { wrapper } = setup();
105+
const stepper = wrapper.findComponent(VStepper);
106+
await stepper.setValue(2);
107+
expect(stepper.props("modelValue")).toBe(2);
108+
109+
const backButton = wrapper.get("[data-testid='registration-back-button']");
110+
await backButton.trigger("click");
111+
expect(stepper.props("modelValue")).toBe(1);
112+
});
113+
114+
it("should focus heading of previous step on click", async () => {
115+
const { wrapper } = setup();
116+
const stepper = wrapper.findComponent(VStepper);
117+
await stepper.setValue(2);
118+
119+
const backButton = wrapper.get("[data-testid='registration-back-button']");
120+
await backButton.trigger("click");
121+
122+
const welcomeStepHeading = wrapper.get("#step-heading-language");
123+
expect(document.activeElement).toEqual(welcomeStepHeading.element);
124+
});
125+
});
126+
127+
describe("continue button", () => {
128+
it("should go to next step on click", async () => {
129+
const { wrapper } = setup();
130+
const stepper = wrapper.findComponent(VStepper);
131+
const continueButton = wrapper.get("[data-testid='registration-continue-button']");
132+
133+
expect(stepper.props("modelValue")).toBe(1);
134+
135+
await continueButton.trigger("click");
136+
expect(stepper.props("modelValue")).toBe(2);
137+
});
138+
139+
it("should focus heading of next step on click", async () => {
140+
const { wrapper } = setup();
141+
const continueButton = wrapper.get("[data-testid='registration-continue-button']");
142+
143+
await continueButton.trigger("click");
144+
await nextTick();
145+
146+
const welcomeStepHeading = wrapper.get("#step-heading-welcome");
147+
expect(document.activeElement).toEqual(welcomeStepHeading.element);
148+
});
149+
});
86150
});
87151

88152
describe("Language Selection Component", () => {
89153
it("should call setSelectedLanguage when language is updated", async () => {
90154
const { wrapper, useRegistration } = setup();
91-
const languageSelectionComponent = wrapper.findComponent({ name: "LanguageSelection" });
155+
const languageSelectionComponent = wrapper.getComponent(LanguageSelection);
92156

93157
languageSelectionComponent.vm.$emit("update:selected-language", LanguageType.En);
94158
await nextTick();
@@ -99,10 +163,35 @@ describe("Registration.vue", () => {
99163
it("should have default language as German even if the selectedLanguage's value is undefined", () => {
100164
const { wrapper, useRegistration } = setup();
101165
useRegistration.selectedLanguage.value = undefined;
102-
const languageSelectionComponent = wrapper.findComponent({ name: "LanguageSelection" });
166+
const languageSelectionComponent = wrapper.getComponent(LanguageSelection);
103167

104168
expect(languageSelectionComponent.props("selectedLanguage")).toBe(LanguageType.De);
105169
});
106170
});
171+
172+
describe("Password Step", () => {
173+
it("should render Password component at password step (3)", async () => {
174+
const { wrapper } = setup();
175+
const stepper = wrapper.findComponent(VStepper);
176+
await stepper.setValue(3);
177+
178+
const passwordComponent = wrapper.findComponent(Password);
179+
expect(passwordComponent.exists()).toBe(true);
180+
});
181+
182+
it("should focus first invalid field on continue button click", async () => {
183+
const { wrapper } = setup();
184+
// we need to step twice here to reach the password step as just setValue() will not initiliaze the step forms array properly
185+
const continueButton = wrapper.get("[data-testid='registration-continue-button']");
186+
await continueButton.trigger("click");
187+
await continueButton.trigger("click");
188+
189+
await continueButton.trigger("click");
190+
await flushPromises();
191+
192+
const passwordField = wrapper.get("[data-testid='password']").get("input");
193+
expect(document.activeElement).toBe(passwordField.element);
194+
});
195+
});
107196
});
108197
});

0 commit comments

Comments
 (0)