sass-references/angular-material/material/checkbox/checkbox.spec.ts

1412 lines
50 KiB
TypeScript

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<any>;
function createComponent<T>(componentType: Type<T>) {
TestBed.configureTestingModule({
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule, componentType],
});
return TestBed.createComponent<T>(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 = <HTMLInputElement>checkboxNativeElement.querySelector('input');
labelElement = <HTMLLabelElement>checkboxNativeElement.querySelector('label');
checkboxElement = <HTMLLabelElement>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 = <HTMLLabelElement>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 <input/>', 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 = <HTMLInputElement>checkboxNativeElement.querySelector('input');
labelElement = <HTMLLabelElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>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 = <HTMLInputElement>checkboxNativeElement.querySelector('input');
ngModel = checkboxDebugElement.injector.get<NgModel>(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 = <HTMLInputElement>checkboxNativeElement.querySelector('input');
ngModel = checkboxDebugElement.injector.get<NgModel>(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 = <HTMLInputElement>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 = <HTMLInputElement>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<SingleCheckbox> = 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<SingleCheckbox> = 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<SingleCheckbox> = 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: `
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true" (click)="onCheckboxClick($event)">
<mat-checkbox
[id]="checkboxId"
[required]="isRequired"
[labelPosition]="labelPos"
[checked]="isChecked"
[(indeterminate)]="isIndeterminate"
[disabled]="isDisabled"
[color]="checkboxColor"
[disableRipple]="disableRipple"
[value]="checkboxValue"
[disabledInteractive]="disabledInteractive"
(change)="onCheckboxChange($event)">
Simple checkbox
</mat-checkbox>
</div>`,
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: `<mat-checkbox [required]="isRequired" [(ngModel)]="isGood"
[disabled]="isDisabled">Be good</mat-checkbox>`,
standalone: true,
imports: [MatCheckbox, FormsModule],
})
class CheckboxWithNgModel {
isGood = false;
isRequired = true;
isDisabled = false;
}
@Component({
template: `<mat-checkbox [required]="isRequired" [(ngModel)]="isGood">Be good</mat-checkbox>`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatCheckbox, FormsModule],
})
class CheckboxWithNgModelAndOnPush extends CheckboxWithNgModel {}
/** Simple test component with multiple checkboxes. */
@Component({
template: `
<mat-checkbox>Option 1</mat-checkbox>
<mat-checkbox>Option 2</mat-checkbox>
`,
standalone: true,
imports: [MatCheckbox],
})
class MultipleCheckboxes {}
/** Simple test component with tabIndex */
@Component({
template: `
<mat-checkbox
[tabIndex]="customTabIndex"
[disabled]="isDisabled">
</mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithTabIndex {
customTabIndex: number = 7;
isDisabled: boolean = false;
}
/** Simple test component with an aria-label set. */
@Component({
template: `<mat-checkbox aria-label="Super effective"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithAriaLabel {}
/** Simple test component with an aria-label set. */
@Component({
template: `<mat-checkbox aria-labelledby="some-id"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithAriaLabelledby {}
/** Simple test component with an aria-describedby set. */
@Component({
template: `<mat-checkbox aria-describedby="some-id"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithAriaDescribedby {}
/** Simple test component with an aria-expanded set with true. */
@Component({
template: `<mat-checkbox aria-expanded="true"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithPositiveAriaExpanded {}
/** Simple test component with an aria-expanded set with false. */
@Component({
template: `<mat-checkbox aria-expanded="false"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithNegativeAriaExpanded {}
/** Simple test component with an aria-controls set. */
@Component({
template: `<mat-checkbox aria-controls="some-id"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithAriaControls {}
/** Simple test component with an aria-owns set. */
@Component({
template: `<mat-checkbox aria-owns="some-id"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithAriaOwns {}
/** Simple test component with name attribute */
@Component({
template: `<mat-checkbox name="test-name"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithNameAttribute {}
/** Simple test component with change event */
@Component({
template: `<mat-checkbox (change)="lastEvent = $event"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithChangeEvent {
lastEvent: MatCheckboxChange;
}
/** Test component with reactive forms */
@Component({
template: `<mat-checkbox [formControl]="formControl"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox, ReactiveFormsModule],
})
class CheckboxWithFormControl {
formControl = new FormControl(false);
}
/** Test component without label */
@Component({
template: `<mat-checkbox>{{ label }}</mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithoutLabel {
label: string;
}
/** Test component with the native tabindex attribute. */
@Component({
template: `<mat-checkbox tabindex="5"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithTabindexAttr {}
@Component({
template: `<mat-checkbox aria-label="Checkbox" aria-labelledby="something"></mat-checkbox>`,
standalone: true,
imports: [MatCheckbox],
})
class CheckboxWithStaticAriaAttributes {}