import {dispatchFakeEvent} from '@angular/cdk/testing/private'; import {ChangeDetectionStrategy, Component, DebugElement, Type} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush, flushMicrotasks} from '@angular/core/testing'; import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; import {By} from '@angular/platform-browser'; import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckbox, MatCheckboxChange, MatCheckboxDefaultOptions, MatCheckboxModule, } from './index'; describe('MatCheckbox', () => { let fixture: ComponentFixture; function createComponent(componentType: Type) { TestBed.configureTestingModule({ imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule, componentType], }); return TestBed.createComponent(componentType); } describe('basic behaviors', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let checkboxInstance: MatCheckbox; let testComponent: SingleCheckbox; let inputElement: HTMLInputElement; let labelElement: HTMLLabelElement; let checkboxElement: HTMLElement; beforeEach(() => { fixture = createComponent(SingleCheckbox); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input'); labelElement = checkboxNativeElement.querySelector('label'); checkboxElement = checkboxNativeElement.querySelector('.mdc-checkbox'); }); it('should add and remove the checked state', fakeAsync(() => { expect(checkboxInstance.checked).toBe(false); expect(inputElement.checked).toBe(false); testComponent.isChecked = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.checked).toBe(true); expect(inputElement.checked).toBe(true); expect(inputElement.hasAttribute('aria-checked')) .withContext('Expect aria-checked attribute to not be used') .toBe(false); testComponent.isChecked = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.checked).toBe(false); expect(inputElement.checked).toBe(false); })); it('should hide the internal SVG', () => { const svg = checkboxNativeElement.querySelector('svg')!; expect(svg.getAttribute('aria-hidden')).toBe('true'); }); it('should toggle checkbox ripple disabledness correctly', fakeAsync(() => { const rippleSelector = '.mat-ripple-element:not(.mat-checkbox-persistent-ripple)'; testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(0); flush(); testComponent.isDisabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(1); flush(); })); it('should add and remove indeterminate state', fakeAsync(() => { expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(false); testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(true); testComponent.isIndeterminate = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(false); })); it('should set indeterminate to false when input clicked', fakeAsync(() => { testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.indeterminate).toBe(true); expect(inputElement.indeterminate).toBe(true); expect(testComponent.isIndeterminate).toBe(true); inputElement.click(); fixture.detectChanges(); // Flush the microtasks because the forms module updates the model state asynchronously. flush(); // The checked property has been updated from the model and now the view needs // to reflect the state change. fixture.detectChanges(); expect(checkboxInstance.checked).toBe(true); expect(inputElement.indeterminate).toBe(false); expect(inputElement.checked).toBe(true); expect(testComponent.isIndeterminate).toBe(false); testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.indeterminate).toBe(true); expect(inputElement.indeterminate).toBe(true); expect(inputElement.checked).toBe(true); expect(testComponent.isIndeterminate).toBe(true); inputElement.click(); fixture.detectChanges(); // Flush the microtasks because the forms module updates the model state asynchronously. flush(); // The checked property has been updated from the model and now the view needs // to reflect the state change. fixture.detectChanges(); expect(checkboxInstance.checked).toBe(false); expect(inputElement.indeterminate).toBe(false); expect(inputElement.checked).toBe(false); expect(testComponent.isIndeterminate).toBe(false); })); it('should not set indeterminate to false when checked is set programmatically', fakeAsync(() => { testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(checkboxInstance.indeterminate).toBe(true); expect(inputElement.indeterminate).toBe(true); expect(testComponent.isIndeterminate).toBe(true); testComponent.isChecked = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.checked).toBe(true); expect(inputElement.indeterminate).toBe(true); expect(inputElement.checked).toBe(true); expect(testComponent.isIndeterminate).toBe(true); testComponent.isChecked = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.checked).toBe(false); expect(inputElement.indeterminate).toBe(true); expect(inputElement.checked).toBe(false); expect(testComponent.isIndeterminate).toBe(true); })); it('should change native element checked when check programmatically', () => { expect(inputElement.checked).toBe(false); checkboxInstance.checked = true; fixture.detectChanges(); expect(inputElement.checked).toBe(true); }); it('should toggle checked state on click', fakeAsync(() => { expect(checkboxInstance.checked).toBe(false); labelElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(true); labelElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(false); })); it('should change from indeterminate to checked on click', fakeAsync(() => { testComponent.isChecked = false; testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.checked).toBe(false); expect(checkboxInstance.indeterminate).toBe(true); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(true); expect(checkboxInstance.indeterminate).toBe(false); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(false); expect(checkboxInstance.indeterminate).toBe(false); })); it('should add and remove disabled state', fakeAsync(() => { expect(checkboxInstance.disabled).toBe(false); expect(inputElement.tabIndex).toBe(0); expect(inputElement.disabled).toBe(false); testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.disabled).toBe(true); expect(inputElement.disabled).toBe(true); testComponent.isDisabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.disabled).toBe(false); expect(inputElement.tabIndex).toBe(0); expect(inputElement.disabled).toBe(false); })); it('should not toggle `checked` state upon interation while disabled', () => { testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); checkboxNativeElement.click(); expect(checkboxInstance.checked).toBe(false); }); it('should overwrite indeterminate state when clicked', fakeAsync(() => { testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); inputElement.click(); fixture.detectChanges(); // Flush the microtasks because the indeterminate state will be updated in the next tick. flush(); expect(checkboxInstance.checked).toBe(true); expect(checkboxInstance.indeterminate).toBe(false); })); it('should preserve the user-provided id', fakeAsync(() => { expect(checkboxNativeElement.id).toBe('simple-check'); expect(inputElement.id).toBe('simple-check-input'); })); it('should generate a unique id for the checkbox input if no id is set', fakeAsync(() => { testComponent.checkboxId = null; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxInstance.inputId).toMatch(/mat-mdc-checkbox-\w+\d+/); expect(inputElement.id).toBe(checkboxInstance.inputId); })); it('should project the checkbox content into the label element', fakeAsync(() => { let label = checkboxNativeElement.querySelector('label'); expect(label.textContent!.trim()).toBe('Simple checkbox'); })); it('should make the host element a tab stop', fakeAsync(() => { expect(inputElement.tabIndex).toBe(0); })); it('should add a css class to position the label before the checkbox', fakeAsync(() => { testComponent.labelPos = 'before'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.querySelector('.mdc-form-field')!.classList).toContain( 'mdc-form-field--align-end', ); })); it('should trigger the click once when clicking on the ', fakeAsync(() => { spyOn(testComponent, 'onCheckboxClick'); expect(inputElement.checked).toBe(false); inputElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1); })); it('should trigger the click event once when clicking on the label', fakeAsync(() => { // By default, when clicking on a label element, a generated click will be dispatched // on the associated input element. // Since we're using a label element and a visual hidden input, this behavior can led // to an issue, where the click events on the checkbox are getting executed twice. spyOn(testComponent, 'onCheckboxClick'); expect(inputElement.checked).toBe(false); labelElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1); })); it('should trigger a change event when the native input does', fakeAsync(() => { spyOn(testComponent, 'onCheckboxChange'); expect(inputElement.checked).toBe(false); labelElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(testComponent.onCheckboxChange).toHaveBeenCalledTimes(1); })); it('should not trigger the change event by changing the native value', fakeAsync(() => { spyOn(testComponent, 'onCheckboxChange'); expect(inputElement.checked).toBe(false); testComponent.isChecked = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(testComponent.onCheckboxChange).not.toHaveBeenCalled(); })); it('should keep the view in sync if the `checked` value changes inside the `change` listener', fakeAsync(() => { spyOn(testComponent, 'onCheckboxChange').and.callFake(() => { checkboxInstance.checked = false; }); labelElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(false); })); it('should forward the required attribute', fakeAsync(() => { testComponent.isRequired = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputElement.required).toBe(true); testComponent.isRequired = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputElement.required).toBe(false); })); it('should focus on underlying input element when focus() is called', fakeAsync(() => { expect(document.activeElement).not.toBe(inputElement); checkboxInstance.focus(); fixture.detectChanges(); expect(document.activeElement).toBe(inputElement); })); it('should focus underlying input element when the touch target is clicked', fakeAsync(() => { const touchTarget = checkboxElement.querySelector( '.mat-mdc-checkbox-touch-target', ) as HTMLElement; expect(document.activeElement).not.toBe(inputElement); touchTarget.click(); fixture.detectChanges(); flush(); expect(document.activeElement).toBe(inputElement); })); it('should forward the value to input element', fakeAsync(() => { testComponent.checkboxValue = 'basic_checkbox'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputElement.value).toBe('basic_checkbox'); })); it('should remove the SVG checkmark from the tab order', fakeAsync(() => { expect(checkboxNativeElement.querySelector('svg')!.getAttribute('focusable')).toBe('false'); })); it('should be able to mark a checkbox as disabled while keeping it interactive', fakeAsync(() => { testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList).not.toContain( 'mat-mdc-checkbox-disabled-interactive', ); expect(inputElement.hasAttribute('aria-disabled')).toBe(false); expect(inputElement.tabIndex).toBe(-1); expect(inputElement.disabled).toBe(true); testComponent.disabledInteractive = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList).toContain('mat-mdc-checkbox-disabled-interactive'); expect(inputElement.getAttribute('aria-disabled')).toBe('true'); expect(inputElement.tabIndex).toBe(0); expect(inputElement.disabled).toBe(false); })); it('should not change the checked state if disabled and interactive', fakeAsync(() => { testComponent.isDisabled = testComponent.disabledInteractive = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputElement.checked).toBe(false); inputElement.click(); fixture.detectChanges(); expect(inputElement.checked).toBe(false); })); describe('ripple elements', () => { it('should show ripples on label mousedown', fakeAsync(() => { const rippleSelector = '.mat-ripple-element:not(.mat-checkbox-persistent-ripple)'; expect(checkboxNativeElement.querySelector(rippleSelector)).toBeFalsy(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(1); flush(); })); it('should not show ripples when disabled', fakeAsync(() => { const rippleSelector = '.mat-ripple-element:not(.mat-checkbox-persistent-ripple)'; testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(0); flush(); testComponent.isDisabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(1); flush(); })); it('should remove ripple if matRippleDisabled input is set', fakeAsync(() => { const rippleSelector = '.mat-ripple-element:not(.mat-checkbox-persistent-ripple)'; testComponent.disableRipple = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(0); flush(); testComponent.disableRipple = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(checkboxElement, 'mousedown'); dispatchFakeEvent(checkboxElement, 'mouseup'); checkboxElement.click(); expect(checkboxNativeElement.querySelectorAll(rippleSelector).length).toBe(1); flush(); })); }); describe('color behaviour', () => { it('should apply class based on color attribute', fakeAsync(() => { testComponent.checkboxColor = 'primary'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList.contains('mat-primary')).toBe(true); testComponent.checkboxColor = 'accent'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList.contains('mat-accent')).toBe(true); })); it('should not clear previous defined classes', fakeAsync(() => { checkboxNativeElement.classList.add('custom-class'); testComponent.checkboxColor = 'primary'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList.contains('mat-primary')).toBe(true); expect(checkboxNativeElement.classList.contains('custom-class')).toBe(true); testComponent.checkboxColor = 'accent'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList.contains('mat-primary')).toBe(false); expect(checkboxNativeElement.classList.contains('mat-accent')).toBe(true); expect(checkboxNativeElement.classList.contains('custom-class')).toBe(true); })); it('should default to accent if no color is passed in', fakeAsync(() => { testComponent.checkboxColor = undefined; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(checkboxNativeElement.classList).toContain('mat-accent'); })); }); describe(`when MAT_CHECKBOX_CLICK_ACTION is 'check'`, () => { beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule, SingleCheckbox], providers: [{provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: {clickAction: 'check'}}], }); fixture = createComponent(SingleCheckbox); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement; labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement; }); it('should not set `indeterminate` to false on click if check is set', fakeAsync(() => { testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); inputElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(inputElement.indeterminate).toBe(true); })); }); describe(`when MAT_CHECKBOX_CLICK_ACTION is 'noop'`, () => { beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule, SingleCheckbox], providers: [{provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: {clickAction: 'noop'}}], }); fixture = createComponent(SingleCheckbox); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement; labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement; }); it('should not change `indeterminate` on click if noop is set', fakeAsync(() => { testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); inputElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate).toBe(true); })); it(`should not change 'checked' or 'indeterminate' on click if noop is set`, fakeAsync(() => { testComponent.isChecked = true; testComponent.isIndeterminate = true; fixture.changeDetectorRef.markForCheck(); inputElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(true); expect(inputElement.indeterminate).toBe(true); testComponent.isChecked = false; fixture.changeDetectorRef.markForCheck(); inputElement.click(); fixture.detectChanges(); flush(); expect(inputElement.checked).toBe(false); expect(inputElement.indeterminate) .withContext('indeterminate should not change') .toBe(true); })); }); it('should have a focus indicator', () => { const checkboxRippleNativeElement = checkboxNativeElement.querySelector( '.mat-mdc-checkbox-ripple', )!; expect(checkboxRippleNativeElement.classList.contains('mat-focus-indicator')).toBe(true); }); }); describe('with change event and no initial value', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let checkboxInstance: MatCheckbox; let testComponent: CheckboxWithChangeEvent; let inputElement: HTMLInputElement; let labelElement: HTMLLabelElement; beforeEach(() => { fixture = createComponent(CheckboxWithChangeEvent); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input'); labelElement = checkboxNativeElement.querySelector('label'); }); it('should emit the event to the change observable', fakeAsync(() => { let changeSpy = jasmine.createSpy('onChangeObservable'); checkboxInstance.change.subscribe(changeSpy); fixture.detectChanges(); expect(changeSpy).not.toHaveBeenCalled(); // When changing the native `checked` property the checkbox will not fire a change event, // because the element is not focused and it's not the native behavior of the input // element. labelElement.click(); fixture.detectChanges(); flush(); expect(changeSpy).toHaveBeenCalledTimes(1); })); it('should not emit a DOM event to the change output', fakeAsync(() => { fixture.detectChanges(); expect(testComponent.lastEvent).toBeUndefined(); // Trigger the click on the inputElement, because the input will probably // emit a DOM event to the change output. inputElement.click(); fixture.detectChanges(); flush(); // We're checking the arguments type / emitted value to be a boolean, because sometimes the // emitted value can be a DOM Event, which is not valid. // See angular/angular#4059 expect(testComponent.lastEvent.checked).toBe(true); })); }); describe('aria handling', () => { it('should use the provided aria-label', fakeAsync(() => { fixture = createComponent(CheckboxWithAriaLabel); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; const checkboxNativeElement = checkboxDebugElement.nativeElement; const inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-label')).toBe('Super effective'); })); it('should not set the aria-label attribute if no value is provided', fakeAsync(() => { fixture = createComponent(SingleCheckbox); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('input').hasAttribute('aria-label')).toBe(false); })); it('should use the provided aria-labelledby', fakeAsync(() => { fixture = createComponent(CheckboxWithAriaLabelledby); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; const checkboxNativeElement = checkboxDebugElement.nativeElement; const inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-labelledby')).toBe('some-id'); })); it('should not assign aria-labelledby if none is provided', fakeAsync(() => { fixture = createComponent(SingleCheckbox); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; const checkboxNativeElement = checkboxDebugElement.nativeElement; const inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-labelledby')).toBe(null); })); it('should clear the static aria attributes from the host node', () => { fixture = createComponent(CheckboxWithStaticAriaAttributes); const checkbox = fixture.debugElement.query(By.directive(MatCheckbox))!.nativeElement; fixture.detectChanges(); expect(checkbox.hasAttribute('aria')).toBe(false); expect(checkbox.hasAttribute('aria-labelledby')).toBe(false); }); }); describe('with provided aria-describedby ', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let inputElement: HTMLInputElement; it('should use the provided aria-describedby', () => { fixture = createComponent(CheckboxWithAriaDescribedby); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-describedby')).toBe('some-id'); }); it('should not assign aria-describedby if none is provided', () => { fixture = createComponent(SingleCheckbox); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-describedby')).toBe(null); }); }); describe('with provided aria-expanded', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let inputElement: HTMLInputElement; it('should use the provided postive aria-expanded', () => { fixture = createComponent(CheckboxWithPositiveAriaExpanded); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); it('should use the provided negative aria-expanded', () => { fixture = createComponent(CheckboxWithNegativeAriaExpanded); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); it('should not assign aria-expanded if none is provided', () => { fixture = createComponent(SingleCheckbox); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-expanded')).toBe(null); }); }); describe('with provided aria-controls', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let inputElement: HTMLInputElement; it('should use the provided aria-controls', () => { fixture = createComponent(CheckboxWithAriaControls); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-controls')).toBe('some-id'); }); it('should not assign aria-controls if none is provided', () => { fixture = createComponent(SingleCheckbox); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-controls')).toBe(null); }); }); describe('with provided aria-owns', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let inputElement: HTMLInputElement; it('should use the provided aria-owns', () => { fixture = createComponent(CheckboxWithAriaOwns); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-owns')).toBe('some-id'); }); it('should not assign aria-owns if none is provided', () => { fixture = createComponent(SingleCheckbox); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); fixture.detectChanges(); expect(inputElement.getAttribute('aria-owns')).toBe(null); }); }); describe('with provided tabIndex', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let testComponent: CheckboxWithTabIndex; let inputElement: HTMLInputElement; beforeEach(() => { fixture = createComponent(CheckboxWithTabIndex); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; inputElement = checkboxNativeElement.querySelector('input'); }); it('should preserve any given tabIndex', fakeAsync(() => { expect(inputElement.tabIndex).toBe(7); })); it('should preserve given tabIndex when the checkbox is disabled then enabled', fakeAsync(() => { testComponent.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.customTabIndex = 13; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.isDisabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(inputElement.tabIndex).toBe(13); })); }); describe('with native tabindex attribute', () => { it('should properly detect native tabindex attribute', fakeAsync(() => { fixture = createComponent(CheckboxWithTabindexAttr); fixture.detectChanges(); const checkbox = fixture.debugElement.query(By.directive(MatCheckbox))! .componentInstance as MatCheckbox; expect(checkbox.tabIndex) .withContext('Expected tabIndex property to have been set based on the native attribute') .toBe(5); })); it('should clear the tabindex attribute from the host element', fakeAsync(() => { fixture = createComponent(CheckboxWithTabindexAttr); fixture.detectChanges(); const checkbox = fixture.debugElement.query(By.directive(MatCheckbox))!.nativeElement; expect(checkbox.getAttribute('tabindex')).toBeFalsy(); })); }); describe('with multiple checkboxes', () => { beforeEach(() => { fixture = createComponent(MultipleCheckboxes); fixture.detectChanges(); }); it('should assign a unique id to each checkbox', fakeAsync(() => { let [firstId, secondId] = fixture.debugElement .queryAll(By.directive(MatCheckbox)) .map(debugElement => debugElement.nativeElement.querySelector('input').id); expect(firstId).toMatch(/mat-mdc-checkbox-\w+\d+-input/); expect(secondId).toMatch(/mat-mdc-checkbox-\w+\d+-input/); expect(firstId).not.toEqual(secondId); })); }); describe('with ngModel', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; let checkboxInstance: MatCheckbox; let inputElement: HTMLInputElement; let ngModel: NgModel; beforeEach(() => { fixture = createComponent(CheckboxWithNgModel); fixture.componentInstance.isRequired = false; fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input'); ngModel = checkboxDebugElement.injector.get(NgModel); }); it('should be pristine, untouched, and valid initially', fakeAsync(() => { expect(ngModel.valid).toBe(true); expect(ngModel.pristine).toBe(true); expect(ngModel.touched).toBe(false); })); it('should have correct control states after interaction', fakeAsync(() => { inputElement.click(); fixture.detectChanges(); // Flush the timeout that is being created whenever a `click` event has been fired by // the underlying input. flush(); // After the value change through interaction, the control should be dirty, but remain // untouched as long as the focus is still on the underlying input. expect(ngModel.pristine).toBe(false); expect(ngModel.touched).toBe(false); // If the input element loses focus, the control should remain dirty but should // also turn touched. dispatchFakeEvent(inputElement, 'blur'); fixture.detectChanges(); flush(); expect(ngModel.pristine).toBe(false); expect(ngModel.touched).toBe(true); })); it('should mark the element as touched on blur when inside an OnPush parent', fakeAsync(() => { fixture.destroy(); TestBed.resetTestingModule(); fixture = createComponent(CheckboxWithNgModelAndOnPush); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxNativeElement = checkboxDebugElement.nativeElement; checkboxInstance = checkboxDebugElement.componentInstance; inputElement = checkboxNativeElement.querySelector('input'); ngModel = checkboxDebugElement.injector.get(NgModel); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxNativeElement.classList).not.toContain('ng-touched'); dispatchFakeEvent(inputElement, 'blur'); fixture.detectChanges(); flushMicrotasks(); fixture.detectChanges(); expect(checkboxNativeElement.classList).toContain('ng-touched'); })); it('should not throw an error when disabling while focused', fakeAsync(() => { expect(() => { // Focus the input element because after disabling, the `blur` event should automatically // fire and not result in a changed after checked exception. Related: #12323 inputElement.focus(); fixture.componentInstance.isDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); }).not.toThrow(); })); it('should toggle checked state on click', fakeAsync(() => { expect(checkboxInstance.checked).toBe(false); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(true); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(false); })); it('should validate with RequiredTrue validator', fakeAsync(() => { fixture.componentInstance.isRequired = true; inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(true); expect(ngModel.valid).toBe(true); inputElement.click(); fixture.detectChanges(); flush(); expect(checkboxInstance.checked).toBe(false); expect(ngModel.valid).toBe(false); })); it('should update the ngModel value when using the `toggle` method', fakeAsync(() => { const checkbox = fixture.debugElement.query(By.directive(MatCheckbox)).componentInstance; expect(fixture.componentInstance.isGood).toBe(false); checkbox.toggle(); fixture.detectChanges(); expect(fixture.componentInstance.isGood).toBe(true); })); }); describe('with name attribute', () => { beforeEach(() => { fixture = createComponent(CheckboxWithNameAttribute); fixture.detectChanges(); }); it('should forward name value to input element', fakeAsync(() => { let checkboxElement = fixture.debugElement.query(By.directive(MatCheckbox))!; let inputElement = checkboxElement.nativeElement.querySelector('input'); expect(inputElement.getAttribute('name')).toBe('test-name'); })); }); describe('with form control', () => { let checkboxDebugElement: DebugElement; let checkboxInstance: MatCheckbox; let testComponent: CheckboxWithFormControl; let inputElement: HTMLInputElement; beforeEach(() => { fixture = createComponent(CheckboxWithFormControl); fixture.detectChanges(); checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxInstance = checkboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; inputElement = checkboxDebugElement.nativeElement.querySelector('input'); }); it('should toggle the disabled state', fakeAsync(() => { expect(checkboxInstance.disabled).toBe(false); testComponent.formControl.disable(); fixture.detectChanges(); expect(checkboxInstance.disabled).toBe(true); expect(inputElement.disabled).toBe(true); testComponent.formControl.enable(); fixture.detectChanges(); expect(checkboxInstance.disabled).toBe(false); expect(inputElement.disabled).toBe(false); })); }); describe('without label', () => { let checkboxInnerContainer: HTMLElement; beforeEach(() => { fixture = createComponent(CheckboxWithoutLabel); const checkboxDebugEl = fixture.debugElement.query(By.directive(MatCheckbox))!; checkboxInnerContainer = checkboxDebugEl.query(By.css('.mdc-form-field'))!.nativeElement; }); it('should not add the "name" attribute if it is not passed in', fakeAsync(() => { fixture.detectChanges(); expect(checkboxInnerContainer.querySelector('input')!.hasAttribute('name')).toBe(false); })); it('should not add the "value" attribute if it is not passed in', fakeAsync(() => { fixture.detectChanges(); expect(checkboxInnerContainer.querySelector('input')!.hasAttribute('value')).toBe(false); })); }); }); describe('MatCheckboxDefaultOptions', () => { describe('when MAT_CHECKBOX_DEFAULT_OPTIONS overridden', () => { function configure(defaults: MatCheckboxDefaultOptions) { TestBed.configureTestingModule({ imports: [MatCheckboxModule, FormsModule, SingleCheckbox, SingleCheckbox], providers: [{provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: defaults}], }); } it('should override default color in component', () => { configure({color: 'primary'}); const fixture: ComponentFixture = TestBed.createComponent(SingleCheckbox); fixture.detectChanges(); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; expect(checkboxDebugElement.nativeElement.classList).toContain('mat-primary'); }); it('should not override explicit input bindings', () => { configure({color: 'primary'}); const fixture: ComponentFixture = TestBed.createComponent(SingleCheckbox); fixture.componentInstance.checkboxColor = 'warn'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; expect(checkboxDebugElement.nativeElement.classList).not.toContain('mat-primary'); expect(checkboxDebugElement.nativeElement.classList).toContain('mat-warn'); expect(checkboxDebugElement.nativeElement.classList).toContain('mat-warn'); }); it('should default to accent if config does not specify color', () => { configure({clickAction: 'noop'}); const fixture: ComponentFixture = TestBed.createComponent(SingleCheckbox); fixture.componentInstance.checkboxColor = undefined; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; expect(checkboxDebugElement.nativeElement.classList).toContain('mat-accent'); }); }); }); /** Simple component for testing a single checkbox. */ @Component({ template: `
Simple checkbox
`, standalone: true, imports: [MatCheckbox], }) class SingleCheckbox { labelPos: 'before' | 'after' = 'after'; isChecked = false; isRequired = false; isIndeterminate = false; isDisabled = false; disableRipple = false; parentElementClicked = false; parentElementKeyedUp = false; disabledInteractive = false; checkboxId: string | null = 'simple-check'; checkboxColor: ThemePalette = 'primary'; checkboxValue: string = 'single_checkbox'; onCheckboxClick: (event?: Event) => void = () => {}; onCheckboxChange: (event?: MatCheckboxChange) => void = () => {}; } /** Simple component for testing an MatCheckbox with required ngModel. */ @Component({ template: `Be good`, standalone: true, imports: [MatCheckbox, FormsModule], }) class CheckboxWithNgModel { isGood = false; isRequired = true; isDisabled = false; } @Component({ template: `Be good`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MatCheckbox, FormsModule], }) class CheckboxWithNgModelAndOnPush extends CheckboxWithNgModel {} /** Simple test component with multiple checkboxes. */ @Component({ template: ` Option 1 Option 2 `, standalone: true, imports: [MatCheckbox], }) class MultipleCheckboxes {} /** Simple test component with tabIndex */ @Component({ template: ` `, standalone: true, imports: [MatCheckbox], }) class CheckboxWithTabIndex { customTabIndex: number = 7; isDisabled: boolean = false; } /** Simple test component with an aria-label set. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithAriaLabel {} /** Simple test component with an aria-label set. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithAriaLabelledby {} /** Simple test component with an aria-describedby set. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithAriaDescribedby {} /** Simple test component with an aria-expanded set with true. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithPositiveAriaExpanded {} /** Simple test component with an aria-expanded set with false. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithNegativeAriaExpanded {} /** Simple test component with an aria-controls set. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithAriaControls {} /** Simple test component with an aria-owns set. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithAriaOwns {} /** Simple test component with name attribute */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithNameAttribute {} /** Simple test component with change event */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithChangeEvent { lastEvent: MatCheckboxChange; } /** Test component with reactive forms */ @Component({ template: ``, standalone: true, imports: [MatCheckbox, ReactiveFormsModule], }) class CheckboxWithFormControl { formControl = new FormControl(false); } /** Test component without label */ @Component({ template: `{{ label }}`, standalone: true, imports: [MatCheckbox], }) class CheckboxWithoutLabel { label: string; } /** Test component with the native tabindex attribute. */ @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithTabindexAttr {} @Component({ template: ``, standalone: true, imports: [MatCheckbox], }) class CheckboxWithStaticAriaAttributes {}