diff --git a/src/app/extensions/captcha/captcha.module.ts b/src/app/extensions/captcha/captcha.module.ts deleted file mode 100644 index 14a5084a34b..00000000000 --- a/src/app/extensions/captcha/captcha.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RECAPTCHA_V3_SITE_KEY, RecaptchaModule, RecaptchaV3Module } from 'ng-recaptcha'; - -import { SharedModule } from 'ish-shared/shared.module'; - -import { CaptchaExportsModule } from './exports/captcha-exports.module'; -import { CaptchaV2Component } from './shared/captcha/captcha-v2/captcha-v2.component'; -import { CaptchaV3Component } from './shared/captcha/captcha-v3/captcha-v3.component'; -import { CaptchaComponent } from './shared/captcha/captcha/captcha.component'; - -export function getSynchronizedSiteKey(captchaExportsModule: CaptchaExportsModule) { - return captchaExportsModule.siteKey; -} - -// not-dead-code -@NgModule({ - imports: [RecaptchaModule, RecaptchaV3Module, SharedModule], - declarations: [CaptchaComponent, CaptchaV2Component, CaptchaV3Component], - exports: [SharedModule], - entryComponents: [CaptchaComponent], - providers: [ - { - provide: RECAPTCHA_V3_SITE_KEY, - useFactory: getSynchronizedSiteKey, - deps: [CaptchaExportsModule], - }, - ], -}) -export class CaptchaModule {} diff --git a/src/app/extensions/captcha/exports/captcha-exports.module.ts b/src/app/extensions/captcha/exports/captcha-exports.module.ts index 2216f626ee5..4c4cf392f2b 100644 --- a/src/app/extensions/captcha/exports/captcha-exports.module.ts +++ b/src/app/extensions/captcha/exports/captcha-exports.module.ts @@ -1,35 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { ReactiveComponentLoaderModule } from '@wishtack/reactive-component-loader'; -import { take } from 'rxjs/operators'; -import { whenTruthy } from 'ish-core/utils/operators'; - -import { CaptchaFacade } from '../facades/captcha.facade'; +import { SitekeyProviderService } from '../services/sitekey-provider/sitekey-provider.service'; import { LazyCaptchaComponent } from './captcha/lazy-captcha/lazy-captcha.component'; @NgModule({ - imports: [ - CommonModule, - ReactiveComponentLoaderModule.withModule({ - moduleId: 'ish-extensions-captcha', - loadChildren: '../captcha.module#CaptchaModule', - }), - ], + imports: [CommonModule], declarations: [LazyCaptchaComponent], exports: [LazyCaptchaComponent], + providers: [SitekeyProviderService], }) -export class CaptchaExportsModule { - siteKey: string; - - constructor(captchaFacade: CaptchaFacade) { - // synchronize asynchronous site key so we can provide it synchronously for the recaptcha service later on. - captchaFacade.captchaSiteKey$ - .pipe( - whenTruthy(), - take(1) - ) - .subscribe(siteKey => (this.siteKey = siteKey)); - } -} +export class CaptchaExportsModule {} diff --git a/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.html b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.html index 8e77b9c8b26..a61aef13d6c 100644 --- a/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.html +++ b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.html @@ -1,8 +1 @@ - + diff --git a/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.spec.ts b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.spec.ts new file mode 100644 index 00000000000..23920d9a452 --- /dev/null +++ b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.spec.ts @@ -0,0 +1,113 @@ +import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { RECAPTCHA_V3_SITE_KEY, RecaptchaComponent } from 'ng-recaptcha'; +import { EMPTY, of } from 'rxjs'; +import { anyString, instance, mock, when } from 'ts-mockito'; + +import { configurationReducer } from 'ish-core/store/configuration/configuration.reducer'; +import { ngrxTesting } from 'ish-core/utils/dev/ngrx-testing'; + +import { CaptchaFacade } from '../../../facades/captcha.facade'; +import { CaptchaV2Component, CaptchaV2ComponentModule } from '../../../shared/captcha/captcha-v2/captcha-v2.component'; +import { CaptchaV3Component, CaptchaV3ComponentModule } from '../../../shared/captcha/captcha-v3/captcha-v3.component'; + +import { LazyCaptchaComponent } from './lazy-captcha.component'; + +describe('Lazy Captcha Component', () => { + let fixture: ComponentFixture; + let component: LazyCaptchaComponent; + let element: HTMLElement; + let captchaFacade: CaptchaFacade; + + beforeEach(async(() => { + captchaFacade = mock(CaptchaFacade); + when(captchaFacade.captchaVersion$).thenReturn(EMPTY); + when(captchaFacade.captchaSiteKey$).thenReturn(of('captchaSiteKeyASDF')); + when(captchaFacade.captchaActive$(anyString())).thenReturn(of(true)); + + TestBed.configureTestingModule({ + declarations: [MockComponent(RecaptchaComponent)], + imports: [ + CaptchaV2ComponentModule, + CaptchaV3ComponentModule, + RouterTestingModule, + TranslateModule.forRoot(), + ngrxTesting({ + reducers: { configuration: configurationReducer }, + }), + ], + providers: [ + { provide: CaptchaFacade, useFactory: () => instance(captchaFacade) }, + { provide: RECAPTCHA_V3_SITE_KEY, useValue: 'captchaSiteKeyQWERTY' }, + ], + }) + .overrideModule(CaptchaV2ComponentModule, { set: { entryComponents: [CaptchaV2Component] } }) + .overrideModule(CaptchaV3ComponentModule, { set: { entryComponents: [CaptchaV3Component] } }) + // .overrideComponent(LazyCaptchaComponent, { set: { entryComponents: [CaptchaV2Component, CaptchaV3Component] } }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LazyCaptchaComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.form = new FormGroup({ + captcha: new FormControl(''), + captchaAction: new FormControl(''), + }); + component.cssClass = 'd-none'; + component.topic = 'register'; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should render v2 component when configured', fakeAsync(() => { + when(captchaFacade.captchaVersion$).thenReturn(of(2 as 2)); + fixture.detectChanges(); + + tick(500); + expect(element).toMatchInlineSnapshot(``); + const v2Cmp: CaptchaV2Component = fixture.debugElement.query(By.css('ish-captcha-v2'))?.componentInstance; + expect(v2Cmp).toBeTruthy(); + expect(v2Cmp.cssClass).toEqual('d-none'); + })); + it('should render v3 component when configured', fakeAsync(() => { + when(captchaFacade.captchaVersion$).thenReturn(of(3 as 3)); + fixture.detectChanges(); + + tick(500); + expect(element).toMatchInlineSnapshot(``); + const v3Cmp: CaptchaV3Component = fixture.debugElement.query(By.css('ish-captcha-v3'))?.componentInstance; + expect(v3Cmp).toBeTruthy(); + })); + + // errors are thrown if required input parameters are missing + it('should throw an error if there is no form set as input parameter', () => { + component.form = undefined; + expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( + `"required input parameter
is missing for FormElementComponent"` + ); + }); + + it('should throw an error if there is no control "captcha" in the given form', () => { + delete component.form.controls.captcha; + expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( + `"form control 'captcha' does not exist in the given form for LazyCaptchaComponent"` + ); + }); + + it('should throw an error if there is no control "captchaAction" in the given form', () => { + delete component.form.controls.captchaAction; + expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( + `"form control 'captchaAction' does not exist in the given form for LazyCaptchaComponent"` + ); + }); +}); diff --git a/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.ts b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.ts index 3d5ea101ac2..8012671f9e4 100644 --- a/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.ts +++ b/src/app/extensions/captcha/exports/captcha/lazy-captcha/lazy-captcha.component.ts @@ -1,6 +1,18 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { + ChangeDetectionStrategy, + Compiler, + Component, + Injector, + Input, + NgModuleFactory, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { AbstractControl, FormGroup, Validators } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { switchMapTo, takeUntil } from 'rxjs/operators'; import { CaptchaFacade } from '../../../facades/captcha.facade'; @@ -21,8 +33,9 @@ import { CaptchaFacade } from '../../../facades/captcha.facade'; templateUrl: './lazy-captcha.component.html', changeDetection: ChangeDetectionStrategy.Default, }) -// tslint:disable-next-line:component-creation-test -export class LazyCaptchaComponent implements OnInit { +export class LazyCaptchaComponent implements OnInit, OnDestroy { + @ViewChild('anchor', { read: ViewContainerRef, static: true }) anchor: ViewContainerRef; + /** form containing the captcha form controls */ @@ -40,16 +53,79 @@ export class LazyCaptchaComponent implements OnInit { | 'redemptionOfGiftCardsAndCertificates' | 'register'; - captchaActive$: Observable; - - componentLocation = { - moduleId: 'ish-extensions-captcha', - selector: 'ish-captcha', - }; + private destroy$ = new Subject(); - constructor(private captchaFacade: CaptchaFacade) {} + constructor(private captchaFacade: CaptchaFacade, private compiler: Compiler, private injector: Injector) {} ngOnInit() { - this.captchaActive$ = this.captchaFacade.captchaActive$(this.topic); + this.sanityCheck(); + + this.captchaFacade + .captchaActive$(this.topic) + .pipe(switchMapTo(this.captchaFacade.captchaVersion$), takeUntil(this.destroy$)) + .subscribe(async version => { + if (version === 3) { + this.actionFormControl.setValue(this.topic); + + const { CaptchaV3Component, CaptchaV3ComponentModule } = await import( + '../../../shared/captcha/captcha-v3/captcha-v3.component' + ); + const moduleFactory = await this.loadModuleFactory(CaptchaV3ComponentModule); + const moduleRef = moduleFactory.create(this.injector); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(CaptchaV3Component); + + const componentRef = this.anchor.createComponent(factory); + + componentRef.instance.parentForm = this.form; + } else if (version === 2) { + this.formControl.setValidators([Validators.required]); + this.formControl.updateValueAndValidity(); + + const { CaptchaV2Component, CaptchaV2ComponentModule } = await import( + '../../../shared/captcha/captcha-v2/captcha-v2.component' + ); + const moduleFactory = await this.loadModuleFactory(CaptchaV2ComponentModule); + const moduleRef = moduleFactory.create(this.injector); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(CaptchaV2Component); + + const componentRef = this.anchor.createComponent(factory); + + componentRef.instance.cssClass = this.cssClass; + componentRef.instance.parentForm = this.form; + } + }); + } + + // tslint:disable-next-line: no-any + private async loadModuleFactory(t: any) { + if (t instanceof NgModuleFactory) { + return t; + } else { + return await this.compiler.compileModuleAsync(t); + } + } + + private sanityCheck() { + if (!this.form) { + throw new Error('required input parameter is missing for FormElementComponent'); + } + if (!this.formControl) { + throw new Error(`form control 'captcha' does not exist in the given form for LazyCaptchaComponent`); + } + if (!this.actionFormControl) { + throw new Error(`form control 'captchaAction' does not exist in the given form for LazyCaptchaComponent`); + } + } + + private get formControl(): AbstractControl { + return this.form?.get('captcha'); + } + + private get actionFormControl(): AbstractControl { + return this.form?.get('captchaAction'); + } + + ngOnDestroy() { + this.destroy$.next(); } } diff --git a/src/app/extensions/captcha/facades/captcha.facade.ts b/src/app/extensions/captcha/facades/captcha.facade.ts index e97ba5590d2..68ce5377f5b 100644 --- a/src/app/extensions/captcha/facades/captcha.facade.ts +++ b/src/app/extensions/captcha/facades/captcha.facade.ts @@ -9,9 +9,9 @@ import { whenTruthy } from 'ish-core/utils/operators'; // tslint:disable:member-ordering @Injectable({ providedIn: 'root' }) export class CaptchaFacade { - constructor(private store: Store<{}>) {} + constructor(private store: Store) {} - captchaVersion$ = this.store.pipe( + captchaVersion$: Observable<2 | 3 | undefined> = this.store.pipe( select( getServerConfigParameter<{ ReCaptchaV2ServiceDefinition: { runnable: string }; @@ -45,9 +45,10 @@ export class CaptchaFacade { return this.store.pipe( filter(() => !!key), select(getFeatures), - map(features => !features.includes('noCaptcha')), + filter(features => !features.includes('noCaptcha')), switchMapTo(this.captchaVersion$), - map(x => !!x) + map(x => !!x), + whenTruthy() /* TODO: replace with the following when configuration REST API sends correct values select(getServerConfigParameter('captcha.' + key)) */ diff --git a/src/app/extensions/captcha/services/sitekey-provider/sitekey-provider.service.ts b/src/app/extensions/captcha/services/sitekey-provider/sitekey-provider.service.ts new file mode 100644 index 00000000000..5f89b12991f --- /dev/null +++ b/src/app/extensions/captcha/services/sitekey-provider/sitekey-provider.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { take } from 'rxjs/operators'; + +import { whenTruthy } from 'ish-core/utils/operators'; + +import { CaptchaFacade } from '../../facades/captcha.facade'; + +@Injectable() +export class SitekeyProviderService { + // not-dead-code + siteKey: string; + + constructor(captchaFacade: CaptchaFacade) { + // synchronize asynchronous site key so we can provide it synchronously for the recaptcha service later on. + captchaFacade.captchaSiteKey$.pipe(whenTruthy(), take(1)).subscribe(storeSiteKey => (this.siteKey = storeSiteKey)); + } +} + +export function getSynchronizedSiteKey(service: SitekeyProviderService) { + return service.siteKey; +} diff --git a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.html b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.html index 427f650fab0..6600a809265 100644 --- a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.html +++ b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.spec.ts b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.spec.ts index bc50e5e440b..6fad94c7070 100644 --- a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.spec.ts +++ b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.spec.ts @@ -2,6 +2,10 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { RecaptchaModule } from 'ng-recaptcha'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { CaptchaFacade } from '../../../facades/captcha.facade'; import { CaptchaV2Component } from './captcha-v2.component'; @@ -11,9 +15,13 @@ describe('Captcha V2 Component', () => { let element: HTMLElement; beforeEach(async(() => { + const captchaFacade = mock(CaptchaFacade); + when(captchaFacade.captchaSiteKey$).thenReturn(of('captchaV2SiteKey')); + TestBed.configureTestingModule({ declarations: [CaptchaV2Component], imports: [ReactiveFormsModule, RecaptchaModule.forRoot(), TranslateModule.forRoot()], + providers: [{ provide: CaptchaFacade, useFactory: () => instance(captchaFacade) }], }).compileComponents(); })); @@ -21,9 +29,8 @@ describe('Captcha V2 Component', () => { fixture = TestBed.createComponent(CaptchaV2Component); component = fixture.componentInstance; element = fixture.nativeElement; - component.captchaSiteKey = 'captchaV2SiteKey'; - const fb: FormBuilder = TestBed.get(FormBuilder); + const fb = TestBed.inject(FormBuilder); component.parentForm = fb.group({ captcha: fb.control(''), captchaAction: fb.control(''), diff --git a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.ts b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.ts index c79db8c7d46..ff7ac8d5ad3 100644 --- a/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.ts +++ b/src/app/extensions/captcha/shared/captcha/captcha-v2/captcha-v2.component.ts @@ -1,5 +1,11 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, NgModule, OnInit } from '@angular/core'; import { AbstractControl, FormGroup } from '@angular/forms'; +import { RecaptchaModule } from 'ng-recaptcha'; +import { Observable } from 'rxjs'; + +import { SharedModule } from 'ish-shared/shared.module'; + +import { CaptchaFacade } from '../../../facades/captcha.facade'; /** * The Captcha V2 Component @@ -12,35 +18,36 @@ import { AbstractControl, FormGroup } from '@angular/forms'; changeDetection: ChangeDetectionStrategy.Default, }) export class CaptchaV2Component implements OnInit { - @Input() captchaSiteKey: string; @Input() parentForm: FormGroup; @Input() cssClass: string; + captchaSiteKey$: Observable; + + constructor(private captchaFacade: CaptchaFacade) {} + ngOnInit() { - this.getActionFormControl().setValue(undefined); - this.getFormControl().updateValueAndValidity(); + this.captchaSiteKey$ = this.captchaFacade.captchaSiteKey$; + + this.parentForm.get('captchaAction').setValue(undefined); + this.formControl.updateValueAndValidity(); } /* writes the captcha response token in the captcha form field */ resolved(captchaResponse: string) { - this.getFormControl().setValue(captchaResponse); + this.formControl.setValue(captchaResponse); } - /** - * get the captcha form control according to the controlName - */ - getFormControl(): AbstractControl { - return this.parentForm && this.parentForm.get('captcha'); - } - - /** - * get the captcha action form control according to the controlName - */ - getActionFormControl(): AbstractControl { - return this.parentForm.get('captchaAction'); + private get formControl(): AbstractControl { + return this.parentForm?.get('captcha'); } get hasError(): boolean { - return this.getFormControl() && this.getFormControl().invalid && this.getFormControl().dirty; + return this.formControl?.invalid && this?.formControl.dirty; } } + +@NgModule({ + declarations: [CaptchaV2Component], + imports: [RecaptchaModule, SharedModule], +}) +export class CaptchaV2ComponentModule {} diff --git a/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.spec.ts b/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.spec.ts index 3b7e39e21b8..bee7f31f40b 100644 --- a/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.spec.ts +++ b/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; import { RECAPTCHA_V3_SITE_KEY, RecaptchaV3Module } from 'ng-recaptcha'; import { CaptchaV3Component } from './captcha-v3.component'; @@ -21,6 +22,10 @@ describe('Captcha V3 Component', () => { fixture = TestBed.createComponent(CaptchaV3Component); component = fixture.componentInstance; element = fixture.nativeElement; + component.parentForm = new FormGroup({ + captcha: new FormControl(''), + captchaAction: new FormControl(''), + }); }); it('should be created', () => { diff --git a/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.ts b/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.ts index ec52062c9f0..b8bb8100168 100644 --- a/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.ts +++ b/src/app/extensions/captcha/shared/captcha/captcha-v3/captcha-v3.component.ts @@ -1,9 +1,14 @@ -import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, NgModule, OnDestroy, OnInit } from '@angular/core'; import { FormGroup, Validators } from '@angular/forms'; -import { ReCaptchaV3Service } from 'ng-recaptcha'; +import { RECAPTCHA_V3_SITE_KEY, ReCaptchaV3Service, RecaptchaV3Module } from 'ng-recaptcha'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { + SitekeyProviderService, + getSynchronizedSiteKey, +} from '../../../services/sitekey-provider/sitekey-provider.service'; + /** * The Captcha V3 Component * @@ -14,26 +19,40 @@ import { takeUntil } from 'rxjs/operators'; template: '', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CaptchaV3Component implements OnChanges, OnDestroy { +// tslint:disable-next-line: no-intelligence-in-artifacts +export class CaptchaV3Component implements OnInit, OnDestroy { @Input() parentForm: FormGroup; private destroy$ = new Subject(); constructor(private recaptchaV3Service: ReCaptchaV3Service) {} - /* write the captcha response token in the captcha form field */ - ngOnChanges(c: SimpleChanges) { + ngOnInit() { this.parentForm.get('captchaAction').setValidators([Validators.required]); - if (this.parentForm && c.parentForm.isFirstChange()) { - this.recaptchaV3Service - .execute(this.parentForm.get('captchaAction').value) - .pipe(takeUntil(this.destroy$)) - .subscribe(token => this.parentForm.get('captcha').setValue(token)); - } + this.recaptchaV3Service + .execute(this.parentForm.get('captchaAction').value) + .pipe(takeUntil(this.destroy$)) + .subscribe(token => { + this.parentForm.get('captcha').setValue(token); + this.parentForm.get('captcha').updateValueAndValidity(); + }); } ngOnDestroy() { this.destroy$.next(); } } + +@NgModule({ + imports: [RecaptchaV3Module], + declarations: [CaptchaV3Component], + providers: [ + { + provide: RECAPTCHA_V3_SITE_KEY, + useFactory: getSynchronizedSiteKey, + deps: [SitekeyProviderService], + }, + ], +}) +export class CaptchaV3ComponentModule {} diff --git a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.html b/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.html deleted file mode 100644 index 5ea21dbe4f6..00000000000 --- a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.spec.ts b/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.spec.ts deleted file mode 100644 index 97dea456326..00000000000 --- a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { FormControl, FormGroup } from '@angular/forms'; -import { MockComponent } from 'ng-mocks'; -import { of } from 'rxjs'; -import { anyString, instance, mock, when } from 'ts-mockito'; - -import { configurationReducer } from 'ish-core/store/configuration/configuration.reducer'; -import { ngrxTesting } from 'ish-core/utils/dev/ngrx-testing'; - -import { CaptchaFacade } from '../../../facades/captcha.facade'; -import { CaptchaV2Component } from '../captcha-v2/captcha-v2.component'; -import { CaptchaV3Component } from '../captcha-v3/captcha-v3.component'; - -import { CaptchaComponent } from './captcha.component'; - -describe('Captcha Component', () => { - let fixture: ComponentFixture; - let component: CaptchaComponent; - let element: HTMLElement; - - beforeEach(async(() => { - const captchaFacade = mock(CaptchaFacade); - when(captchaFacade.captchaVersion$).thenReturn(of(2 as 2)); - when(captchaFacade.captchaSiteKey$).thenReturn(of('captchaSiteKeyASDF')); - when(captchaFacade.captchaActive$(anyString())).thenReturn(of(true)); - - TestBed.configureTestingModule({ - declarations: [CaptchaComponent, MockComponent(CaptchaV2Component), MockComponent(CaptchaV3Component)], - imports: [ - ngrxTesting({ - reducers: { configuration: configurationReducer }, - }), - ], - providers: [{ provide: CaptchaFacade, useFactory: () => instance(captchaFacade) }], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CaptchaComponent); - component = fixture.componentInstance; - element = fixture.nativeElement; - component.form = new FormGroup({ - captcha: new FormControl(''), - captchaAction: new FormControl(''), - }); - component.cssClass = 'd-none'; - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - expect(element).toBeTruthy(); - expect(() => fixture.detectChanges()).not.toThrow(); - expect(element).toMatchInlineSnapshot(` - - `); - }); - - // errors are thrown if required input parameters are missing - it('should throw an error if there is no form set as input parameter', () => { - component.form = undefined; - expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( - `"required input parameter is missing for FormElementComponent"` - ); - }); - - it('should throw an error if there is no control "captcha" in the given form', () => { - delete component.form.controls.captcha; - expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( - `"form control 'captcha' does not exist in the given form for CaptchaComponent"` - ); - }); - - it('should throw an error if there is no control "captchaAction" in the given form', () => { - delete component.form.controls.captchaAction; - expect(() => fixture.detectChanges()).toThrowErrorMatchingInlineSnapshot( - `"form control 'captchaAction' does not exist in the given form for CaptchaComponent"` - ); - }); -}); diff --git a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.ts b/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.ts deleted file mode 100644 index 8ef6a9eeacf..00000000000 --- a/src/app/extensions/captcha/shared/captcha/captcha/captcha.component.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { AbstractControl, FormGroup, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; - -import { CaptchaFacade } from '../../../facades/captcha.facade'; - -@Component({ - selector: 'ish-captcha', - templateUrl: './captcha.component.html', - changeDetection: ChangeDetectionStrategy.Default, -}) -export class CaptchaComponent implements OnInit { - @Input() form: FormGroup; - @Input() cssClass: string; - - captchaVersion$: Observable<2 | 3 | undefined>; - captchaSiteKey$: Observable; - - constructor(private captchaFacade: CaptchaFacade) {} - - ngOnInit() { - this.captchaVersion$ = this.captchaFacade.captchaVersion$; - this.captchaSiteKey$ = this.captchaFacade.captchaSiteKey$; - - this.initComponent(); - } - - initComponent() { - if (!this.form) { - throw new Error('required input parameter is missing for FormElementComponent'); - } - if (!this.getFormControl()) { - throw new Error(`form control 'captcha' does not exist in the given form for CaptchaComponent`); - } - if (!this.getActionFormControl()) { - throw new Error(`form control 'captchaAction' does not exist in the given form for CaptchaComponent`); - } - - this.captchaFacade - .captchaActive$(this.getActionFormControl().value) - .pipe(take(1)) - .subscribe(enabled => { - /* check, if required input parameters are available */ - if (enabled) { - this.getFormControl().setValidators([Validators.required]); - this.getFormControl().updateValueAndValidity(); - } - }); - } - - /** - * get the captcha form control according to the controlName - */ - getFormControl(): AbstractControl { - return this.form && this.form.get('captcha'); - } - - /** - * get the captcha action form control according to the controlName - */ - getActionFormControl(): AbstractControl { - return this.form.get('captchaAction'); - } -} diff --git a/tslint.json b/tslint.json index a2539672ff0..0330f1b3f82 100644 --- a/tslint.json +++ b/tslint.json @@ -654,6 +654,10 @@ "name": "^([A-Z].*)StoreModule$", "file": ".*/(|store)/-store\\.module\\.ts$" }, + { + "name": "^([A-Z].*)ComponentModule$", + "file": ".*/\\.component\\.ts$" + }, { "name": "^(.*)Module$", "file": ".*(cypress.*|//|/projects//src/app/|/core/[a-z][a-z0-9-]+)\\.module\\.ts$"