ipsos-components
Version:
Material Design components for Angular
1,275 lines (963 loc) • 152 kB
text/typescript
import {Directionality} from '@angular/cdk/bidi';
import {DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
import {OverlayContainer} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
wrappedErrorMessage,
} from '@angular/cdk/testing';
import {
ChangeDetectionStrategy,
Component,
DebugElement,
OnInit,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core';
import {
async,
ComponentFixture,
fakeAsync,
flush,
inject,
TestBed,
tick,
} from '@angular/core/testing';
import {
ControlValueAccessor,
FormControl,
FormGroup,
FormGroupDirective,
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {
ErrorStateMatcher,
FloatLabelType,
MAT_LABEL_GLOBAL_OPTIONS,
MatOption,
} from '@angular/material/core';
import {MatFormFieldModule} from '@angular/material/form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {map} from 'rxjs/operators/map';
import {Subject} from 'rxjs/Subject';
import {MatSelectModule} from './index';
import {MatSelect} from './select';
import {
getMatSelectDynamicMultipleError,
getMatSelectNonArrayValueError,
getMatSelectNonFunctionValueError,
} from './select-errors';
/** The debounce interval when typing letters to select an option. */
const LETTER_KEY_DEBOUNCE_INTERVAL = 200;
const platform = new Platform();
describe('MatSelect', () => {
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
let dir: {value: 'ltr'|'rtl'};
let scrolledSubject = new Subject();
let viewportRuler: ViewportRuler;
// Providers used for all mat-select tests
const commonProviders = [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}},
{
provide: ScrollDispatcher, useFactory: () => ({
scrolled: () => scrolledSubject.asObservable(),
}),
},
];
// NgModule imports used for all mat-select tests.
const commonModuleImports = [
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
FormsModule,
NoopAnimationsModule,
];
/**
* Configures the test module for MatSelect with the given declarations. This is broken out so
* that we're only compiling the necessary test components for each test in order to speed up
* overall test time.
* @param declarations Components to declare for this block
*/
function configureMatSelectTestingModule(declarations) {
TestBed.configureTestingModule({
imports: commonModuleImports,
declarations: declarations,
providers: commonProviders,
}).compileComponents();
inject([OverlayContainer], (oc: OverlayContainer) => {
overlayContainer = oc;
overlayContainerElement = oc.getContainerElement();
})();
}
afterEach(() => {
overlayContainer.ngOnDestroy();
});
describe('core', () => {
beforeEach(async(() => {
configureMatSelectTestingModule([
BasicSelect,
MultiSelect,
SelectWithGroups,
]);
}));
describe('accessibility', () => {
describe('for select', () => {
let fixture: ComponentFixture<BasicSelect>;
let select: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
select = fixture.debugElement.query(By.css('mat-select')).nativeElement;
}));
it('should set the role of the select to listbox', fakeAsync(() => {
expect(select.getAttribute('role')).toEqual('listbox');
}));
it('should set the aria label of the select to the placeholder', fakeAsync(() => {
expect(select.getAttribute('aria-label')).toEqual('Food');
}));
it('should support setting a custom aria-label', fakeAsync(() => {
fixture.componentInstance.ariaLabel = 'Custom Label';
fixture.detectChanges();
expect(select.getAttribute('aria-label')).toEqual('Custom Label');
}));
it('should not set an aria-label if aria-labelledby is specified', fakeAsync(() => {
fixture.componentInstance.ariaLabelledby = 'myLabelId';
fixture.detectChanges();
expect(select.getAttribute('aria-label')).toBeFalsy('Expected no aria-label to be set.');
expect(select.getAttribute('aria-labelledby')).toBe('myLabelId');
}));
it('should not have aria-labelledby in the DOM if it`s not specified', fakeAsync(() => {
fixture.detectChanges();
expect(select.hasAttribute('aria-labelledby')).toBeFalsy();
}));
it('should set the tabindex of the select to 0 by default', fakeAsync(() => {
expect(select.getAttribute('tabindex')).toEqual('0');
}));
it('should be able to override the tabindex', fakeAsync(() => {
fixture.componentInstance.tabIndexOverride = 3;
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toBe('3');
}));
it('should set aria-required for required selects', fakeAsync(() => {
expect(select.getAttribute('aria-required'))
.toEqual('false', `Expected aria-required attr to be false for normal selects.`);
fixture.componentInstance.isRequired = true;
fixture.detectChanges();
expect(select.getAttribute('aria-required'))
.toEqual('true', `Expected aria-required attr to be true for required selects.`);
}));
it('should set the mat-select-required class for required selects', fakeAsync(() => {
expect(select.classList).not.toContain(
'mat-select-required', `Expected the mat-select-required class not to be set.`);
fixture.componentInstance.isRequired = true;
fixture.detectChanges();
expect(select.classList).toContain(
'mat-select-required', `Expected the mat-select-required class to be set.`);
}));
it('should set aria-invalid for selects that are invalid and touched', fakeAsync(() => {
expect(select.getAttribute('aria-invalid'))
.toEqual('false', `Expected aria-invalid attr to be false for valid selects.`);
fixture.componentInstance.isRequired = true;
fixture.componentInstance.control.markAsTouched();
fixture.detectChanges();
expect(select.getAttribute('aria-invalid'))
.toEqual('true', `Expected aria-invalid attr to be true for invalid selects.`);
}));
it('should set aria-disabled for disabled selects', fakeAsync(() => {
expect(select.getAttribute('aria-disabled')).toEqual('false');
fixture.componentInstance.control.disable();
fixture.detectChanges();
expect(select.getAttribute('aria-disabled')).toEqual('true');
}));
it('should set the tabindex of the select to -1 if disabled', fakeAsync(() => {
fixture.componentInstance.control.disable();
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toEqual('-1');
fixture.componentInstance.control.enable();
fixture.detectChanges();
expect(select.getAttribute('tabindex')).toEqual('0');
}));
it('should select options via the arrow keys on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).toBeFalsy('Expected no initial value.');
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(options[0].selected).toBe(true, 'Expected first option to be selected.');
expect(formControl.value).toBe(options[0].value,
'Expected value from first option to have been set on the model.');
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
// Note that the third option is skipped, because it is disabled.
expect(options[3].selected).toBe(true, 'Expected fourth option to be selected.');
expect(formControl.value).toBe(options[3].value,
'Expected value from fourth option to have been set on the model.');
dispatchKeyboardEvent(select, 'keydown', UP_ARROW);
expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
expect(formControl.value).toBe(options[1].value,
'Expected value from second option to have been set on the model.');
}));
it('should be able to select options by typing on a closed select', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
const options = fixture.componentInstance.options.toArray();
expect(formControl.value).toBeFalsy('Expected no initial value.');
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
tick(200);
expect(options[1].selected).toBe(true, 'Expected second option to be selected.');
expect(formControl.value).toBe(options[1].value,
'Expected value from second option to have been set on the model.');
dispatchEvent(select, createKeyboardEvent('keydown', 69, undefined, 'e'));
tick(200);
expect(options[5].selected).toBe(true, 'Expected sixth option to be selected.');
expect(formControl.value).toBe(options[5].value,
'Expected value from sixth option to have been set on the model.');
}));
it('should open the panel when pressing the arrow keys on a closed multiple select',
fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');
const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.');
}));
it('should do nothing when typing on a closed multi-select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
const instance = multiFixture.componentInstance;
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
const initialValue = instance.control.value;
expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.');
dispatchEvent(select, createKeyboardEvent('keydown', 80, undefined, 'p'));
expect(instance.select.panelOpen).toBe(false, 'Expected panel to stay closed.');
expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.');
}));
it('should do nothing if the key manager did not change the active item', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
expect(formControl.value).toBeNull('Expected form control value to be empty.');
expect(formControl.pristine).toBe(true, 'Expected form control to be clean.');
dispatchKeyboardEvent(select, 'keydown', 16); // Press a random key.
expect(formControl.value).toBeNull('Expected form control value to stay empty.');
expect(formControl.pristine).toBe(true, 'Expected form control to stay clean.');
}));
it('should continue from the selected option when the value is set programmatically',
fakeAsync(() => {
const formControl = fixture.componentInstance.control;
formControl.setValue('eggs-5');
fixture.detectChanges();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(formControl.value).toBe('pasta-6');
expect(fixture.componentInstance.options.toArray()[6].selected).toBe(true);
}));
it('should not shift focus when the selected options are updated programmatically ' +
'in a multi select', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
multiFixture.componentInstance.select.open();
multiFixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[3].focus();
expect(document.activeElement).toBe(options[3], 'Expected fourth option to be focused.');
multiFixture.componentInstance.control.setValue(['steak-0', 'sushi-7']);
multiFixture.detectChanges();
expect(document.activeElement)
.toBe(options[3], 'Expected fourth option to remain focused.');
}));
it('should not cycle through the options if the control is disabled', fakeAsync(() => {
const formControl = fixture.componentInstance.control;
formControl.setValue('eggs-5');
formControl.disable();
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(formControl.value).toBe('eggs-5', 'Expected value to remain unchaged.');
}));
it('should not wrap selection after reaching the end of the options', fakeAsync(() => {
const lastOption = fixture.componentInstance.options.last;
fixture.componentInstance.options.forEach(() => {
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
});
expect(lastOption.selected).toBe(true, 'Expected last option to be selected.');
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(lastOption.selected).toBe(true, 'Expected last option to stay selected.');
}));
it('should not open a multiple select when tabbing through', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
expect(multiFixture.componentInstance.select.panelOpen)
.toBe(false, 'Expected panel to be closed initially.');
dispatchKeyboardEvent(select, 'keydown', TAB);
expect(multiFixture.componentInstance.select.panelOpen)
.toBe(false, 'Expected panel to stay closed.');
}));
it('should prevent the default action when pressing space', fakeAsync(() => {
const event = dispatchKeyboardEvent(select, 'keydown', SPACE);
expect(event.defaultPrevented).toBe(true);
}));
it('should consider the selection a result of a user action when closed', fakeAsync(() => {
const option = fixture.componentInstance.options.first;
const spy = jasmine.createSpy('option selection spy');
const subscription =
option.onSelectionChange.pipe(map(e => e.isUserInput)).subscribe(spy);
dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
expect(spy).toHaveBeenCalledWith(true);
subscription.unsubscribe();
}));
it('should be able to focus the select trigger', fakeAsync(() => {
document.body.focus(); // ensure that focus isn't on the trigger already
fixture.componentInstance.select.focus();
expect(document.activeElement).toBe(select, 'Expected select element to be focused.');
}));
// Having `aria-hidden` on the trigger avoids issues where
// screen readers read out the wrong amount of options.
it('should set aria-hidden on the trigger element', fakeAsync(() => {
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
expect(trigger.getAttribute('aria-hidden'))
.toBe('true', 'Expected aria-hidden to be true when the select is open.');
}));
it('should set `aria-multiselectable` to true on multi-select instances', fakeAsync(() => {
fixture.destroy();
const multiFixture = TestBed.createComponent(MultiSelect);
multiFixture.detectChanges();
select = multiFixture.debugElement.query(By.css('mat-select')).nativeElement;
expect(select.getAttribute('aria-multiselectable')).toBe('true');
}));
it('should set aria-multiselectable false on single-selection instances', fakeAsync(() => {
expect(select.getAttribute('aria-multiselectable')).toBe('false');
}));
it('should set aria-activedescendant only while the panel is open', fakeAsync(() => {
fixture.componentInstance.control.setValue('chips-4');
fixture.detectChanges();
const host = fixture.debugElement.query(By.css('mat-select')).nativeElement;
expect(host.hasAttribute('aria-activedescendant'))
.toBe(false, 'Expected no aria-activedescendant on init.');
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll('mat-option');
expect(host.getAttribute('aria-activedescendant'))
.toBe(options[4].id, 'Expected aria-activedescendant to match the active option.');
fixture.componentInstance.select.close();
fixture.detectChanges();
flush();
expect(host.hasAttribute('aria-activedescendant'))
.toBe(false, 'Expected no aria-activedescendant when closed.');
}));
it('should set aria-activedescendant based on the focused option', fakeAsync(() => {
const host = fixture.debugElement.query(By.css('mat-select')).nativeElement;
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
const options = overlayContainerElement.querySelectorAll('mat-option');
expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id);
[1, 2, 3].forEach(() => {
dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
fixture.detectChanges();
});
expect(host.getAttribute('aria-activedescendant')).toBe(options[4].id);
dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(host.getAttribute('aria-activedescendant')).toBe(options[3].id);
}));
});
describe('for options', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let options: NodeListOf<HTMLElement>;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
trigger.click();
fixture.detectChanges();
options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
}));
it('should set the role of mat-option to option', fakeAsync(() => {
expect(options[0].getAttribute('role')).toEqual('option');
expect(options[1].getAttribute('role')).toEqual('option');
expect(options[2].getAttribute('role')).toEqual('option');
}));
it('should set aria-selected on each option', fakeAsync(() => {
expect(options[0].getAttribute('aria-selected')).toEqual('false');
expect(options[1].getAttribute('aria-selected')).toEqual('false');
expect(options[2].getAttribute('aria-selected')).toEqual('false');
options[1].click();
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
expect(options[0].getAttribute('aria-selected')).toEqual('false');
expect(options[1].getAttribute('aria-selected')).toEqual('true');
expect(options[2].getAttribute('aria-selected')).toEqual('false');
}));
it('should set the tabindex of each option according to disabled state', fakeAsync(() => {
expect(options[0].getAttribute('tabindex')).toEqual('0');
expect(options[1].getAttribute('tabindex')).toEqual('0');
expect(options[2].getAttribute('tabindex')).toEqual('-1');
}));
it('should set aria-disabled for disabled options', fakeAsync(() => {
expect(options[0].getAttribute('aria-disabled')).toEqual('false');
expect(options[1].getAttribute('aria-disabled')).toEqual('false');
expect(options[2].getAttribute('aria-disabled')).toEqual('true');
fixture.componentInstance.foods[2]['disabled'] = false;
fixture.detectChanges();
expect(options[0].getAttribute('aria-disabled')).toEqual('false');
expect(options[1].getAttribute('aria-disabled')).toEqual('false');
expect(options[2].getAttribute('aria-disabled')).toEqual('false');
}));
});
describe('for option groups', () => {
let fixture: ComponentFixture<SelectWithGroups>;
let trigger: HTMLElement;
let groups: NodeListOf<HTMLElement>;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(SelectWithGroups);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
trigger.click();
fixture.detectChanges();
groups =
overlayContainerElement.querySelectorAll('mat-optgroup') as NodeListOf<HTMLElement>;
}));
it('should set the appropriate role', fakeAsync(() => {
expect(groups[0].getAttribute('role')).toBe('group');
}));
it('should set the `aria-labelledby` attribute', fakeAsync(() => {
let group = groups[0];
let label = group.querySelector('label')!;
expect(label.getAttribute('id')).toBeTruthy('Expected label to have an id.');
expect(group.getAttribute('aria-labelledby'))
.toBe(label.getAttribute('id'), 'Expected `aria-labelledby` to match the label id.');
}));
it('should set the `aria-disabled` attribute if the group is disabled', fakeAsync(() => {
expect(groups[1].getAttribute('aria-disabled')).toBe('true');
}));
});
});
describe('overlay panel', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
}));
it('should not throw when attempting to open too early', () => {
// Create component and then immediately open without running change detection
fixture = TestBed.createComponent(BasicSelect);
expect(() => fixture.componentInstance.select.open()).not.toThrow();
});
it('should open the panel when trigger is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(overlayContainerElement.textContent).toContain('Steak');
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(overlayContainerElement.textContent).toContain('Tacos');
}));
it('should close the panel when an item is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should close the panel when a click occurs outside the panel', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const backdrop =
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
fixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toEqual('');
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should set the width of the overlay based on the trigger', fakeAsync(() => {
trigger.style.width = '200px';
trigger.click();
fixture.detectChanges();
flush();
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.minWidth).toBe('200px');
}));
it('should not attempt to open a select that does not have any options', fakeAsync(() => {
fixture.componentInstance.foods = [];
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should close the panel when tabbing out', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
dispatchKeyboardEvent(trigger, 'keydown', TAB);
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select.panelOpen).toBe(false);
}));
it('should focus the first option when pressing HOME', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const event = dispatchKeyboardEvent(trigger, 'keydown', HOME);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
expect(event.defaultPrevented).toBe(true);
}));
it('should focus the last option when pressing END', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const event = dispatchKeyboardEvent(trigger, 'keydown', END);
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
expect(event.defaultPrevented).toBe(true);
}));
it('should be able to set extra classes on the panel', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-select-panel') as HTMLElement;
expect(panel.classList).toContain('custom-one');
expect(panel.classList).toContain('custom-two');
}));
it('should prevent the default action when pressing SPACE on an option', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('mat-option')!;
const event = dispatchKeyboardEvent(option, 'keydown', SPACE);
expect(event.defaultPrevented).toBe(true);
}));
it('should prevent the default action when pressing ENTER on an option', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
const option = overlayContainerElement.querySelector('mat-option')!;
const event = dispatchKeyboardEvent(option, 'keydown', ENTER);
expect(event.defaultPrevented).toBe(true);
}));
it('should update disableRipple properly on each option', fakeAsync(() => {
const options = fixture.componentInstance.options.toArray();
expect(options.every(option => option.disableRipple === false))
.toBeTruthy('Expected all options to have disableRipple set to false initially.');
fixture.componentInstance.disableRipple = true;
fixture.detectChanges();
expect(options.every(option => option.disableRipple === true))
.toBeTruthy('Expected all options to have disableRipple set to true.');
}));
it('should not show ripples if they were disabled', fakeAsync(() => {
fixture.componentInstance.disableRipple = true;
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option')!;
dispatchFakeEvent(option, 'mousedown');
dispatchFakeEvent(option, 'mouseup');
expect(option.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));
});
describe('selection logic', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let formField: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
}));
it('should not float label if no option is selected', fakeAsync(() => {
expect(formField.classList.contains('mat-form-field-should-float'))
.toBe(false, 'Label should not be floating');
}));
it('should focus the first option if no option is selected', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(0);
}));
it('should select an option when it is clicked', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
let option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
trigger.click();
fixture.detectChanges();
flush();
option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
expect(option.classList).toContain('mat-selected');
expect(fixture.componentInstance.options.first.selected).toBe(true);
expect(fixture.componentInstance.select.selected)
.toBe(fixture.componentInstance.options.first);
}));
it('should deselect other options when one is selected', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
let options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
flush();
trigger.click();
fixture.detectChanges();
flush();
options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList).not.toContain('mat-selected');
expect(options[2].classList).not.toContain('mat-selected');
const optionInstances = fixture.componentInstance.options.toArray();
expect(optionInstances[1].selected).toBe(false);
expect(optionInstances[2].selected).toBe(false);
}));
it('should deselect other options when one is programmatically selected', fakeAsync(() => {
let control = fixture.componentInstance.control;
let foods = fixture.componentInstance.foods;
trigger.click();
fixture.detectChanges();
flush();
let options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
flush();
control.setValue(foods[1].value);
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[0].classList)
.not.toContain('mat-selected', 'Expected first option to no longer be selected');
expect(options[1].classList)
.toContain('mat-selected', 'Expected second option to be selected');
const optionInstances = fixture.componentInstance.options.toArray();
expect(optionInstances[0].selected)
.toBe(false, 'Expected first option to no longer be selected');
expect(optionInstances[1].selected)
.toBe(true, 'Expected second option to be selected');
}));
it('should remove selection if option has been removed', fakeAsync(() => {
let select = fixture.componentInstance.select;
trigger.click();
fixture.detectChanges();
flush();
let firstOption = overlayContainerElement.querySelectorAll('mat-option')[0] as HTMLElement;
firstOption.click();
fixture.detectChanges();
expect(select.selected).toBe(select.options.first, 'Expected first option to be selected.');
fixture.componentInstance.foods = [];
fixture.detectChanges();
flush();
expect(select.selected)
.toBeUndefined('Expected selection to be removed when option no longer exists.');
}));
it('should display the selected option in the trigger', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
const value = fixture.debugElement.query(By.css('.mat-select-value')).nativeElement;
expect(formField.classList.contains('mat-form-field-should-float'))
.toBe(true, 'Label should be floating');
expect(value.textContent).toContain('Steak');
}));
it('should focus the selected option if an option is selected', fakeAsync(() => {
// must wait for initial writeValue promise to finish
flush();
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
trigger.click();
fixture.detectChanges();
flush();
// must wait for animation to finish
fixture.detectChanges();
expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(1);
}));
it('should select an option that was added after initialization', fakeAsync(() => {
fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
trigger.click();
fixture.detectChanges();
flush();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[8].click();
fixture.detectChanges();
flush();
expect(trigger.textContent).toContain('Potatoes');
expect(fixture.componentInstance.select.selected)
.toBe(fixture.componentInstance.options.last);
}));
it('should not select disabled options', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[2].click();
fixture.detectChanges();
expect(fixture.componentInstance.select.panelOpen).toBe(true);
expect(options[2].classList).not.toContain('mat-selected');
expect(fixture.componentInstance.select.selected).toBeUndefined();
}));
it('should not select options inside a disabled group', fakeAsync(() => {
fixture.destroy();
const groupFixture = TestBed.createComponent(SelectWithGroups);
groupFixture.detectChanges();
groupFixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
groupFixture.detectChanges();
const disabledGroup = overlayContainerElement.querySelectorAll('mat-optgroup')[1];
const options = disabledGroup.querySelectorAll('mat-option');
(options[0] as HTMLElement).click();
groupFixture.detectChanges();
expect(groupFixture.componentInstance.select.panelOpen).toBe(true);
expect(options[0].classList).not.toContain('mat-selected');
expect(groupFixture.componentInstance.select.selected).toBeUndefined();
}));
it('should not throw if triggerValue accessed with no selected value', fakeAsync(() => {
expect(() => fixture.componentInstance.select.triggerValue).not.toThrow();
}));
});
describe('forms integration', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
}));
it('should take an initial view value with reactive forms', fakeAsync(() => {
fixture.componentInstance.control = new FormControl('pizza-1');
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent)
.toContain('Pizza', `Expected trigger to be populated by the control's initial value.`);
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
trigger.click();
fixture.detectChanges();
flush();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList)
.toContain('mat-selected',
`Expected option with the control's initial value to be selected.`);
}));
it('should set the view value from the form', fakeAsync(() => {
let value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent.trim()).toBe('Food');
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent)
.toContain('Pizza', `Expected trigger to be populated by the control's new value.`);
trigger.click();
fixture.detectChanges();
flush();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList).toContain(
'mat-selected', `Expected option with the control's new value to be selected.`);
}));
it('should update the form value when the view changes', fakeAsync(() => {
expect(fixture.componentInstance.control.value)
.toEqual(null, `Expected the control's value to be empty initially.`);
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.value)
.toEqual('steak-0', `Expected control's value to be set to the new option.`);
}));
it('should clear the selection when a nonexistent option value is selected', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
fixture.componentInstance.control.setValue('gibberish');
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent.trim())
.toBe('Food', `Expected trigger to show the placeholder.`);
expect(trigger.textContent)
.not.toContain('Pizza', `Expected trigger is cleared when option value is not found.`);
trigger.click();
fixture.detectChanges();
flush();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList)
.not.toContain('mat-selected', `Expected option w/ the old value not to be selected.`);
}));
it('should clear the selection when the control is reset', fakeAsync(() => {
fixture.componentInstance.control.setValue('pizza-1');
fixture.detectChanges();
fixture.componentInstance.control.reset();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('.mat-select-value'));
expect(value.nativeElement.textContent.trim())
.toBe('Food', `Expected trigger to show the placeholder.`);
expect(trigger.textContent)
.not.toContain('Pizza', `Expected trigger is cleared when option value is not found.`);
trigger.click();
fixture.detectChanges();
flush();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(options[1].classList)
.not.toContain('mat-selected', `Expected option w/ the old value not to be selected.`);
}));
it('should set the control to touched when the select is touched', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.toEqual(false, `Expected the control to start off as untouched.`);
trigger.click();
dispatchFakeEvent(trigger, 'blur');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.toEqual(false, `Expected the control to stay untouched when menu opened.`);
const backdrop =
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
dispatchFakeEvent(trigger, 'blur');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.touched)
.toEqual(true, `Expected the control to be touched as soon as focus left the select.`);
}));
it('should not set touched when a disabled select is touched', fakeAsync(() => {
expect(fixture.componentInstance.control.touched)
.toBe(false, 'Expected the control to start off as untouched.');
fixture.componentInstance.control.disable();
dispatchFakeEvent(trigger, 'blur');
expect(fixture.componentInstance.control.touched)
.toBe(false, 'Expected the control to stay untouched.');
}));
it('should set the control to dirty when the select value changes in DOM', fakeAsync(() => {
expect(fixture.componentInstance.control.dirty)
.toEqual(false, `Expected control to start out pristine.`);
trigger.click();
fixture.detectChanges();
flush();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
flush();
expect(fixture.componentInstance.control.dirty)
.toEqual(true, `Expected control to be dirty after value was changed by user.`);
}));
it('should not set the control to dirty when the value changes programmatically',
fakeAsync(() => {
expect(fixture.componentInstance.control.dirty)
.toEqual(false, `Expected control to start out pristine.`);
fixture.componentInstance.control.setValue('pizza-1');
expect(fixture.componentInstance.control.dirty)
.toEqual(false, `Expected control to stay pristine after programmatic change.`);
}));
it('should set an asterisk after the label if control is required', fakeAsync(() => {
let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
expect(requiredMarker)
.toBeNull(`Expected label not to have an asterisk, as control was not required.`);
fixture.componentInstance.isRequired = true;
fixture.detectChanges();
requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker'));
expect(requiredMarker)
.not.toBeNull(`Expected label to have an asterisk, as control was required.`);
}));
});
describe('disabled behavior', () => {
it('should disable itself when control is disabled programmatically', fakeAsync(() => {
const fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
fixture.componentInstance.control.disable();
fixture.detectChanges();
let trigger =
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.toEqual('default', `Expected cursor to be default arrow on disabled control.`);
trigger.click();
fixture.detectChanges();
expect(overlayContainerElement.textContent)
.toEqual('', `Expected select panel to stay closed.`);
expect(fixture.componentInstance.select.panelOpen)
.toBe(false, `Expected select panelOpen property to stay false.`);
fixture.componentInstance.control.enable();
fixture.detectChanges();
expect(getComputedStyle(trigger).getPropertyValue('cursor'))
.toEqual('pointer', `Expected cursor to be a pointer on enabled control.`);
trigger.click();
fixture.detectChanges();
expect(overlayContainerElement.textContent)
.toContain('Steak', `Expected select panel to open normally on re-enabled control`);
expect(fixture.componentInstance.select.panelOpen)
.toBe(true, `Expected select panelOpen property to become true.`);
}));
});
describe('animations', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
let formField: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
formField = fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
}));
it('should float the label when the panel is open and unselected', fakeAsync(() => {
expect(formField.classList.contains('mat-form-field-should-float'))
.toBe(false, 'Expected label to initially have a normal position.');
fixture.componentInstance.select.open();
fixture.detectChanges();
flush();
expect(formField.classList).toContain('mat-form-field-should-float',
'Expected label to animate up to floating position.');
fixture.componentInstance.select.close();
fixture.detectChanges();
flush();
expect(formField.classList).not.toContain('mat-form-field-should-float',
'Expected placeholder to animate back down to normal position.');
}));
it('should add a class to the panel when the menu is done animating', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-select-panel')!;
expect(panel.classList).not.toContain('mat-select-panel-done-animating');
flush();
fixture.detectChanges();
expect(panel.classList).toContain('mat-select-p