ipsos-components
Version:
Material Design components for Angular
1,283 lines (993 loc) • 70.4 kB
text/typescript
import {Direction, Directionality} from '@angular/cdk/bidi';
import {DOWN_ARROW, ENTER, ESCAPE, SPACE, UP_ARROW, TAB} from '@angular/cdk/keycodes';
import {OverlayContainer} from '@angular/cdk/overlay';
import {map} from 'rxjs/operators/map';
import {startWith} from 'rxjs/operators/startWith';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
dispatchKeyboardEvent,
dispatchFakeEvent,
typeInElement,
MockNgZone,
} from '@angular/cdk/testing';
import {
ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit,
QueryList,
ViewChild,
ViewChildren,
NgZone,
} from '@angular/core';
import {
async,
ComponentFixture,
fakeAsync,
inject,
TestBed,
tick,
flush,
} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatOption} from '@angular/material/core';
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {MatInputModule} from '../input/index';
import {
getMatAutocompleteMissingPanelError,
MatAutocomplete,
MatAutocompleteModule,
MatAutocompleteSelectedEvent,
MatAutocompleteTrigger,
} from './index';
describe('MatAutocomplete', () => {
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
let scrolledSubject = new Subject();
let zone: MockNgZone;
// Creates a test component fixture.
function createComponent(component: any, dir: Direction = 'ltr'): ComponentFixture<any> {
TestBed.configureTestingModule({
imports: [
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
FormsModule,
ReactiveFormsModule,
NoopAnimationsModule
],
declarations: [component],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})},
{provide: ScrollDispatcher, useFactory: () => ({
scrolled: () => scrolledSubject.asObservable()
})},
{provide: NgZone, useFactory: () => {
zone = new MockNgZone();
return zone;
}}
]
});
TestBed.compileComponents();
inject([OverlayContainer], (oc: OverlayContainer) => {
overlayContainer = oc;
overlayContainerElement = oc.getContainerElement();
})();
return TestBed.createComponent(component);
}
afterEach(() => {
if (overlayContainer) {
overlayContainer.ngOnDestroy();
}
});
describe('panel toggling', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
beforeEach(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
});
it('should open the panel when the input is focused', () => {
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel state to start out closed.`);
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel state to read open when input is focused.`);
expect(overlayContainerElement.textContent)
.toContain('Alabama', `Expected panel to display when input is focused.`);
expect(overlayContainerElement.textContent)
.toContain('California', `Expected panel to display when input is focused.`);
});
it('should not open the panel on focus if the input is readonly', fakeAsync(() => {
const trigger = fixture.componentInstance.trigger;
input.readOnly = true;
fixture.detectChanges();
expect(trigger.panelOpen).toBe(false, 'Expected panel state to start out closed.');
dispatchFakeEvent(input, 'focusin');
flush();
fixture.detectChanges();
expect(trigger.panelOpen).toBe(false, 'Expected panel to stay closed.');
}));
it('should open the panel programmatically', () => {
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel state to start out closed.`);
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel state to read open when opened programmatically.`);
expect(overlayContainerElement.textContent)
.toContain('Alabama', `Expected panel to display when opened programmatically.`);
expect(overlayContainerElement.textContent)
.toContain('California', `Expected panel to display when opened programmatically.`);
});
it('should show the panel when the first open is after the initial zone stabilization',
async(() => {
// Note that we're running outside the Angular zone, in order to be able
// to test properly without the subscription from `_subscribeToClosingActions`
// giving us a false positive.
fixture.ngZone!.runOutsideAngular(() => {
fixture.componentInstance.trigger.openPanel();
Promise.resolve().then(() => {
expect(fixture.componentInstance.panel.showPanel)
.toBe(true, `Expected panel to be visible.`);
});
});
}));
it('should close the panel when the user clicks away', fakeAsync(() => {
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();
dispatchFakeEvent(document, 'click');
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected clicking outside the panel to set its state to closed.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected clicking outside the panel to close the panel.`);
}));
it('should close the panel when the user taps away on a touch device', fakeAsync(() => {
dispatchFakeEvent(input, 'focus');
fixture.detectChanges();
flush();
dispatchFakeEvent(document, 'touchend');
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected tapping outside the panel to set its state to closed.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected tapping outside the panel to close the panel.`);
}));
it('should close the panel when an option is clicked', fakeAsync(() => {
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
option.click();
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected clicking an option to set the panel state to closed.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected clicking an option to close the panel.`);
}));
it('should close the panel when a newly created option is clicked', fakeAsync(() => {
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
zone.simulateZoneExit();
// Filter down the option list to a subset of original options ('Alabama', 'California')
typeInElement('al', input);
fixture.detectChanges();
tick();
let options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
// Changing value from 'Alabama' to 'al' to re-populate the option list,
// ensuring that 'California' is created new.
typeInElement('al', input);
fixture.detectChanges();
tick();
options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected clicking a new option to set the panel state to closed.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected clicking a new option to close the panel.`);
}));
it('should close the panel programmatically', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
fixture.componentInstance.trigger.closePanel();
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected closing programmatically to set the panel state to closed.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected closing programmatically to close the panel.`);
});
it('should not throw when attempting to close the panel of a destroyed autocomplete', () => {
const trigger = fixture.componentInstance.trigger;
trigger.openPanel();
fixture.detectChanges();
fixture.destroy();
expect(() => trigger.closePanel()).not.toThrow();
});
it('should hide the panel when the options list is empty', fakeAsync(() => {
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel') as HTMLElement;
expect(panel.classList)
.toContain('mat-autocomplete-visible', `Expected panel to start out visible.`);
// Filter down the option list such that no options match the value
typeInElement('af', input);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(panel.classList)
.toContain('mat-autocomplete-hidden', `Expected panel to hide itself when empty.`);
}));
it('should keep the label floating until the panel closes', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('always', 'Expected label to float as soon as panel opens.');
zone.simulateZoneExit();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('auto', 'Expected label to return to auto state after panel closes.');
}));
it('should not open the panel when the `input` event is invoked on a non-focused input', () => {
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel state to start out closed.`);
input.value = 'Alabama';
dispatchFakeEvent(input, 'input');
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel state to stay closed.`);
});
it('should not mess with label placement if set to never', fakeAsync(() => {
fixture.componentInstance.floatLabel = 'never';
fixture.detectChanges();
fixture.componentInstance.trigger.openPanel();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('never', 'Expected label to stay static.');
flush();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('never', 'Expected label to stay in static state after close.');
}));
it('should not mess with label placement if set to always', fakeAsync(() => {
fixture.componentInstance.floatLabel = 'always';
fixture.detectChanges();
fixture.componentInstance.trigger.openPanel();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('always', 'Expected label to stay elevated on open.');
flush();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.formField.floatLabel)
.toEqual('always', 'Expected label to stay elevated after close.');
}));
it('should toggle the visibility when typing and closing the panel', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
tick();
fixture.detectChanges();
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')!.classList)
.toContain('mat-autocomplete-visible', 'Expected panel to be visible.');
typeInElement('x', input);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')!.classList)
.toContain('mat-autocomplete-hidden', 'Expected panel to be hidden.');
fixture.componentInstance.trigger.closePanel();
fixture.detectChanges();
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
typeInElement('al', input);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')!.classList)
.toContain('mat-autocomplete-visible', 'Expected panel to be visible.');
}));
it('should animate the label when the input is focused', () => {
const inputContainer = fixture.componentInstance.formField;
spyOn(inputContainer, '_animateAndLockLabel');
expect(inputContainer._animateAndLockLabel).not.toHaveBeenCalled();
dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focusin');
expect(inputContainer._animateAndLockLabel).toHaveBeenCalled();
});
it('should provide the open state of the panel', fakeAsync(() => {
expect(fixture.componentInstance.panel.isOpen).toBeFalsy(
`Expected the panel to be unopened initially.`);
dispatchFakeEvent(input, 'focusin');
fixture.detectChanges();
flush();
expect(fixture.componentInstance.panel.isOpen).toBeTruthy(
`Expected the panel to be opened on focus.`);
}));
});
it('should have the correct text direction in RTL', () => {
const rtlFixture = createComponent(SimpleAutocomplete, 'rtl');
rtlFixture.detectChanges();
rtlFixture.componentInstance.trigger.openPanel();
rtlFixture.detectChanges();
const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
expect(overlayPane.getAttribute('dir')).toEqual('rtl');
});
describe('forms integration', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
beforeEach(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
});
it('should update control value as user types with input value', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
typeInElement('a', input);
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.value)
.toEqual('a', 'Expected control value to be updated as user types.');
typeInElement('al', input);
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.value)
.toEqual('al', 'Expected control value to be updated as user types.');
});
it('should update control value when option is selected with option value', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.value)
.toEqual({code: 'CA', name: 'California'},
'Expected control value to equal the selected option value.');
}));
it('should update the control back to a string if user types after an option is selected',
fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
typeInElement('Californi', input);
fixture.detectChanges();
tick();
expect(fixture.componentInstance.stateCtrl.value)
.toEqual('Californi', 'Expected control value to revert back to string.');
}));
it('should fill the text field with display value when an option is selected', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(input.value)
.toContain('California', `Expected text field to fill with selected value.`);
}));
it('should fill the text field with value if displayWith is not set', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
fixture.componentInstance.panel.displayWith = null;
fixture.componentInstance.options.toArray()[1].value = 'test value';
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(input.value)
.toContain('test value', `Expected input to fall back to selected option's value.`);
}));
it('should fill the text field correctly if value is set to obj programmatically',
fakeAsync(() => {
fixture.componentInstance.stateCtrl.setValue({code: 'AL', name: 'Alabama'});
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(input.value)
.toContain('Alabama', `Expected input to fill with matching option's viewValue.`);
}));
it('should clear the text field if value is reset programmatically', fakeAsync(() => {
typeInElement('Alabama', input);
fixture.detectChanges();
tick();
fixture.componentInstance.stateCtrl.reset();
tick();
fixture.detectChanges();
tick();
expect(input.value).toEqual('', `Expected input value to be empty after reset.`);
}));
it('should disable input in view when disabled programmatically', () => {
const formFieldElement =
fixture.debugElement.query(By.css('.mat-form-field')).nativeElement;
expect(input.disabled)
.toBe(false, `Expected input to start out enabled in view.`);
expect(formFieldElement.classList.contains('mat-form-field-disabled'))
.toBe(false, `Expected input underline to start out with normal styles.`);
fixture.componentInstance.stateCtrl.disable();
fixture.detectChanges();
expect(input.disabled)
.toBe(true, `Expected input to be disabled in view when disabled programmatically.`);
expect(formFieldElement.classList.contains('mat-form-field-disabled'))
.toBe(true, `Expected input underline to display disabled styles.`);
});
it('should mark the autocomplete control as dirty as user types', () => {
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(false, `Expected control to start out pristine.`);
typeInElement('a', input);
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(true, `Expected control to become dirty when the user types into the input.`);
});
it('should mark the autocomplete control as dirty when an option is selected', fakeAsync(() => {
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(false, `Expected control to start out pristine.`);
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(true, `Expected control to become dirty when an option was selected.`);
}));
it('should not mark the control dirty when the value is set programmatically', () => {
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(false, `Expected control to start out pristine.`);
fixture.componentInstance.stateCtrl.setValue('AL');
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
});
it('should mark the autocomplete control as touched on blur', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.touched)
.toBe(false, `Expected control to start out untouched.`);
dispatchFakeEvent(input, 'blur');
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.touched)
.toBe(true, `Expected control to become touched on blur.`);
});
});
describe('keyboard events', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
let DOWN_ARROW_EVENT: KeyboardEvent;
let UP_ARROW_EVENT: KeyboardEvent;
let ENTER_EVENT: KeyboardEvent;
beforeEach(fakeAsync(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
}));
it('should not focus the option when DOWN key is pressed', () => {
spyOn(fixture.componentInstance.options.first, 'focus');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
expect(fixture.componentInstance.options.first.focus).not.toHaveBeenCalled();
});
it('should not close the panel when DOWN key is pressed', () => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel state to stay open when DOWN key is pressed.`);
expect(overlayContainerElement.textContent)
.toContain('Alabama', `Expected panel to keep displaying when DOWN key is pressed.`);
expect(overlayContainerElement.textContent)
.toContain('California', `Expected panel to keep displaying when DOWN key is pressed.`);
});
it('should set the active item to the first option when DOWN key is pressed', () => {
const componentInstance = fixture.componentInstance;
const optionEls =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(componentInstance.trigger.panelOpen)
.toBe(true, 'Expected first down press to open the pane.');
componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(componentInstance.trigger.activeOption === componentInstance.options.first)
.toBe(true, 'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(componentInstance.trigger.activeOption === componentInstance.options.toArray()[1])
.toBe(true, 'Expected second option to be active.');
expect(optionEls[0].classList).not.toContain('mat-active');
expect(optionEls[1].classList).toContain('mat-active');
});
it('should set the active item to the last option when UP key is pressed', () => {
const componentInstance = fixture.componentInstance;
const optionEls =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(componentInstance.trigger.panelOpen)
.toBe(true, 'Expected first up press to open the pane.');
componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
fixture.detectChanges();
expect(componentInstance.trigger.activeOption === componentInstance.options.last)
.toBe(true, 'Expected last option to be active.');
expect(optionEls[10].classList).toContain('mat-active');
expect(optionEls[0].classList).not.toContain('mat-active');
componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(componentInstance.trigger.activeOption === componentInstance.options.first)
.toBe(true, 'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
});
it('should set the active item properly after filtering', fakeAsync(() => {
const componentInstance = fixture.componentInstance;
componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
}));
it('should set the active item properly after filtering', () => {
const componentInstance = fixture.componentInstance;
typeInElement('o', input);
fixture.detectChanges();
componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
const optionEls =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
expect(componentInstance.trigger.activeOption === componentInstance.options.first)
.toBe(true, 'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
});
it('should fill the text field when an option is selected with ENTER', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
flush();
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
expect(input.value)
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
}));
it('should prevent the default enter key action', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
flush();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
expect(ENTER_EVENT.defaultPrevented)
.toBe(true, 'Expected the default action to have been prevented.');
}));
it('should not prevent the default enter action for a closed panel after a user action', () => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
fixture.detectChanges();
fixture.componentInstance.trigger.closePanel();
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
expect(ENTER_EVENT.defaultPrevented).toBe(false, 'Default action should not be prevented.');
});
it('should fill the text field, not select an option, when SPACE is entered', () => {
typeInElement('New', input);
fixture.detectChanges();
const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT);
fixture.detectChanges();
expect(input.value).not.toContain('New York', `Expected option not to be selected on SPACE.`);
});
it('should mark the control dirty when selecting an option from the keyboard', fakeAsync(() => {
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(false, `Expected control to start out pristine.`);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
flush();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.dirty)
.toBe(true, `Expected control to become dirty when option was selected by ENTER.`);
}));
it('should open the panel again when typing after making a selection', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
flush();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel state to read closed after ENTER key.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected panel to close after ENTER key.`);
typeInElement('Alabama', input);
fixture.detectChanges();
tick();
expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel state to read open when typing in input.`);
expect(overlayContainerElement.textContent)
.toContain('Alabama', `Expected panel to display when typing in input.`);
}));
it('should scroll to active options below the fold', () => {
const trigger = fixture.componentInstance.trigger;
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
// These down arrows will set the 6th option active, below the fold.
[1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));
// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop)
.toEqual(32, `Expected panel to reveal the sixth option.`);
});
it('should scroll to active options on UP arrow', () => {
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
fixture.detectChanges();
// Expect option bottom minus the panel height (528 - 256 = 272)
expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`);
});
it('should not scroll to active options that are fully in the panel', () => {
const trigger = fixture.componentInstance.trigger;
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
// These down arrows will set the 6th option active, below the fold.
[1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));
// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop)
.toEqual(32, `Expected panel to reveal the sixth option.`);
// These up arrows will set the 2nd option active
[4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT));
// Expect no scrolling to have occurred. Still showing bottom of 6th option.
expect(scrollContainer.scrollTop)
.toEqual(32, `Expected panel not to scroll up since sixth option still fully visible.`);
});
it('should scroll to active options that are above the panel', () => {
const trigger = fixture.componentInstance.trigger;
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!;
trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
// These down arrows will set the 7th option active, below the fold.
[1, 2, 3, 4, 5, 6].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT));
// These up arrows will set the 2nd option active
[5, 4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT));
// Expect to show the top of the 2nd option at the top of the panel
expect(scrollContainer.scrollTop)
.toEqual(48, `Expected panel to scroll up when option is above panel.`);
});
it('should close the panel when pressing escape', fakeAsync(() => {
const trigger = fixture.componentInstance.trigger;
const escapeEvent = createKeyboardEvent('keydown', ESCAPE);
const stopPropagationSpy = spyOn(escapeEvent, 'stopPropagation').and.callThrough();
input.focus();
flush();
fixture.detectChanges();
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
trigger._handleKeydown(escapeEvent);
fixture.detectChanges();
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.');
expect(stopPropagationSpy).toHaveBeenCalled();
}));
it('should close the panel when tabbing away from a trigger without results', fakeAsync(() => {
fixture.componentInstance.states = [];
fixture.componentInstance.filteredStates = [];
fixture.detectChanges();
input.focus();
flush();
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
.toBeTruthy('Expected panel to be rendered.');
dispatchKeyboardEvent(input, 'keydown', TAB);
fixture.detectChanges();
expect(overlayContainerElement.querySelector('.mat-autocomplete-panel'))
.toBeFalsy('Expected panel to be removed.');
}));
it('should reset the active option when closing with the escape key', fakeAsync(() => {
const trigger = fixture.componentInstance.trigger;
trigger.openPanel();
fixture.detectChanges();
tick();
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
expect(!!trigger.activeOption).toBe(false, 'Expected no active option.');
// Press the down arrow a few times.
[1, 2, 3].forEach(() => {
trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
});
// Note that this casts to a boolean, in order to prevent Jasmine
// from crashing when trying to stringify the option if the test fails.
expect(!!trigger.activeOption).toBe(true, 'Expected to find an active option.');
trigger._handleKeydown(createKeyboardEvent('keydown', ESCAPE));
tick();
expect(!!trigger.activeOption).toBe(false, 'Expected no active options.');
}));
it('should reset the active option when closing by selecting with enter', fakeAsync(() => {
const trigger = fixture.componentInstance.trigger;
trigger.openPanel();
fixture.detectChanges();
tick();
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
expect(!!trigger.activeOption).toBe(false, 'Expected no active option.');
// Press the down arrow a few times.
[1, 2, 3].forEach(() => {
trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
});
// Note that this casts to a boolean, in order to prevent Jasmine
// from crashing when trying to stringify the option if the test fails.
expect(!!trigger.activeOption).toBe(true, 'Expected to find an active option.');
trigger._handleKeydown(ENTER_EVENT);
tick();
expect(!!trigger.activeOption).toBe(false, 'Expected no active options.');
}));
});
describe('option groups', () => {
let fixture: ComponentFixture<AutocompleteWithGroups>;
let DOWN_ARROW_EVENT: KeyboardEvent;
let UP_ARROW_EVENT: KeyboardEvent;
let container: HTMLElement;
beforeEach(fakeAsync(() => {
fixture = createComponent(AutocompleteWithGroups);
fixture.detectChanges();
DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW);
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
tick();
fixture.detectChanges();
container = document.querySelector('.mat-autocomplete-panel') as HTMLElement;
}));
it('should scroll to active options below the fold', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(container.scrollTop).toBe(0, 'Expected the panel not to scroll.');
// Press the down arrow five times.
[1, 2, 3, 4, 5].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
});
// <option bottom> - <panel height> + <2x group labels> = 128
// 288 - 256 + 96 = 128
expect(container.scrollTop)
.toBe(128, 'Expected panel to reveal the sixth option.');
}));
it('should scroll to active options on UP arrow', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
fixture.detectChanges();
// <option bottom> - <panel height> + <3x group label> = 464
// 576 - 256 + 144 = 464
expect(container.scrollTop).toBe(464, 'Expected panel to reveal last option.');
}));
it('should scroll to active options that are above the panel', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(container.scrollTop).toBe(0, 'Expected panel not to scroll.');
// These down arrows will set the 7th option active, below the fold.
[1, 2, 3, 4, 5, 6].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
});
// These up arrows will set the 2nd option active
[5, 4, 3, 2, 1].forEach(() => {
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
});
// Expect to show the top of the 2nd option at the top of the panel.
// It is offset by 48, because there's a group label above it.
expect(container.scrollTop)
.toBe(96, 'Expected panel to scroll up when option is above panel.');
}));
});
describe('aria', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
beforeEach(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
});
it('should set role of input to combobox', () => {
expect(input.getAttribute('role'))
.toEqual('combobox', 'Expected role of input to be combobox.');
});
it('should set role of autocomplete panel to listbox', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel')).nativeElement;
expect(panel.getAttribute('role'))
.toEqual('listbox', 'Expected role of the panel to be listbox.');
});
it('should set aria-autocomplete to list', () => {
expect(input.getAttribute('aria-autocomplete'))
.toEqual('list', 'Expected aria-autocomplete attribute to equal list.');
});
it('should set aria-activedescendant based on the active option', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
expect(input.hasAttribute('aria-activedescendant'))
.toBe(false, 'Expected aria-activedescendant to be absent if no active item.');
const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(input.getAttribute('aria-activedescendant'))
.toEqual(fixture.componentInstance.options.first.id,
'Expected aria-activedescendant to match the active item after 1 down arrow.');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
expect(input.getAttribute('aria-activedescendant'))
.toEqual(fixture.componentInstance.options.toArray()[1].id,
'Expected aria-activedescendant to match the active item after 2 down arrows.');
}));
it('should set aria-expanded based on whether the panel is open', () => {
expect(input.getAttribute('aria-expanded'))
.toBe('false', 'Expected aria-expanded to be false while panel is closed.');
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
expect(input.getAttribute('aria-expanded'))
.toBe('true', 'Expected aria-expanded to be true while panel is open.');
fixture.componentInstance.trigger.closePanel();
fixture.detectChanges();
expect(input.getAttribute('aria-expanded'))
.toBe('false', 'Expected aria-expanded to be false when panel closes again.');
});
it('should set aria-expanded properly when the panel is hidden', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
expect(input.getAttribute('aria-expanded'))
.toBe('true', 'Expected aria-expanded to be true while panel is open.');
typeInElement('zz', input);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(input.getAttribute('aria-expanded'))
.toBe('false', 'Expected aria-expanded to be false when panel hides itself.');
}));
it('should set aria-owns based on the attached autocomplete', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel')).nativeElement;
expect(input.getAttribute('aria-owns'))
.toEqual(panel.getAttribute('id'), 'Expected aria-owns to match attached autocomplete.');
});
it('should restore focus to the input when clicking to select a value', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
// Focus the option manually since the synthetic click may not do it.
option.focus();
option.click();
fixture.detectChanges();
expect(document.activeElement).toBe(input, 'Expected focus to be restored to the input.');
}));
});
describe('Fallback positions', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
let input: HTMLInputElement;
let inputReference: HTMLInputElement;
beforeEach(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
inputReference = fixture.debugElement.query(By.css('.mat-input-flex')).nativeElement;
});
it('should use below positioning by default', fakeAsync(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const inputBottom = inputReference.getBoundingClientRect().bottom;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelTop = panel.getBoundingClientRect().top;
expect(Math.floor(inputBottom))
.toEqual(Math.floor(panelTop), `Expected panel top to match input bottom by default.`);
}));
it('should reposition the panel on scroll', () => {
const spacer = document.createElement('div');
spacer.style.height = '1000px';
document.body.appendChild(spacer);
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
window.scroll(0, 100);
scrolledSubject.next();
fixture.detectChanges();
const inputBottom = inputReference.getBoundingClientRect().bottom;
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
const panelTop = panel.getBoundingClientRect().top;
expect(Math.floor(inputBottom)).toEqual(Math.floor(panelTop),
'Expected panel top to match input bottom after scrolling.');
document.body.removeChild(spacer);
});
it('should fall back to above position if panel cannot fit below', fakeAsync(() => {
// Push the autocomplete trigger down so it won't have room to open "below"
inputReference.style.bottom = '0';
inputReference.style.position = 'fixed';
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
const inputTop = inputReference.getBoundingClientRect().top;
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
const panelBottom = panel.getBoundingClientRect().bottom;
expect(Math.floor(inputTop))
.toEqual(Math.floor(panelBottom), `Expected panel to fall back to above position.`);
}));
it('should align panel properly when filtering in "above" position', fakeAsync(() => {
// Push the autocomplete trigger down so it won't have room to open "below"
inputReference.style.bottom = '0';
inputReference.style.position = 'fixed';
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
typeInElement('f', input);
fixture.detectChanges();
tick();
const inputTop = inputReference.getBoundingClientRect().top;
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
const panelBottom = panel.getBoundingClientRect().bottom;
expect(Math.floor(inputTop))
.toEqual(Math.floor(panelBottom), `Expected panel to stay aligned after filtering.`);
}));
});
describe('Option selection', () => {
let fixture: ComponentFixture<SimpleAutocomplete>;
beforeEach(() => {
fixture = createComponent(SimpleAutocomplete);
fixture.detectChanges();
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
});
it('should deselect any other selected option', fakeAsync(() => {
let options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
zone.simulateZoneExit();
fixture.detectChanges();
let componentOptions = fixture.componentInstance.options.toArray();
expect(componentOptions[0].selected)
.toBe(true, `Clicked option should be selected.`);
options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(componentOptions[0].selected)
.toBe(false, `Previous option should not be selected.`);
expect(componentOptions[1].selected)
.toBe(true, `New Clicked option should be selected.`);
}));
it('should call deselect only on the previous selected option', fakeAsync(() => {
let options =
overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
zone.simulateZoneExit();
fixture.detectChanges();
let componentOptions = fixture.componentInstance.options.toArray();
componentOptions.forEach(option => spyOn(option, 'deselect'));
expect(componentOptions[0].selected)
.toBe(true, `Clicked option should be