import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ButtonsModule } from '../index'; @Component({ selector: 'buttons-test', template: '' }) class TestButtonsComponent implements OnInit { singleModel = '0'; // eslint-disable-next-line @typescript-eslint/no-explicit-any checkModel: any = { left: false, middle: true, right: false }; radioModel = 'Middle'; radioUncheckableModel: string; myForm: FormGroup; radioGroupControl = new FormControl(null); radioGroupModel = null; groupDisabled = false; constructor(public cdRef: ChangeDetectorRef, private formBuilder: FormBuilder) { } disableGroups() { this.radioGroupControl.disable(); this.groupDisabled = true; } enableGroups() { this.radioGroupControl.enable(); this.groupDisabled = false; } ngOnInit(): void { this.myForm = this.formBuilder.group({ radio: 'Middle' }); } } const html = `
`; const htmlGroupReactive = `
`; const htmlGroupModel = `
`; function createComponent(htmlTemplate, dtc?: string): ComponentFixture { switch (dtc) { case 'OnPush': TestBed.overrideComponent(TestButtonsComponent, { set: { template: htmlTemplate, changeDetection: ChangeDetectionStrategy.OnPush } }); break; default: TestBed.overrideComponent(TestButtonsComponent, { set: { template: htmlTemplate } }); } const fixture = TestBed.createComponent(TestButtonsComponent); fixture.detectChanges(); return fixture; } describe('Directive: Buttons', () => { let fixture: ComponentFixture; let context; let element; beforeEach( fakeAsync(() => { TestBed.configureTestingModule({ declarations: [TestButtonsComponent], imports: [ButtonsModule, FormsModule, ReactiveFormsModule], providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }] }); }) ); afterAll(async () => { await new Promise(resolve => setTimeout(() => resolve(), 500)); // avoid jest open handle error }); describe('checkbox', () => { afterEach(() => { fixture.destroy(); }); it( 'should work correctly with default model values', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; expect(element.querySelector('#default').classList).not.toContain('active'); context.singleModel = true; fixture.detectChanges(); tick(); expect(element.querySelector('#default').classList).toContain('active'); }) ); it( 'should work correctly with default model values with OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); expect(element.querySelector('#default').classList).not.toContain('active'); context.singleModel = true; fixture.detectChanges(); tick(); expect(element.querySelector('#default').classList).toContain('active'); }) ); it( 'should bind custom model values', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; expect(element.querySelector('#custom').classList).not.toContain('active'); context.singleModel = '1'; fixture.detectChanges(); tick(); expect(element.querySelector('#custom').classList).toContain('active'); }) ); it( 'should bind custom model values with OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); expect(element.querySelector('#custom').classList).not.toContain('active'); context.singleModel = '1'; fixture.detectChanges(); tick(); expect(element.querySelector('#custom').classList).toContain('active'); }) ); it('should toggle default model values on click', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; context.singleModel = false; fixture.detectChanges(); const btn = element.querySelector('#default'); fixture.whenStable() .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual(true); expect(btn.classList).toContainEqual('active'); }) .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); }); })); it('should toggle default model values on click OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); context.singleModel = false; fixture.detectChanges(); const btn = element.querySelector('#default'); fixture.whenStable() .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual(true); expect(btn.classList).toContain('active'); }) .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); }); })); it('should toggle custom model values on click', () => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; const btn = element.querySelector('#custom'); fixture.detectChanges(); fixture.whenStable() .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual('1'); expect(btn.classList).toContain('active'); }) .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual('0'); expect(btn.classList).not.toContain('active'); }); }); it('should toggle custom model values on click OnPush', () => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); const btn = element.querySelector('#custom'); fixture.whenStable() .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual('1'); expect(btn.classList).toContain('active'); }) .then(() => { btn.click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.singleModel).toEqual('0'); expect(btn.classList).not.toContain('active'); }); }); it('should not toggle when disabled', () => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; context.singleModel = false; fixture.detectChanges(); const btn = element.querySelector('#disabled'); btn.click(); fixture.detectChanges(); expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); btn.click(); fixture.detectChanges(); expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); }); it('should not toggle when disabled OnPush', () => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); context.singleModel = false; fixture.detectChanges(); const btn = element.querySelector('#disabled'); btn.click(); fixture.detectChanges(); expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); btn.click(); fixture.detectChanges(); expect(context.singleModel).toEqual(false); expect(btn.classList).not.toContain('active'); }); it( 'should work for btn-group', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); const btn = element.querySelector('.btn-group.checkbox'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }); }) ); it( 'should work for btn-group OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); fixture.whenStable().then(() => { const btn = element.querySelector('.btn-group.checkbox'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }); }) ); }); describe('radio', () => { describe('with reactive form', () => { let btn = null; beforeEach( fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); btn = element.querySelector('.btn-group.reactive-radio'); }) ); it( 'should set active class based on model', fakeAsync(() => { expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); context.myForm.get('radio').setValue('Left'); fixture.detectChanges(); tick(); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should set active class via click', fakeAsync(() => { (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.myForm.get('radio').value).toEqual('Left'); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); (btn.children[2] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.myForm.get('radio').value).toEqual('Right'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).toContain('active'); }) ); it('should do nothing when clicking an active radio', fakeAsync(() => { context.myForm.get('radio').setValue('Left'); fixture.detectChanges(); tick(); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); expect(context.myForm.get('radio').value).toEqual('Left'); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should set disabled attribute when form status is changed to disabled', fakeAsync(() => { expect(btn.children[0].getAttribute('disabled')).toBeNull(); expect(btn.children[1].getAttribute('disabled')).toBeNull(); expect(btn.children[2].getAttribute('disabled')).toBeNull(); context.myForm.disable(); fixture.detectChanges(); tick(); expect(btn.children[0].getAttribute('disabled')).toEqual('disabled'); expect(btn.children[1].getAttribute('disabled')).toEqual('disabled'); expect(btn.children[2].getAttribute('disabled')).toEqual('disabled'); }) ); it( 'should not change model when form is disabled', fakeAsync(() => { context.myForm.get('radio').setValue('Left'); fixture.detectChanges(); tick(); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); context.myForm.disable(); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.myForm.get('radio').value).toEqual('Left'); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); }); it( 'should set active class based on model', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); const btn = element.querySelector('.btn-group.radio'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); context.radioModel = 'Left'; fixture.detectChanges(); tick(); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should set active class based on model OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); context.cdRef.markForCheck(); const btn = element.querySelector('.btn-group.radio'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); context.radioModel = 'Left'; fixture.detectChanges(); tick(); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should set active class via click', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); const btn = element.querySelector('.btn-group.radio'); delete context.radioModel; expect(context.radioModel).toBeUndefined(); (btn.children[2] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.radioModel).toEqual('Right'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).toContain('active'); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should set active class via click OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); context.cdRef.markForCheck(); const btn = element.querySelector('.btn-group.radio'); delete context.radioModel; expect(context.radioModel).toBeUndefined(); (btn.children[2] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.radioModel).toEqual('Right'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).toContain('active'); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); tick(); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should do nothing when clicking an active radio', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); const btn = element.querySelector('.btn-group.radio'); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should do nothing when clicking an active radio OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); context.cdRef.markForCheck(); const btn = element.querySelector('.btn-group.radio'); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); expect(context.radioModel).toEqual('Middle'); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) ); it( 'should not toggle when disabled', fakeAsync(() => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); const btn = element.querySelector('.btn-group.radio'); expect(context.radioModel).toEqual('Middle'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); context.radioModel = '1'; fixture.detectChanges(); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); (btn.children[3] as HTMLElement).click(); fixture.detectChanges(); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); }) ); it( 'should not toggle when disabled OnPush', fakeAsync(() => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); tick(); fixture.detectChanges(); context.cdRef.markForCheck(); const btn = element.querySelector('.btn-group.radio'); expect(context.radioModel).toEqual('Middle'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); context.radioModel = '1'; fixture.detectChanges(); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); (btn.children[3] as HTMLElement).click(); fixture.detectChanges(); expect(btn.children[1].classList).toContain('active'); expect(btn.children[3].classList).not.toContain('active'); }) ); it('should not toggle when click in active button without uncheckable', () => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; const btn = element.querySelector('.btn-group.radio'); expect(context.radioModel).toEqual('Middle'); (btn.children[1] as HTMLElement).click(); fixture.detectChanges(); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }); it('should unset active class via click', () => { fixture = createComponent(html); context = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); const btn = element.querySelector('.btn-group.radioUncheckable'); expect(context.radioUncheckableModel).toBeUndefined(); fixture.whenStable() .then(() => { (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.radioUncheckableModel).toEqual('Left'); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) .then(() => { (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.radioUncheckableModel).toBeUndefined(); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }); }); it('should unset active class via click OnPush', () => { fixture = createComponent(html, 'OnPush'); context = fixture.componentInstance; element = fixture.nativeElement; context.cdRef.markForCheck(); const btn = element.querySelector('.btn-group.radioUncheckable'); expect(context.radioUncheckableModel).toBeUndefined(); fixture.whenStable() .then(() => { (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.radioUncheckableModel).toEqual('Left'); expect(btn.children[0].classList).toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }) .then(() => { (btn.children[0] as HTMLElement).click(); fixture.detectChanges(); return fixture.whenStable(); }) .then(() => { expect(context.radioUncheckableModel).toBeUndefined(); expect(btn.children[0].classList).not.toContain('active'); expect(btn.children[1].classList).not.toContain('active'); expect(btn.children[2].classList).not.toContain('active'); }); }); }); describe('radioGroup', () => { let radioGroup: HTMLDivElement; describe('reactive form', () => { radioGroupTests(true); }); describe('ngModel form', () => { radioGroupTests(false); }); function radioGroupTests(reactive: boolean) { function getModelValue() { if (reactive) { return context.radioGroupControl.value; } else { return context.radioGroupModel; } } function setModelValue(value) { if (reactive) { context.radioGroupControl.setValue(value); } else { context.radioGroupModel = value; } } function getHtml() { if (reactive) { return htmlGroupReactive; } else { return htmlGroupModel; } } beforeEach(() => { fixture = createComponent(getHtml()); context = fixture.componentInstance; element = fixture.nativeElement; radioGroup = element.querySelector('.btn-group.radioGroup'); }); it('should have a tabindex=0 if group is enabled', () => { fixture.detectChanges(); expect(radioGroup.getAttribute('tabindex')).toEqual('0'); }); it('should have aria attributes', () => { fixture.detectChanges(); expect(radioGroup.getAttribute('role')).toEqual('radiogroup'); for (let i = 0; i <= 2; i++) { expect(radioGroup.children[i].getAttribute('role')).toEqual('radio'); } }); it( 'should not have a tabindex if group is disabled', fakeAsync(() => { context.disableGroups(); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(radioGroup.hasAttribute('tabindex')).toBeFalsy(); }) ); it('should have no radio selected by default', () => { fixture.detectChanges(); expect(radioGroup.children[0].classList).not.toContain('active'); expect(radioGroup.children[1].classList).not.toContain('active'); expect(radioGroup.children[2].classList).not.toContain('active'); }); it( 'should select radio depending on control value', fakeAsync(() => { setModelValue('Right'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(radioGroup.children[0].classList).not.toContain('active'); expect(radioGroup.children[1].classList).not.toContain('active'); expect(radioGroup.children[2].classList).toContain('active'); expect(radioGroup.children[2].getAttribute('aria-checked')).toEqual('true'); }) ); function pressKeyInRadioGroup(key: string) { const event: Event = new KeyboardEvent('keydown', { key }); radioGroup.dispatchEvent(event); fixture.detectChanges(); } it('should select the next radio when arrow right is pressed', () => { radioGroup.focus(); fixture.detectChanges(); pressKeyInRadioGroup('ArrowRight'); expect(document.activeElement).toEqual(radioGroup.children[1]); expect(getModelValue()).toEqual('Middle'); }); it('should select the next radio when arrow down is pressed ', () => { radioGroup.focus(); fixture.detectChanges(); pressKeyInRadioGroup('ArrowDown'); expect(document.activeElement).toEqual(radioGroup.children[1]); expect(getModelValue()).toEqual('Middle'); }); it('should select the previous radio when arrow left is pressed ', () => { radioGroup.focus(); fixture.detectChanges(); pressKeyInRadioGroup('ArrowLeft'); // It should wrap, so last button is selected expect(document.activeElement).toEqual(radioGroup.children[2]); expect(getModelValue()).toEqual('Right'); }); it('should select the previous radio when arrow top is pressed ', () => { radioGroup.focus(); fixture.detectChanges(); pressKeyInRadioGroup('ArrowUp'); // It should wrap, so last button is selected expect(document.activeElement).toEqual(radioGroup.children[2]); expect(getModelValue()).toEqual('Right'); }); } }); });