diff --git a/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json b/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json index 04fe4cb0136..987c2d9ed13 100644 --- a/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json +++ b/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json @@ -5,6 +5,10 @@ "label": "Title (optional)", "placeholder": "Title" }, + "titleCodeOnOTPForm": { + "label": "Title", + "placeholder": "Select Title" + }, "firstName": { "label": "First name", "placeholder": "First name" @@ -25,38 +29,71 @@ "label": "City/Town (optional)", "placeholder": "Please select City/Town" }, + "cityOnOTPForm": { + "label": "City/Town", + "placeholder": "Please select City/Town" + }, "country": { "label": "Country (optional)", "placeholder": "Select Country" }, + "countryOnOTPForm": { + "label": "Country", + "placeholder": "Select Country" + }, "state": { "label": "State/Province (optional)", "placeholder": "Select State/Province" }, + "stateOnOTPForm": { + "label": "State/Province", + "placeholder": "Select State/Province" + }, "postalCode": { "label": "Zip/Postal code (optional)", "placeholder": "Zip/Postal code" }, + "postalCodeOnOTPForm": { + "label": "Zip/Postal code", + "placeholder": "Zip/Postal code" + }, "addressLine": { "label": "Address (optional)", "placeholder": "Address" }, + "addressLineOnOTPForm": { + "label": "Address", + "placeholder": "Address" + }, "secondAddressLine": { "label": "Address line 2 (optional)", "placeholder": "Address line 2" }, + "secondAddressLineOnOTPForm": { + "label": "Address line 2", + "placeholder": "Address line 2" + }, "phoneNumber": { "label": "Phone number (optional)", "placeholder": "Phone number" }, + "phoneNumberOnOTPForm": { + "label": "Phone number", + "placeholder": "Phone number" + }, "message": { "label": "Message (optional)", "placeholder": "An example data for the message field: \"Department: Ground support; Position: Chief safe guard; Report to: Steve Jackson; Comments: Please create new account for me\"." + }, + "messageOnOTPForm": { + "label": "Message", + "placeholder": "An example data for the message field: \"Department: Ground support; Position: Chief safe guard; Report to: Steve Jackson; Comments: Please create new account for me\"." } }, "messageToApproverTemplate": "Company name: {{companyName}},\n Phone number: {{phoneNumber}},\n Address: {{addressLine}} {{secondAddressLine}} {{city}} {{state}} {{postalCode}} {{country}},\n Message: {{message}}", "successFormSubmitMessage": "Thank you for registering! A representative will contact you shortly and confirm your access information.", "formSubmitButtonLabel": "Register", + "continueWithOTP": "Continue", "goToLoginButtonLabel": "Already registered? Go to Sign in", "httpHandlers": { "conflict": "User with this e-mail address already exists." diff --git a/feature-libs/organization/user-registration/components/public_api.ts b/feature-libs/organization/user-registration/components/public_api.ts index 6ba919f1311..ac0b9ae58ad 100644 --- a/feature-libs/organization/user-registration/components/public_api.ts +++ b/feature-libs/organization/user-registration/components/public_api.ts @@ -6,3 +6,5 @@ export * from './user-registration-components.module'; export * from './form/index'; +export * from './registration-otp-form/index'; +export * from './verification-token-form/index'; diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/index.ts b/feature-libs/organization/user-registration/components/registration-otp-form/index.ts new file mode 100644 index 00000000000..7aceb71b2fd --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './user-registration-otp-form.component'; +export * from './user-registration-otp-form.module'; diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html new file mode 100644 index 00000000000..d74d1ba71c9 --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html @@ -0,0 +1,422 @@ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts new file mode 100644 index 00000000000..b0264423838 --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts @@ -0,0 +1,266 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Observable, of } from 'rxjs'; +import { + Country, + GlobalMessageService, + GlobalMessageType, + I18nTestingModule, + Region, + RoutingService, + Title, + Translatable, +} from '@spartacus/core'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; +import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component'; +import { UserRegistrationFormService } from '../form'; +import createSpy = jasmine.createSpy; +import { Pipe, PipeTransform } from '@angular/core'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { + FormErrorsModule, + NgSelectA11yDirective, + SpinnerComponent, +} from '@spartacus/storefront'; +import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; + +class MockRoutingService implements Partial { + go = () => Promise.resolve(true); +} + +class MockGlobalMessageService implements Partial { + add(_: string | Translatable, __: GlobalMessageType, ___?: number): void {} +} + +class MockVerificationTokenFacade implements Partial { + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); +} + +const mockForm: FormGroup = new FormGroup({ + titleCode: new FormControl(), + firstName: new FormControl(), + lastName: new FormControl(), + companyName: new FormControl(), + email: new FormControl(), + country: new FormGroup({ + isocode: new FormControl(), + }), + region: new FormGroup({ + isocode: new FormControl(), + }), + town: new FormControl(), + line1: new FormControl(), + line2: new FormControl(), + postalCode: new FormControl(), + phoneNumber: new FormControl(), + message: new FormControl(), +}); + +const mockRegions: Region[] = [ + { + isocode: 'CA-ON', + name: 'Ontario', + }, + { + isocode: 'CA-QC', + name: 'Quebec', + }, +]; + +const mockTitles: Title[] = [ + { + code: '0002', + name: 'Mr.', + }, + { + code: '0001', + name: 'Mrs.', + }, +]; + +const mockCountries: Country[] = [ + { + isocode: 'CA', + name: 'Canada', + }, + { + isocode: 'PL', + name: 'Poland', + }, +]; + +class MockUserRegistrationFormService + implements Partial +{ + getTitles(): Observable { + return of(mockTitles); + } + + getCountries(): Observable { + return of(mockCountries); + } + + getRegions(): Observable { + return of(mockRegions); + } + + get form(): FormGroup { + return mockForm; + } +} + +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} +describe('UserRegistrationOTPFormComponent', () => { + let component: UserRegistrationOTPFormComponent; + let fixture: ComponentFixture; + let verificationTokenFacade: VerificationTokenFacade; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + NgSelectModule, + FormErrorsModule, + ], + declarations: [ + UserRegistrationOTPFormComponent, + MockUrlPipe, + NgSelectA11yDirective, + SpinnerComponent, + MockFeatureDirective, + ], + providers: [ + { provide: RoutingService, useClass: MockRoutingService }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { + provide: VerificationTokenFacade, + useClass: MockVerificationTokenFacade, + }, + { + provide: UserRegistrationFormService, + useClass: MockUserRegistrationFormService, + }, + ], + }); + verificationTokenFacade = TestBed.inject(VerificationTokenFacade); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserRegistrationOTPFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form', () => { + expect(component.registerForm).toBeTruthy(); + expect(component.registerForm.get('email')).toBeTruthy(); + }); + + it('should not submit if form is invalid', () => { + component.registerForm.setValue({ + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + email: '', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }); + component.onSubmit(); + expect( + verificationTokenFacade.createVerificationToken + ).not.toHaveBeenCalled(); + }); + + it('should submit form when valid', () => { + const verificationData = { + loginId: 'test@example.com', + purpose: 'REGISTRATION', + }; + component.registerForm.setValue({ + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + email: 'test@example.com', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }); + component.onSubmit(); + expect( + verificationTokenFacade.createVerificationToken + ).toHaveBeenCalledWith(verificationData); + }); + + it('should mark all fields as touched if form is invalid', () => { + spyOn(component.registerForm, 'markAllAsTouched').and.callThrough(); + component.registerForm.patchValue({ + email: '', + }); + component.onSubmit(); + expect(component.registerForm.markAllAsTouched).toHaveBeenCalled(); + expect( + verificationTokenFacade.createVerificationToken + ).not.toHaveBeenCalled(); + }); + + it('should navigate to verifyTokenRegister route', () => { + const routingService = TestBed.inject(RoutingService); + spyOn(routingService, 'go').and.callThrough(); + const verificationToken = { tokenId: 'testToken', expiresIn: '300' }; + const formData = { + email: 'test@example.com', + titleCode: '0001', + firstName: 'John', + lastName: 'Doe', + companyName: 'Company', + country: { isocode: 'CA' }, + region: { isocode: 'CA-ON' }, + town: 'Townsville', + line1: '123 Main St', + line2: '', + postalCode: '12345', + phoneNumber: '1234567890', + message: '', + }; + component.registerForm.setValue(formData); + component['goToVerificationTokenForm'](verificationToken); + expect(routingService.go).toHaveBeenCalledWith( + { cxRoute: 'verifyTokenForRegistration' }, + { + state: { + registrationDataForm: formData, + loginId: 'test@example.com', + tokenId: 'testToken', + expiresIn: '300', + }, + } + ); + }); +}); diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts new file mode 100644 index 00000000000..0cfbe6fb938 --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { + Country, + GlobalMessageService, + Region, + RoutingService, + WindowRef, +} from '@spartacus/core'; +import { Title } from '@spartacus/user/profile/root'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { + VerificationToken, + VerificationTokenCreation, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants'; +import { UserRegistrationFormService } from '../form'; + +@Component({ + selector: 'cx-user-registration-form', + templateUrl: './user-registration-otp-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserRegistrationOTPFormComponent { + protected routingService = inject(RoutingService); + protected verificationTokenFacade = inject(VerificationTokenFacade); + protected winRef = inject(WindowRef); + protected globalMessageService = inject(GlobalMessageService, { + optional: true, + }); + protected userRegistrationFormService = inject(UserRegistrationFormService); + protected busy$ = new BehaviorSubject(false); + titles$: Observable = this.userRegistrationFormService.getTitles(); + + countries$: Observable = + this.userRegistrationFormService.getCountries(); + + regions$: Observable = + this.userRegistrationFormService.getRegions(); + + registerForm: FormGroup = this.userRegistrationFormService.form; + + isLoading$ = new BehaviorSubject(false); + + onSubmit(): void { + if (!this.registerForm.valid) { + this.registerForm.markAllAsTouched(); + return; + } + + this.busy$.next(true); + const tokenCreationReqBody = this.collectDataFromRegistrationForm(); + this.verificationTokenFacade + .createVerificationToken(tokenCreationReqBody) + .subscribe({ + next: (result: VerificationToken) => + this.goToVerificationTokenForm(result), + error: () => this.busy$.next(false), + complete: () => this.onCreateVerificationTokenComplete(), + }); + } + + protected goToVerificationTokenForm( + verificationToken: VerificationToken + ): void { + this.routingService.go( + { + cxRoute: 'verifyTokenForRegistration', + }, + { + state: { + registrationDataForm: this.registerForm.value, + loginId: this.registerForm.value.email.toLowerCase(), + tokenId: verificationToken.tokenId, + expiresIn: verificationToken.expiresIn, + }, + } + ); + } + + protected collectDataFromRegistrationForm(): VerificationTokenCreation { + return { + loginId: this.registerForm.value.email.toLowerCase(), + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }; + } + + protected onCreateVerificationTokenComplete(): void { + this.registerForm.reset(); + this.busy$.next(false); + } +} diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts new file mode 100644 index 00000000000..c141bc08fcc --- /dev/null +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { + CmsConfig, + ConfigModule, + FeaturesConfigModule, + I18nModule, + NotAuthGuard, + UrlModule, +} from '@spartacus/core'; +import { + FormErrorsModule, + NgSelectA11yModule, + SpinnerModule, +} from '@spartacus/storefront'; +import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule, + UrlModule, + I18nModule, + SpinnerModule, + FormErrorsModule, + NgSelectModule, + NgSelectA11yModule, + ConfigModule.withConfig({ + cmsComponents: { + RegisterB2BCustomerWithOTPComponent: { + component: UserRegistrationOTPFormComponent, + guards: [NotAuthGuard], + }, + }, + }), + FeaturesConfigModule, + ], + declarations: [UserRegistrationOTPFormComponent], + exports: [UserRegistrationOTPFormComponent], +}) +export class UserRegistrationOTPFormModule {} diff --git a/feature-libs/organization/user-registration/components/user-registration-components.module.ts b/feature-libs/organization/user-registration/components/user-registration-components.module.ts index 9bb33ae90c8..6ec8b416649 100644 --- a/feature-libs/organization/user-registration/components/user-registration-components.module.ts +++ b/feature-libs/organization/user-registration/components/user-registration-components.module.ts @@ -7,8 +7,15 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { UserRegistrationFormModule } from './form/user-registration-form.module'; +import { UserRegistrationOTPFormModule } from './registration-otp-form/user-registration-otp-form.module'; +import { RegisterVerificationTokenFormModule } from './verification-token-form'; @NgModule({ - imports: [RouterModule, UserRegistrationFormModule], + imports: [ + RouterModule, + UserRegistrationFormModule, + UserRegistrationOTPFormModule, + RegisterVerificationTokenFormModule, + ], }) export class UserRegistrationComponentsModule {} diff --git a/feature-libs/organization/user-registration/components/user-registration-constants.ts b/feature-libs/organization/user-registration/components/user-registration-constants.ts new file mode 100644 index 00000000000..ce0691639d0 --- /dev/null +++ b/feature-libs/organization/user-registration/components/user-registration-constants.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ONE_TIME_PASSWORD_REGISTRATION_PURPOSE = 'REGISTRATION'; diff --git a/feature-libs/organization/user-registration/components/verification-token-form/index.ts b/feature-libs/organization/user-registration/components/verification-token-form/index.ts new file mode 100644 index 00000000000..95c06e44791 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './verification-token-form-component.service'; +export * from './verification-token-form.component'; +export * from './verification-token-form.module'; diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts new file mode 100644 index 00000000000..f0ceddfd2d3 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -0,0 +1,183 @@ +import { TestBed } from '@angular/core/testing'; +import { FormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + AuthConfigService, + GlobalMessageService, + OAuthFlow, + RoutingService, + TranslationService, + GlobalMessageType, +} from '@spartacus/core'; +import { + OrganizationUserRegistration, + UserRegistrationFacade, +} from '@spartacus/organization/user-registration/root'; +import { of } from 'rxjs'; +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; + +class MockGlobalMessageService implements Partial { + add() {} +} + +class MockOrganizationUserRegistrationFacade + implements Partial +{ + registerUser(userData: OrganizationUserRegistration) { + return of(userData); + } +} + +class MockRoutingService implements Partial { + go = () => Promise.resolve(true); +} + +class MockTranslationService implements Partial { + translate(value: string, options: any) { + return of(value + Object.values(options)); + } +} + +class MockAuthConfigService implements Partial { + getOAuthFlow() { + return OAuthFlow.ResourceOwnerPasswordFlow; + } +} + +describe('RegisterVerificationTokenFormComponentService', () => { + let service: RegisterVerificationTokenFormComponentService; + let routingService: RoutingService; + let globalMessageService: GlobalMessageService; + let userRegistrationFacade: UserRegistrationFacade; + let translationService: TranslationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + FormBuilder, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + { + provide: GlobalMessageService, + useClass: MockGlobalMessageService, + }, + { + provide: AuthConfigService, + useClass: MockAuthConfigService, + }, + { + provide: TranslationService, + useClass: MockTranslationService, + }, + { + provide: UserRegistrationFacade, + useClass: MockOrganizationUserRegistrationFacade, + }, + ], + }); + service = TestBed.inject(RegisterVerificationTokenFormComponentService); + routingService = TestBed.inject(RoutingService); + globalMessageService = TestBed.inject(GlobalMessageService); + userRegistrationFacade = TestBed.inject(UserRegistrationFacade); + translationService = TestBed.inject(TranslationService); + }); + + it('should inject service', () => { + expect(service).toBeTruthy(); + }); + + it('should get the form', () => { + expect(service.form).toBeInstanceOf(UntypedFormGroup); + }); + + it('should redirect to login page', () => { + spyOn(routingService, 'go').and.callThrough(); + + service.registerUser(service.form.value).subscribe().unsubscribe(); + + expect(routingService.go).toHaveBeenCalledWith({ + cxRoute: 'login', + }); + }); + + it('should display a success message after registration', () => { + spyOn(globalMessageService, 'add'); + + service.registerUser(service.form.value).subscribe().unsubscribe(); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { + key: 'userRegistrationForm.successFormSubmitMessage', + params: {}, + }, + GlobalMessageType.MSG_TYPE_CONFIRMATION, + 10000 + ); + }); + + it('should call buildMessageContent and translate correctly', () => { + spyOn(translationService, 'translate').and.callThrough(); + + const formValue = { + phoneNumber: '123456789', + line1: 'Address Line 1', + line2: 'Address Line 2', + city: 'City', + region: { isocode: 'RegionCode' }, + postalCode: '12345', + country: { isocode: 'CountryCode' }, + companyName: 'Company', + message: 'Message content', + }; + + const formValueAfterBind = { + phoneNumber: '123456789', + addressLine: 'Address Line 1', + secondAddressLine: 'Address Line 2', + city: 'City', + state: 'RegionCode', + postalCode: '12345', + country: 'CountryCode', + companyName: 'Company', + message: 'Message content', + }; + + service['buildMessageContent'](formValue).subscribe((result) => { + expect(translationService.translate).toHaveBeenCalledWith( + 'userRegistrationForm.messageToApproverTemplate', + formValueAfterBind + ); + expect(result).toContain('123456789'); + }); + }); + + it('should call registerUser with correct data', () => { + spyOn(userRegistrationFacade, 'registerUser').and.callThrough(); + + service.form.setValue({ + tokenId: 'testTokenId', + tokenCode: 'testTokenCode', + }); + + const formValue = { + titleCode: 'mr', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phoneNumber: '123456789', + addressLine: 'Address Line 1', + secondAddressLine: 'Address Line 2', + city: 'City', + region: { isocode: 'RegionCode' }, + postalCode: '12345', + country: { isocode: 'CountryCode' }, + companyName: 'Company', + message: 'Message content', + }; + + service.registerUser(formValue).subscribe(); + + expect(userRegistrationFacade.registerUser).toHaveBeenCalled(); + }); +}); diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts new file mode 100644 index 00000000000..b39bc0283b5 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { inject, Injectable } from '@angular/core'; +import { + FormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + AuthConfigService, + GlobalMessageService, + GlobalMessageType, + OAuthFlow, + RoutingService, + TranslationService, +} from '@spartacus/core'; +import { + OrganizationUserRegistration, + UserRegistrationFacade, +} from '@spartacus/organization/user-registration/root'; +import { Observable } from 'rxjs'; +import { switchMap, take, tap } from 'rxjs/operators'; + +const globalMsgShowTime: number = 10000; +@Injectable({ + providedIn: 'root', +}) +export class RegisterVerificationTokenFormComponentService { + protected globalMessage: GlobalMessageService = inject(GlobalMessageService); + protected formBuilder = inject(FormBuilder); + protected organizationUserRegistrationFacade: UserRegistrationFacade = inject( + UserRegistrationFacade + ); + protected translationService: TranslationService = inject(TranslationService); + protected authConfigService: AuthConfigService = inject(AuthConfigService); + protected routingService: RoutingService = inject(RoutingService); + + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl('', [Validators.required]), + tokenCode: new UntypedFormControl('', [Validators.required]), + }); + + protected buildMessageContent(formValue: { + [key: string]: any; + }): Observable { + return this.translationService.translate( + 'userRegistrationForm.messageToApproverTemplate', + { + phoneNumber: formValue.phoneNumber, + addressLine: formValue.line1, + secondAddressLine: formValue.line2, + city: formValue.city, + state: formValue.region?.isocode, + postalCode: formValue.postalCode, + country: formValue.country?.isocode, + companyName: formValue.companyName, + message: formValue.message, + } + ); + } + + /** + * Redirects the user back to the login page. + * + * This only happens in case of the `ResourceOwnerPasswordFlow` OAuth flow. + */ + protected redirectToLogin(): void { + if ( + this.authConfigService.getOAuthFlow() === + OAuthFlow.ResourceOwnerPasswordFlow + ) { + this.routingService.go({ cxRoute: 'login' }); + } + } + + displayMessage(key: string, params: Object) { + this.globalMessage.add( + { + key: key, + params, + }, + GlobalMessageType.MSG_TYPE_CONFIRMATION, + globalMsgShowTime + ); + } + + registerUser(formValue: { + [key: string]: any; + }): Observable { + return this.buildMessageContent(formValue).pipe( + take(1), + switchMap((message: string) => + this.organizationUserRegistrationFacade.registerUser({ + titleCode: formValue.titleCode, + firstName: formValue.firstName, + lastName: formValue.lastName, + email: formValue.email, + message: message, + verificationTokenId: this.form.get('tokenId')?.value, + verificationTokenCode: this.form.get('tokenCode')?.value, + }) + ), + tap(() => { + this.displayMessage( + 'userRegistrationForm.successFormSubmitMessage', + {} + ); + this.redirectToLogin(); + }) + ); + } +} diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html new file mode 100644 index 00000000000..3931ac8ac86 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html @@ -0,0 +1,88 @@ + + +
+ + + {{ 'verificationTokenForm.tokenInputHint' | cxTranslate }} + + + + + + +
+ + +
+
+ + + + diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts new file mode 100644 index 00000000000..34ba0dc369e --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts @@ -0,0 +1,241 @@ +import { + ChangeDetectorRef, + DebugElement, + Pipe, + PipeTransform, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { I18nTestingModule, RoutingService } from '@spartacus/core'; +import { + FormErrorsModule, + LaunchDialogService, + SpinnerModule, +} from '@spartacus/storefront'; +import { BehaviorSubject, of } from 'rxjs'; +import createSpy = jasmine.createSpy; +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { RegisterVerificationTokenFormComponent } from './verification-token-form.component'; +import { Store } from '@ngrx/store'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; +const isBusySubject = new BehaviorSubject(false); + +class MockFormComponentService + implements Partial +{ + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl(), + tokenCode: new UntypedFormControl(), + }); + login = createSpy().and.stub(); + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); + displayMessage = createSpy('displayMessage').and.stub(); +} + +class MockRoutingService { + go = createSpy(); +} +class MockStore { + dispatch = jasmine.createSpy(); + select = jasmine.createSpy().and.returnValue(of({})); +} +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +class MockLaunchDialogService implements Partial { + openDialogAndSubscribe = createSpy().and.stub(); +} + +describe('RegisterVerificationTokenFormComponent', () => { + let component: RegisterVerificationTokenFormComponent; + let fixture: ComponentFixture; + let el: DebugElement; + let service: RegisterVerificationTokenFormComponentService; + let facade: VerificationTokenFacade; + let launchDialogService: LaunchDialogService; + let routineservice: RoutingService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + SpinnerModule, + ], + declarations: [RegisterVerificationTokenFormComponent, MockUrlPipe], + providers: [ + { provide: Store, useClass: MockStore }, + { + provide: RegisterVerificationTokenFormComponent, + useClass: MockFormComponentService, + }, + { + provide: LaunchDialogService, + useClass: MockLaunchDialogService, + }, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + ChangeDetectorRef, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterVerificationTokenFormComponent); + service = TestBed.inject(RegisterVerificationTokenFormComponentService); + facade = TestBed.inject(VerificationTokenFacade); + launchDialogService = TestBed.inject(LaunchDialogService); + routineservice = TestBed.inject(RoutingService); + component = fixture.componentInstance; + component.isUpdating$ = isBusySubject; + el = fixture.debugElement; + fixture.detectChanges(); + history.pushState( + { + tokenId: '', + password: 'pw4all', + loginId: 'test@sap.com', + }, + '' + ); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('button') + ).nativeElement; + expect(submitBtn.disabled).toBeTruthy(); + }); + + it('should show the spinner', () => { + isBusySubject.next(true); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + + describe('idle', () => { + it('should enable the submit button', () => { + component.form.enable(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('button')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + }); + + describe('refresh with no tokenId/loginId', () => { + it('should navigate back to login page', () => { + spyOn(service, 'displayMessage'); + history.pushState( + { + tokenId: '', + loginId: '', + }, + '' + ); + component.ngOnInit(); + expect(routineservice.go).toHaveBeenCalledWith(['/login/register']); + expect(service.displayMessage).toHaveBeenCalledWith( + 'verificationTokenForm.needInputCredentials', + {} + ); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + spyOn(service, 'registerUser').and.returnValue( + of({ + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + titleCode: 'Mr', + password: 'Password123', + }) + ); + component.form.setValue({ + tokenId: 'tokenId', + tokenCode: '123', + }); + component.onSubmit(); + expect(service.registerUser).toHaveBeenCalledWith(component.registerData); + }); + + it('should display info dialog', () => { + component.openInfoDailog(); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + + it('should display info dialog when keydown', () => { + const event = { + key: 'Enter', + preventDefault: () => {}, + }; + component.onOpenInfoDailogKeyDown(event as KeyboardEvent); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + + it('should resend OTP', () => { + component.target = 'example@example.com'; + spyOn(component, 'startWaitTimeInterval'); + spyOn(service, 'displayMessage'); + spyOn(facade, 'createVerificationToken').and.returnValue( + of({ + tokenId: 'tokenId', + expiresIn: '300', + }) + ); + + component.resendOTP(); + + expect(component.isResendDisabled).toBe(true); + expect(component.waitTime).toBe(60); + expect(component.startWaitTimeInterval).toHaveBeenCalled(); + expect(facade.createVerificationToken).toHaveBeenCalledWith({ + loginId: 'example@example.com', + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }); + expect(service.displayMessage).toHaveBeenCalledWith( + 'verificationTokenForm.createVerificationToken', + { target: 'example@example.com' } + ); + }); + }); +}); diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts new file mode 100644 index 00000000000..f7b09b38c37 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + OnInit, + ViewChild, + inject, +} from '@angular/core'; +import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; + +import { + VerificationToken, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { RoutingService } from '@spartacus/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { Subject, Subscription } from 'rxjs'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants'; + +@Component({ + selector: 'cx-verification-token-form', + templateUrl: './verification-token-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegisterVerificationTokenFormComponent implements OnInit { + protected service: RegisterVerificationTokenFormComponentService = inject( + RegisterVerificationTokenFormComponentService + ); + protected launchDialogService: LaunchDialogService = + inject(LaunchDialogService); + protected cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + protected routingService: RoutingService = inject(RoutingService); + protected verificationTokenFacade = inject(VerificationTokenFacade); + form: UntypedFormGroup = this.service.form; + isUpdating$: Subject = new Subject(); + waitTime: number = 60; + protected subscriptions = new Subscription(); + + @HostBinding('class.user-form') style = true; + + @ViewChild('noReceiveCodeLink') element: ElementRef; + + @ViewChild('resendLink') resendLink: ElementRef; + + tokenId: string; + + registerData: UntypedFormGroup; + + target: string; + + isResendDisabled: boolean = true; + + ngOnInit() { + if (!!history.state) { + this.registerData = history.state['registrationDataForm']; + this.tokenId = history.state['tokenId']; + this.target = history.state['loginId']; + history.pushState( + { + tokenId: '', + loginId: '', + }, + 'verifyToken' + ); + if (!this.target || !this.tokenId) { + this.service.displayMessage( + 'verificationTokenForm.needInputCredentials', + {} + ); + this.routingService.go(['/login/register']); + } else { + this.startWaitTimeInterval(); + this.service.displayMessage( + 'verificationTokenForm.createVerificationToken', + { target: this.target } + ); + } + } + } + + onSubmit(): void { + if (!this.form.valid) { + this.form.markAllAsTouched(); + return; + } + this.isUpdating$.next(true); + this.subscriptions.add( + this.service.registerUser(this.registerData).subscribe({ + complete: () => this.isUpdating$.next(false), + error: () => { + this.isUpdating$.next(false); + this.form + .get('tokenCode') + ?.setErrors({ invalidTokenCodeError: true }); + }, + }) + ); + } + + resendOTP(): void { + this.isResendDisabled = true; + this.waitTime = 60; + this.resendLink.nativeElement.tabIndex = -1; + this.resendLink.nativeElement.blur(); + this.startWaitTimeInterval(); + this.verificationTokenFacade + .createVerificationToken({ + loginId: this.target, + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }) + .subscribe({ + next: (result: VerificationToken) => (this.tokenId = result.tokenId), + complete: () => + this.service.displayMessage( + 'verificationTokenForm.createVerificationToken', + { target: this.target } + ), + }); + } + + startWaitTimeInterval(): void { + const interval = setInterval(() => { + this.waitTime--; + this.cdr.detectChanges(); + if (this.waitTime <= 0) { + clearInterval(interval); + this.isResendDisabled = false; + this.resendLink.nativeElement.tabIndex = 0; + this.cdr.detectChanges(); + } + }, 1000); + } + + openInfoDailog(): void { + this.launchDialogService.openDialogAndSubscribe( + LAUNCH_CALLER.ACCOUNT_VERIFICATION_TOKEN, + this.element + ); + } + + onOpenInfoDailogKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.openInfoDailog(); + } + } +} diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.module.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.module.ts new file mode 100644 index 00000000000..9468de1ac66 --- /dev/null +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.module.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { + AuthService, + CmsConfig, + FeaturesConfigModule, + GlobalMessageService, + I18nModule, + NotAuthGuard, + UrlModule, + WindowRef, + provideDefaultConfig, +} from '@spartacus/core'; +import { + FormErrorsModule, + IconModule, + KeyboardFocusModule, + SpinnerModule, +} from '@spartacus/storefront'; + +import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { RegisterVerificationTokenFormComponent } from './verification-token-form.component'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + KeyboardFocusModule, + ReactiveFormsModule, + RouterModule, + UrlModule, + IconModule, + I18nModule, + FormErrorsModule, + SpinnerModule, + FeaturesConfigModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + VerifyOTPForB2BRegistrationComponent: { + component: RegisterVerificationTokenFormComponent, + guards: [NotAuthGuard], + providers: [ + { + provide: RegisterVerificationTokenFormComponentService, + useClass: RegisterVerificationTokenFormComponentService, + deps: [ + AuthService, + GlobalMessageService, + VerificationTokenFacade, + WindowRef, + ], + }, + ], + }, + }, + }), + ], + declarations: [RegisterVerificationTokenFormComponent], +}) +export class RegisterVerificationTokenFormModule {} diff --git a/feature-libs/organization/user-registration/root/model/user-registration.model.ts b/feature-libs/organization/user-registration/root/model/user-registration.model.ts index 5fb9650b2e6..1f44cfd9bc9 100644 --- a/feature-libs/organization/user-registration/root/model/user-registration.model.ts +++ b/feature-libs/organization/user-registration/root/model/user-registration.model.ts @@ -10,6 +10,8 @@ export interface OrganizationUserRegistration { firstName: string; lastName: string; message?: string; + verificationTokenId?: string; + verificationTokenCode?: string; } export interface OrganizationUserRegistrationForm diff --git a/feature-libs/organization/user-registration/root/user-registration-root.module.ts b/feature-libs/organization/user-registration/root/user-registration-root.module.ts index 28619546527..23fe9a8eb9d 100644 --- a/feature-libs/organization/user-registration/root/user-registration-root.module.ts +++ b/feature-libs/organization/user-registration/root/user-registration-root.module.ts @@ -12,7 +12,11 @@ export function defaultOrganizationUserRegistrationComponentsConfig(): CmsConfig const config: CmsConfig = { featureModules: { [ORGANIZATION_USER_REGISTRATION_FEATURE]: { - cmsComponents: ['OrganizationUserRegistrationComponent'], + cmsComponents: [ + 'OrganizationUserRegistrationComponent', + 'RegisterB2BCustomerWithOTPComponent', + 'VerifyOTPForB2BRegistrationComponent', + ], }, }, }; diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index 6f7c29744c1..92946757424 100644 --- a/feature-libs/user/account/assets/translations/en/userAccount.json +++ b/feature-libs/user/account/assets/translations/en/userAccount.json @@ -26,6 +26,7 @@ }, "noReceiveCode": "Didn't receive the code?", "verify": "Verify", + "registerVerify": "Register", "back": "Back", "tokenInputHint": "You can request a new code. A timer starts indicating the seconds left until you can resend your request." }, diff --git a/feature-libs/user/account/styles/_verification-token-form.scss b/feature-libs/user/account/styles/_verification-token-form.scss index d9787db7708..fc1b896f5d0 100644 --- a/feature-libs/user/account/styles/_verification-token-form.scss +++ b/feature-libs/user/account/styles/_verification-token-form.scss @@ -29,6 +29,10 @@ } } + .register-b2b-otp-resend-link-text { + margin-top: -1rem; + } + .verify-container { width: 100%; margin-top: 2.5rem; diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts new file mode 100644 index 00000000000..377440f7421 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { listenForTokenAuthenticationRequest } from '../../../../helpers/login'; + +describe('Tabbing order for B2B OTP registration', () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + describe('B2B OTP Registration', () => { + context('B2B OTP Registration page', () => { + beforeEach(() => { + cy.visit('/login/register'); + }); + it('should allow to navigate with tab key for otp registration form and otp verification page(CXSPA-8772)', () => { + cy.get('cx-user-registration-form').should('exist'); + cy.get('[formcontrolname="titleCode"]').ngSelect('Mr.'); + cy.get('[formcontrolname="firstName"]').type('John'); + cy.get('[formcontrolname="lastName"]').type('Doe'); + cy.get('[formcontrolname="companyName"]').type('My Company Inc.'); + var email = + 'test.test' + Math.floor(Math.random() * 10001) + '@sap.com'; + cy.get('[formcontrolname="email"]').type(email); + + cy.get('button[type=submit]').click(); + cy.get('cx-verification-token-form').should('exist'); + listenForUserVerficationCodeEmailReceive(email); + const mailCCV2Url = + Cypress.env('MAIL_CCV2_URL') + + Cypress.env('MAIL_CCV2_PREFIX') + + '/search?query=' + + email + + '&kind=to&start=0&limit=1'; + + cy.request({ + method: 'GET', + url: mailCCV2Url, + }).then((response) => { + const verificationCodeEmailStartText = + 'Please use the following verification code to register in Spartacus powertools Site:

'; + const lableP = '

'; + const items = response.body.items; + const emailBody = items[0].Content.Body; + + const verificationCodeEmailStartIndex = + emailBody.indexOf(verificationCodeEmailStartText) + + verificationCodeEmailStartText.length; + const verificationCodeStartIndex = + emailBody.indexOf(lableP, verificationCodeEmailStartIndex) + + lableP.length; + const verificationCode = emailBody.substring( + verificationCodeStartIndex, + verificationCodeStartIndex + 8 + ); + + listenForTokenAuthenticationRequest(); + cy.get('cx-verification-token-form').within(() => { + cy.get('[formcontrolname="tokenCode"]') + .clear() + .type(verificationCode); + cy.get('button[type=submit]').click(); + }); + cy.get('cx-global-message').should('exist'); + cy.get('cx-global-message').contains( + 'Thank you for registering! A representative will contact you shortly and confirm your access information.' + ); + cy.get('cx-login').should('exist'); + }); + }); + }); + }); +}); + +export function listenForUserVerficationCodeEmailReceive( + customerEmail: string +) { + const mailCCV2Url = + Cypress.env('MAIL_CCV2_URL') + + Cypress.env('MAIL_CCV2_PREFIX') + + '/search?query=' + + customerEmail + + '&kind=to'; + + cy.request({ + method: 'GET', + url: mailCCV2Url, + }).then((response) => { + if (response.body.total != 1) { + listenForUserVerficationCodeEmailReceive(customerEmail); + } + }); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts new file mode 100644 index 00000000000..7d18f83cea8 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + verifyTabbingOrderForRegistrationWithOTP, + verifyTabbingOrderForOTPVerification, +} from '../../../../helpers/b2b/b2b-user-registration'; + +describe('Tabbing order for B2B OTP registration', () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + describe('B2B OTP Registration', () => { + context('B2B OTP Registration page', () => { + beforeEach(() => { + cy.visit('/login/register'); + }); + it('should allow to navigate with tab key for otp registration form and otp verification page(CXSPA-8772)', () => { + cy.get('cx-user-registration-form').should('exist'); + cy.get('[formcontrolname="titleCode"]').ngSelect('Mr.'); + cy.get('[formcontrolname="firstName"]').type('John'); + cy.get('[formcontrolname="lastName"]').type('Doe'); + cy.get('[formcontrolname="companyName"]').type('My Company Inc.'); + cy.get('[formcontrolname="email"]').type('test.test@sap.com'); + verifyTabbingOrderForRegistrationWithOTP(); + + cy.get('button[type=submit]').click(); + cy.get('cx-verification-token-form').should('exist'); + verifyTabbingOrderForOTPVerification(); + }); + }); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts index 63ab1d283ee..0aa85b6ca58 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts @@ -499,6 +499,36 @@ export const tabbingOrderConfig: TabbingOrderConfig = { type: TabbingOrderTypes.LINK, }, ], + userRegistrationFormWithOTP: [ + { type: TabbingOrderTypes.NG_SELECT }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.NG_SELECT }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.TEXT_AREA }, + { + value: 'Continue', + type: TabbingOrderTypes.BUTTON, + }, + ], + verificationToken: [ + { type: TabbingOrderTypes.FORM_FIELD }, + { type: TabbingOrderTypes.BUTTON }, + { + value: 'Register', + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Back', + type: TabbingOrderTypes.BUTTON, + }, + ], quoteDetailsPage: [ { value: 'New Cart', diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts index 04701f45eca..c4b53ea762f 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts @@ -105,6 +105,14 @@ export function verifyTabbingOrder() { ); } +export function verifyTabbingOrderForRegistrationWithOTP() { + tabbingOrder('form', config.userRegistrationFormWithOTP); +} + +export function verifyTabbingOrderForOTPVerification() { + tabbingOrder('form', config.verificationToken); +} + export function verifyFormErrors() { const requiredFirstNameFieldMessage = 'Field First name is required'; const requiredLastNameFieldMessage = 'Field Last name is required';