2929 lines
97 KiB
TypeScript
2929 lines
97 KiB
TypeScript
import {Directionality} from '@angular/cdk/bidi';
|
|
import {
|
|
DOWN_ARROW,
|
|
ENTER,
|
|
ESCAPE,
|
|
LEFT_ARROW,
|
|
PAGE_DOWN,
|
|
PAGE_UP,
|
|
RIGHT_ARROW,
|
|
UP_ARROW,
|
|
} from '@angular/cdk/keycodes';
|
|
import {Overlay} from '@angular/cdk/overlay';
|
|
import {_supportsShadowDom} from '@angular/cdk/platform';
|
|
import {ScrollDispatcher} from '@angular/cdk/scrolling';
|
|
import {
|
|
createKeyboardEvent,
|
|
dispatchEvent,
|
|
dispatchFakeEvent,
|
|
dispatchKeyboardEvent,
|
|
dispatchMouseEvent,
|
|
typeInElement,
|
|
} from '@angular/cdk/testing/private';
|
|
import {Component, Directive, Provider, Type, ViewChild, ViewEncapsulation} from '@angular/core';
|
|
import {ComponentFixture, TestBed, fakeAsync, flush, inject, tick} from '@angular/core/testing';
|
|
import {
|
|
FormControl,
|
|
FormsModule,
|
|
NG_VALIDATORS,
|
|
NgModel,
|
|
ReactiveFormsModule,
|
|
Validator,
|
|
} from '@angular/forms';
|
|
import {MAT_DATE_LOCALE, MatNativeDateModule, NativeDateModule} from '@angular/material/core';
|
|
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
|
|
import {MatInputModule} from '@angular/material/input';
|
|
import {By} from '@angular/platform-browser';
|
|
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
|
import {Subject} from 'rxjs';
|
|
import {DEC, JAN, JUL, JUN, SEP} from '../testing';
|
|
import {MatDatepicker} from './datepicker';
|
|
import {DatepickerDropdownPositionX, DatepickerDropdownPositionY} from './datepicker-base';
|
|
import {MatDatepickerInput} from './datepicker-input';
|
|
import {MatDatepickerToggle} from './datepicker-toggle';
|
|
import {
|
|
MAT_DATEPICKER_SCROLL_STRATEGY,
|
|
MatDateSelectionModel,
|
|
MatDatepickerIntl,
|
|
MatDatepickerModule,
|
|
} from './index';
|
|
|
|
describe('MatDatepicker', () => {
|
|
const SUPPORTS_INTL = typeof Intl != 'undefined';
|
|
|
|
// Creates a test component fixture.
|
|
function createComponent<T>(
|
|
component: Type<T>,
|
|
imports: Type<any>[] = [],
|
|
providers: Provider[] = [],
|
|
declarations: Type<any>[] = [],
|
|
): ComponentFixture<T> {
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
FormsModule,
|
|
MatDatepickerModule,
|
|
MatFormFieldModule,
|
|
MatInputModule,
|
|
NoopAnimationsModule,
|
|
ReactiveFormsModule,
|
|
...imports,
|
|
],
|
|
providers,
|
|
declarations: [component, ...declarations],
|
|
});
|
|
|
|
return TestBed.createComponent(component);
|
|
}
|
|
|
|
describe('with MatNativeDateModule', () => {
|
|
describe('standard datepicker', () => {
|
|
let fixture: ComponentFixture<StandardDatepicker>;
|
|
let testComponent: StandardDatepicker;
|
|
let model: MatDateSelectionModel<Date | null, Date>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get(MatDateSelectionModel);
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should initialize with correct value shown in input', () => {
|
|
if (SUPPORTS_INTL) {
|
|
expect(fixture.nativeElement.querySelector('input').value).toBe('1/1/2020');
|
|
}
|
|
});
|
|
|
|
it('open non-touch should open popup', fakeAsync(() => {
|
|
expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).not.toBeNull();
|
|
}));
|
|
|
|
it('touch should open dialog', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
}));
|
|
|
|
it('should not be able to open more than one dialog', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(0);
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick(500);
|
|
fixture.detectChanges();
|
|
|
|
dispatchKeyboardEvent(document.querySelector('.mat-calendar-body')!, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
tick(100);
|
|
|
|
testComponent.datepicker.open();
|
|
tick(500);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(1);
|
|
}));
|
|
|
|
it('should open datepicker if opened input is set to true', fakeAsync(() => {
|
|
testComponent.opened = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
|
|
|
|
testComponent.opened = false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
|
|
}));
|
|
|
|
it('open in disabled mode should not open the calendar', fakeAsync(() => {
|
|
testComponent.disabled = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
tick();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
}));
|
|
|
|
it('disabled datepicker input should open the calendar if datepicker is enabled', fakeAsync(() => {
|
|
testComponent.datepicker.disabled = false;
|
|
testComponent.datepickerInput.disabled = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
|
|
}));
|
|
|
|
it('close should close popup', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const popup = document.querySelector('.cdk-overlay-pane')!;
|
|
expect(popup).not.toBeNull();
|
|
expect(popup.getBoundingClientRect().height).toBeGreaterThan(0);
|
|
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(popup.getBoundingClientRect().height).toBe(0);
|
|
}));
|
|
|
|
it('should close the popup when pressing ESCAPE', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to be open.')
|
|
.toBe(true);
|
|
|
|
const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to be closed.')
|
|
.toBe(false);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
}));
|
|
|
|
it('should not close the popup when pressing ESCAPE with a modifier key', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to be open.')
|
|
.toBe(true);
|
|
|
|
const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, {
|
|
alt: true,
|
|
});
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to stay open.')
|
|
.toBe(true);
|
|
expect(event.defaultPrevented).toBe(false);
|
|
}));
|
|
|
|
it('should set the proper role on the popup', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const popup = document.querySelector('.mat-datepicker-content-container')!;
|
|
expect(popup).toBeTruthy();
|
|
expect(popup.getAttribute('role')).toBe('dialog');
|
|
}));
|
|
|
|
it(
|
|
'should set aria-labelledby to the one from the input, if not placed inside ' +
|
|
'a mat-form-field',
|
|
fakeAsync(() => {
|
|
expect(fixture.nativeElement.querySelector('mat-form-field')).toBeFalsy();
|
|
|
|
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
|
|
input.setAttribute('aria-labelledby', 'test-label');
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const popup = document.querySelector(
|
|
'.cdk-overlay-pane .mat-datepicker-content-container',
|
|
)!;
|
|
expect(popup).toBeTruthy();
|
|
expect(popup.getAttribute('aria-labelledby')).toBe('test-label');
|
|
}),
|
|
);
|
|
|
|
it('close should close dialog', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
}));
|
|
|
|
it('setting selected via click should update input and close calendar', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
|
|
|
|
let cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
dispatchMouseEvent(cells[1], 'click');
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
|
|
}));
|
|
|
|
it('setting selected via enter press should update input and close calendar', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
|
|
|
|
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
|
|
|
|
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
|
|
}));
|
|
|
|
it(
|
|
'clicking the currently selected date should close the calendar ' +
|
|
'without firing selectedChanged',
|
|
fakeAsync(() => {
|
|
const spy = jasmine.createSpy('selectionChanged spy');
|
|
const selectedSubscription = model.selectionChanged.subscribe(spy);
|
|
|
|
for (let changeCount = 1; changeCount < 3; changeCount++) {
|
|
const currentDay = changeCount;
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(document.querySelector('mat-datepicker-content')).not.toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, currentDay));
|
|
|
|
let cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
dispatchMouseEvent(cells[1], 'click');
|
|
fixture.detectChanges();
|
|
flush();
|
|
}
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
|
|
selectedSubscription.unsubscribe();
|
|
}),
|
|
);
|
|
|
|
it(
|
|
'pressing enter on the currently selected date should close the calendar without ' +
|
|
'firing selectedChanged',
|
|
fakeAsync(() => {
|
|
const spy = jasmine.createSpy('selectionChanged spy');
|
|
const selectedSubscription = model.selectionChanged.subscribe(spy);
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
|
|
expect(calendarBodyEl).not.toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
|
|
|
|
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
fixture.whenStable().then(() => {
|
|
expect(spy).not.toHaveBeenCalled();
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
|
|
selectedSubscription.unsubscribe();
|
|
});
|
|
}),
|
|
);
|
|
|
|
it('startAt should fallback to input value', () => {
|
|
expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
|
|
});
|
|
|
|
it('should attach popup to native input', () => {
|
|
let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin();
|
|
expect(attachToRef.nativeElement.tagName.toLowerCase())
|
|
.withContext('popup should be attached to native input')
|
|
.toBe('input');
|
|
});
|
|
|
|
it('input should aria-owns calendar after opened in non-touch mode', fakeAsync(() => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
expect(inputEl.getAttribute('aria-owns')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
let ownedElementId = inputEl.getAttribute('aria-owns');
|
|
expect(ownedElementId).not.toBeNull();
|
|
|
|
let ownedElement = document.getElementById(ownedElementId);
|
|
expect(ownedElement).not.toBeNull();
|
|
expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
|
|
}));
|
|
|
|
it('input should aria-owns calendar after opened in touch mode', fakeAsync(() => {
|
|
testComponent.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
expect(inputEl.getAttribute('aria-owns')).toBeNull();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
let ownedElementId = inputEl.getAttribute('aria-owns');
|
|
expect(ownedElementId).not.toBeNull();
|
|
|
|
let ownedElement = document.getElementById(ownedElementId);
|
|
expect(ownedElement).not.toBeNull();
|
|
expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
|
|
}));
|
|
|
|
it('should not throw when given wrong data type', () => {
|
|
testComponent.date = '1/1/2017' as any;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
|
|
expect(() => fixture.detectChanges()).not.toThrow();
|
|
});
|
|
|
|
it('should clear out the backdrop subscriptions on close', fakeAsync(() => {
|
|
for (let i = 0; i < 3; i++) {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
tick();
|
|
}
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const spy = jasmine.createSpy('close event spy');
|
|
const subscription = testComponent.datepicker.closedStream.subscribe(spy);
|
|
const backdrop = document.querySelector('.cdk-overlay-backdrop')! as HTMLElement;
|
|
|
|
backdrop.click();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
subscription.unsubscribe();
|
|
}));
|
|
|
|
it('should reset the datepicker when it is closed externally', fakeAsync(() => {
|
|
TestBed.resetTestingModule();
|
|
|
|
const scrolledSubject = new Subject();
|
|
|
|
// Stub out a `CloseScrollStrategy` so we can trigger a detachment via the `OverlayRef`.
|
|
fixture = createComponent(
|
|
StandardDatepicker,
|
|
[MatNativeDateModule],
|
|
[
|
|
{
|
|
provide: ScrollDispatcher,
|
|
useValue: {scrolled: () => scrolledSubject},
|
|
},
|
|
{
|
|
provide: MAT_DATEPICKER_SCROLL_STRATEGY,
|
|
deps: [Overlay],
|
|
useFactory: (overlay: Overlay) => () => overlay.scrollStrategies.close(),
|
|
},
|
|
],
|
|
);
|
|
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(true);
|
|
|
|
scrolledSubject.next();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
}));
|
|
|
|
it('should close the datepicker using ALT + UP_ARROW', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(true);
|
|
|
|
const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true});
|
|
|
|
dispatchEvent(document.body, event);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
}));
|
|
|
|
it('should not close the datepicker when using CTRL + SHIFT + ALT + UP_ARROW', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(true);
|
|
|
|
const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {
|
|
alt: true,
|
|
shift: true,
|
|
control: true,
|
|
});
|
|
|
|
dispatchEvent(document.body, event);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(true);
|
|
}));
|
|
|
|
it('should open the datepicker using ALT + DOWN_ARROW', fakeAsync(() => {
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
|
|
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
|
|
|
|
dispatchEvent(fixture.nativeElement.querySelector('input'), event);
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(true);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
}));
|
|
|
|
it('should not open for ALT + DOWN_ARROW on readonly input', fakeAsync(() => {
|
|
const input = fixture.nativeElement.querySelector('input');
|
|
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
|
|
input.setAttribute('readonly', 'true');
|
|
|
|
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
|
|
|
|
dispatchEvent(input, event);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
expect(event.defaultPrevented).toBe(false);
|
|
}));
|
|
|
|
it('should not open the datepicker using SHIFT + CTRL + ALT + DOWN_ARROW', fakeAsync(() => {
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
|
|
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {
|
|
alt: true,
|
|
shift: true,
|
|
control: true,
|
|
});
|
|
|
|
dispatchEvent(fixture.nativeElement.querySelector('input'), event);
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened).toBe(false);
|
|
expect(event.defaultPrevented).toBe(false);
|
|
}));
|
|
|
|
it('should show the invisible close button on focus', fakeAsync(() => {
|
|
testComponent.opened = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
|
|
expect(button.classList).toContain('cdk-visually-hidden');
|
|
|
|
dispatchFakeEvent(button, 'focus');
|
|
fixture.detectChanges();
|
|
expect(button.classList).not.toContain('cdk-visually-hidden');
|
|
|
|
dispatchFakeEvent(button, 'blur');
|
|
fixture.detectChanges();
|
|
expect(button.classList).toContain('cdk-visually-hidden');
|
|
}));
|
|
|
|
it('should close the overlay when clicking on the invisible close button', fakeAsync(() => {
|
|
testComponent.opened = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
|
|
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
|
|
|
|
button.click();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
|
|
}));
|
|
|
|
it('should prevent the default action of navigation keys before the focus timeout has elapsed', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
|
|
// Do the assertions before flushing the delays since we want
|
|
// to check specifically what happens before they have fired.
|
|
[UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, PAGE_UP, PAGE_DOWN].forEach(keyCode => {
|
|
const event = dispatchKeyboardEvent(document.body, 'keydown', keyCode);
|
|
fixture.detectChanges();
|
|
expect(event.defaultPrevented)
|
|
.withContext(`Expected default action to be prevented for key code ${keyCode}`)
|
|
.toBe(true);
|
|
});
|
|
|
|
tick();
|
|
flush();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with too many inputs', () => {
|
|
it('should throw when multiple inputs registered', fakeAsync(() => {
|
|
const fixture = createComponent(MultiInputDatepicker, [MatNativeDateModule]);
|
|
expect(() => fixture.detectChanges()).toThrow();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker that is assigned to input at a later point', () => {
|
|
it('should not throw on ALT + DOWN_ARROW for input without datepicker', fakeAsync(() => {
|
|
const fixture = createComponent(DelayedDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
expect(() => {
|
|
const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
|
|
dispatchEvent(fixture.nativeElement.querySelector('input'), event);
|
|
fixture.detectChanges();
|
|
flush();
|
|
}).not.toThrow();
|
|
}));
|
|
|
|
it('should handle value changes when a datepicker is assigned after init', fakeAsync(() => {
|
|
const fixture = createComponent(DelayedDatepicker, [MatNativeDateModule]);
|
|
const testComponent: DelayedDatepicker = fixture.componentInstance;
|
|
const toSelect = new Date(2017, JAN, 1);
|
|
fixture.detectChanges();
|
|
|
|
const model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get(MatDateSelectionModel);
|
|
|
|
expect(testComponent.datepickerInput.value).toBeNull();
|
|
expect(model.selection).toBeNull();
|
|
|
|
testComponent.assignedDatepicker = testComponent.datepicker;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.assignedDatepicker.select(toSelect);
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepickerInput.value).toEqual(toSelect);
|
|
expect(model.selection).toEqual(toSelect);
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with no inputs', () => {
|
|
let fixture: ComponentFixture<NoInputDatepicker>;
|
|
let testComponent: NoInputDatepicker;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(NoInputDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('should not throw when accessing disabled property', () => {
|
|
expect(() => testComponent.datepicker.disabled).not.toThrow();
|
|
});
|
|
|
|
it('should throw when opened with no registered inputs', fakeAsync(() => {
|
|
expect(() => testComponent.datepicker.open()).toThrow();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with startAt', () => {
|
|
let fixture: ComponentFixture<DatepickerWithStartAt>;
|
|
let testComponent: DatepickerWithStartAt;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithStartAt, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('explicit startAt should override input value', () => {
|
|
expect(testComponent.datepicker.startAt).toEqual(new Date(2010, JAN, 1));
|
|
});
|
|
});
|
|
|
|
describe('datepicker with startView set to year', () => {
|
|
let fixture: ComponentFixture<DatepickerWithStartViewYear>;
|
|
let testComponent: DatepickerWithStartViewYear;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithStartViewYear, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should start at the specified view', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;
|
|
|
|
// When the calendar is in year view, the first cell should be for a month rather than
|
|
// for a date.
|
|
// When the calendar is in year view, the first cell should be for a month rather than
|
|
// for a date.
|
|
expect(firstCalendarCell.textContent!.trim())
|
|
.withContext('Expected the calendar to be in year-view')
|
|
.toBe('JAN');
|
|
}));
|
|
|
|
it('should fire yearSelected when user selects calendar year in year view', fakeAsync(() => {
|
|
spyOn(testComponent, 'onYearSelection');
|
|
expect(testComponent.onYearSelection).not.toHaveBeenCalled();
|
|
|
|
testComponent.datepicker.open();
|
|
tick();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
const cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
|
|
dispatchMouseEvent(cells[0], 'click');
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.onYearSelection).toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with startView set to multiyear', () => {
|
|
let fixture: ComponentFixture<DatepickerWithStartViewMultiYear>;
|
|
let testComponent: DatepickerWithStartViewMultiYear;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithStartViewMultiYear, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
|
|
spyOn(testComponent, 'onMultiYearSelection');
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should start at the specified view', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;
|
|
|
|
// When the calendar is in year view, the first cell should be for a month rather than
|
|
// for a date.
|
|
// When the calendar is in year view, the first cell should be for a month rather than
|
|
// for a date.
|
|
expect(firstCalendarCell.textContent!.trim())
|
|
.withContext('Expected the calendar to be in multi-year-view')
|
|
.toBe('2016');
|
|
}));
|
|
|
|
it('should fire yearSelected when user selects calendar year in multiyear view', fakeAsync(() => {
|
|
expect(testComponent.onMultiYearSelection).not.toHaveBeenCalled();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
|
|
dispatchMouseEvent(cells[0], 'click');
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(testComponent.onMultiYearSelection).toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with ngModel', () => {
|
|
let fixture: ComponentFixture<DatepickerWithNgModel>;
|
|
let testComponent: DatepickerWithNgModel;
|
|
let model: MatDateSelectionModel<Date | null, Date>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithNgModel, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
fixture.whenStable().then(() => {
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get(MatDateSelectionModel);
|
|
});
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('should update datepicker when model changes', fakeAsync(() => {
|
|
expect(testComponent.datepickerInput.value).toBeNull();
|
|
expect(model.selection).toBeNull();
|
|
|
|
let selected = new Date(2017, JAN, 1);
|
|
testComponent.selected = selected;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepickerInput.value).toEqual(selected);
|
|
expect(model.selection).toEqual(selected);
|
|
}));
|
|
|
|
it('should update model when date is selected', fakeAsync(() => {
|
|
expect(testComponent.selected).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toBeNull();
|
|
|
|
let selected = new Date(2017, JAN, 1);
|
|
testComponent.datepicker.select(selected);
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.selected).toEqual(selected);
|
|
expect(testComponent.datepickerInput.value).toEqual(selected);
|
|
}));
|
|
|
|
it('should mark input dirty after input event', () => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-pristine');
|
|
|
|
inputEl.value = '2001-01-01';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-dirty');
|
|
});
|
|
|
|
it('should mark input dirty after date selected', fakeAsync(() => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-pristine');
|
|
|
|
testComponent.datepicker.select(new Date(2017, JAN, 1));
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-dirty');
|
|
}));
|
|
|
|
it('should mark input dirty after invalid value is typed in', () => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-pristine');
|
|
|
|
inputEl.value = 'hello there';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-dirty');
|
|
});
|
|
|
|
it('should not mark dirty after model change', fakeAsync(() => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-pristine');
|
|
|
|
testComponent.selected = new Date(2017, JAN, 1);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-pristine');
|
|
}));
|
|
|
|
it('should mark input touched on blur', () => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-untouched');
|
|
|
|
dispatchFakeEvent(inputEl, 'focus');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-untouched');
|
|
|
|
dispatchFakeEvent(inputEl, 'blur');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-touched');
|
|
});
|
|
|
|
it('should mark input as touched when the datepicker is closed', fakeAsync(() => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-untouched');
|
|
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-untouched');
|
|
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-touched');
|
|
}));
|
|
|
|
it('should reformat the input value on blur', () => {
|
|
if (SUPPORTS_INTL) {
|
|
// Skip this test if the internationalization API is not supported in the current
|
|
// browser. Browsers like Safari 9 do not support the "Intl" API.
|
|
return;
|
|
}
|
|
|
|
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
inputEl.value = '2001-01-01';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
dispatchFakeEvent(inputEl, 'blur');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.value).toBe('1/1/2001');
|
|
});
|
|
|
|
it('should not reformat invalid dates on blur', () => {
|
|
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
inputEl.value = 'very-valid-date';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
dispatchFakeEvent(inputEl, 'blur');
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.value).toBe('very-valid-date');
|
|
});
|
|
|
|
it('should mark input touched on calendar selection', fakeAsync(() => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.classList).toContain('ng-untouched');
|
|
|
|
testComponent.datepicker.select(new Date(2017, JAN, 1));
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.classList).toContain('ng-touched');
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with formControl', () => {
|
|
let fixture: ComponentFixture<DatepickerWithFormControl>;
|
|
let testComponent: DatepickerWithFormControl;
|
|
let model: MatDateSelectionModel<Date | null, Date>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithFormControl, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get(MatDateSelectionModel);
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('should update datepicker when formControl changes', () => {
|
|
expect(testComponent.datepickerInput.value).toBeNull();
|
|
expect(model.selection).toBeNull();
|
|
|
|
let selected = new Date(2017, JAN, 1);
|
|
testComponent.formControl.setValue(selected);
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepickerInput.value).toEqual(selected);
|
|
expect(model.selection).toEqual(selected);
|
|
});
|
|
|
|
it('should update formControl when date is selected', () => {
|
|
expect(testComponent.formControl.value).toBeNull();
|
|
expect(testComponent.datepickerInput.value).toBeNull();
|
|
|
|
let selected = new Date(2017, JAN, 1);
|
|
testComponent.datepicker.select(selected);
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.formControl.value).toEqual(selected);
|
|
expect(testComponent.datepickerInput.value).toEqual(selected);
|
|
});
|
|
|
|
it('should disable input when form control disabled', () => {
|
|
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
expect(inputEl.disabled).toBe(false);
|
|
|
|
testComponent.formControl.disable();
|
|
fixture.detectChanges();
|
|
|
|
expect(inputEl.disabled).toBe(true);
|
|
});
|
|
|
|
it('should disable toggle when form control disabled', () => {
|
|
expect(testComponent.datepickerToggle.disabled).toBe(false);
|
|
|
|
testComponent.formControl.disable();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepickerToggle.disabled).toBe(true);
|
|
});
|
|
|
|
it(
|
|
'should not dispatch FormControl change event for invalid values on input when set ' +
|
|
'to update on blur',
|
|
fakeAsync(() => {
|
|
const formControl = new FormControl({value: null} as unknown as Date, {updateOn: 'blur'});
|
|
const spy = jasmine.createSpy('change spy');
|
|
const subscription = formControl.valueChanges.subscribe(spy);
|
|
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
const setValue = (value: string) => {
|
|
inputEl.value = value;
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
};
|
|
|
|
fixture.componentInstance.formControl = formControl;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
setValue('10/10/2010');
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
setValue('10/10/');
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
setValue('10/10');
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
dispatchFakeEvent(inputEl, 'blur');
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
subscription.unsubscribe();
|
|
}),
|
|
);
|
|
|
|
it('should set the matDatepickerParse error when an invalid value is typed for the first time', () => {
|
|
const formControl = fixture.componentInstance.formControl;
|
|
|
|
expect(formControl.hasError('matDatepickerParse')).toBe(false);
|
|
|
|
typeInElement(fixture.nativeElement.querySelector('input'), 'Today');
|
|
fixture.detectChanges();
|
|
|
|
expect(formControl.hasError('matDatepickerParse')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('datepicker with mat-datepicker-toggle', () => {
|
|
let fixture: ComponentFixture<DatepickerWithToggle>;
|
|
let testComponent: DatepickerWithToggle;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithToggle, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should set `aria-haspopup` on the toggle button', () => {
|
|
const button = fixture.debugElement.query(By.css('button'))!;
|
|
|
|
expect(button).toBeTruthy();
|
|
expect(button.nativeElement.getAttribute('aria-haspopup')).toBe('dialog');
|
|
});
|
|
|
|
it('should set a default `aria-label` on the toggle button', () => {
|
|
const button = fixture.debugElement.query(By.css('button'))!;
|
|
|
|
expect(button).toBeTruthy();
|
|
expect(button.nativeElement.getAttribute('aria-label')).toBe('Open calendar');
|
|
});
|
|
|
|
it('should be able to change the button `aria-label`', () => {
|
|
fixture.componentInstance.ariaLabel = 'Toggle the datepicker';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const button = fixture.debugElement.query(By.css('button'))!;
|
|
|
|
expect(button).toBeTruthy();
|
|
expect(button.nativeElement.getAttribute('aria-label')).toBe('Toggle the datepicker');
|
|
});
|
|
|
|
it('should open calendar when toggle clicked', () => {
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
|
|
let toggle = fixture.debugElement.query(By.css('button'))!;
|
|
dispatchMouseEvent(toggle.nativeElement, 'click');
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
});
|
|
|
|
it('should not open calendar when toggle clicked if datepicker is disabled', () => {
|
|
testComponent.datepicker.disabled = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
expect(toggle.hasAttribute('disabled')).toBe(true);
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
|
|
dispatchMouseEvent(toggle, 'click');
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
});
|
|
|
|
it('should not open calendar when toggle clicked if input is disabled', () => {
|
|
expect(testComponent.datepicker.disabled).toBe(false);
|
|
|
|
testComponent.input.disabled = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
expect(toggle.hasAttribute('disabled')).toBe(true);
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
|
|
dispatchMouseEvent(toggle, 'click');
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
|
|
});
|
|
|
|
it('should set the `button` type on the trigger to prevent form submissions', () => {
|
|
let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
expect(toggle.getAttribute('type')).toBe('button');
|
|
});
|
|
|
|
it('should remove the underlying SVG icon from the tab order', () => {
|
|
const icon = fixture.debugElement.nativeElement.querySelector('svg');
|
|
expect(icon.getAttribute('focusable')).toBe('false');
|
|
});
|
|
|
|
it('should restore focus to the toggle after the calendar is closed', fakeAsync(() => {
|
|
let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
fixture.componentInstance.touchUI = false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
toggle.focus();
|
|
expect(document.activeElement).withContext('Expected toggle to be focused.').toBe(toggle);
|
|
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
let pane = document.querySelector('.cdk-overlay-pane')!;
|
|
|
|
expect(pane).withContext('Expected calendar to be open.').toBeTruthy();
|
|
expect(pane.contains(document.activeElement))
|
|
.withContext('Expected focus to be inside the calendar.')
|
|
.toBe(true);
|
|
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected focus to be restored to toggle.')
|
|
.toBe(toggle);
|
|
}));
|
|
|
|
it('should restore focus when placed inside a shadow root', fakeAsync(() => {
|
|
if (!_supportsShadowDom()) {
|
|
return;
|
|
}
|
|
|
|
fixture.destroy();
|
|
TestBed.resetTestingModule();
|
|
fixture = createComponent(DatepickerWithToggleInShadowDom, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
|
|
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
fixture.componentInstance.touchUI = false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
toggle.focus();
|
|
spyOn(toggle, 'focus').and.callThrough();
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
// We have to assert by looking at the `focus` method, because
|
|
// `document.activeElement` will return the shadow root.
|
|
expect(toggle.focus).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should allow for focus restoration to be disabled', fakeAsync(() => {
|
|
let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
fixture.componentInstance.touchUI = false;
|
|
fixture.componentInstance.restoreFocus = false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
toggle.focus();
|
|
expect(document.activeElement).withContext('Expected toggle to be focused.').toBe(toggle);
|
|
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
let pane = document.querySelector('.cdk-overlay-pane')!;
|
|
|
|
expect(pane).withContext('Expected calendar to be open.').toBeTruthy();
|
|
expect(pane.contains(document.activeElement))
|
|
.withContext('Expected focus to be inside the calendar.')
|
|
.toBe(true);
|
|
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.not.withContext('Expected focus not to be restored to toggle.')
|
|
.toBe(toggle);
|
|
}));
|
|
|
|
it('should not override focus if it was moved inside the closed event in touchUI mode', fakeAsync(() => {
|
|
const focusTarget = document.createElement('button');
|
|
const datepicker = fixture.componentInstance.datepicker;
|
|
const subscription = datepicker.closedStream.subscribe(() => focusTarget.focus());
|
|
const input = fixture.nativeElement.querySelector('input');
|
|
|
|
focusTarget.setAttribute('tabindex', '0');
|
|
document.body.appendChild(focusTarget);
|
|
|
|
// Important: we're testing the touchUI behavior on particular.
|
|
fixture.componentInstance.touchUI = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
// Focus the input before opening so that the datepicker restores focus to it on close.
|
|
input.focus();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected input to be focused on init.')
|
|
.toBe(input);
|
|
|
|
datepicker.open();
|
|
fixture.detectChanges();
|
|
tick(500);
|
|
fixture.detectChanges();
|
|
|
|
expect(document.activeElement).not.toBe(
|
|
input,
|
|
'Expected input not to be focused while dialog is open.',
|
|
);
|
|
|
|
datepicker.close();
|
|
fixture.detectChanges();
|
|
tick(500);
|
|
fixture.detectChanges();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected alternate focus target to be focused after closing.')
|
|
.toBe(focusTarget);
|
|
|
|
focusTarget.remove();
|
|
subscription.unsubscribe();
|
|
}));
|
|
|
|
it('should re-render when the i18n labels change', inject(
|
|
[MatDatepickerIntl],
|
|
(intl: MatDatepickerIntl) => {
|
|
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
intl.openCalendarLabel = 'Open the calendar, perhaps?';
|
|
intl.changes.next();
|
|
fixture.detectChanges();
|
|
|
|
expect(toggle.getAttribute('aria-label')).toBe('Open the calendar, perhaps?');
|
|
},
|
|
));
|
|
|
|
it('should toggle the active state of the datepicker toggle', fakeAsync(() => {
|
|
const toggle = fixture.debugElement.query(By.css('mat-datepicker-toggle'))!.nativeElement;
|
|
|
|
expect(toggle.classList).not.toContain('mat-datepicker-toggle-active');
|
|
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(toggle.classList).toContain('mat-datepicker-toggle-active');
|
|
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(toggle.classList).not.toContain('mat-datepicker-toggle-active');
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with custom mat-datepicker-toggle icon', () => {
|
|
it('should be able to override the mat-datepicker-toggle icon', fakeAsync(() => {
|
|
const fixture = createComponent(DatepickerWithCustomIcon, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
expect(fixture.nativeElement.querySelector('.mat-datepicker-toggle .custom-icon'))
|
|
.withContext('Expected custom icon to be rendered.')
|
|
.toBeTruthy();
|
|
|
|
expect(fixture.nativeElement.querySelector('.mat-datepicker-toggle mat-icon'))
|
|
.withContext('Expected default icon to be removed.')
|
|
.toBeFalsy();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with tabindex on mat-datepicker-toggle', () => {
|
|
it('should forward the tabindex to the underlying button', () => {
|
|
const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
const button = fixture.nativeElement.querySelector('.mat-datepicker-toggle button');
|
|
|
|
expect(button.getAttribute('tabindex')).toBe('7');
|
|
});
|
|
|
|
it('should remove the tabindex from the mat-datepicker-toggle host', () => {
|
|
const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
const host = fixture.nativeElement.querySelector('.mat-datepicker-toggle');
|
|
|
|
expect(host.hasAttribute('tabindex')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('datepicker inside mat-form-field', () => {
|
|
let fixture: ComponentFixture<FormFieldDatepicker>;
|
|
let testComponent: FormFieldDatepicker;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(FormFieldDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should float the placeholder when an invalid value is entered', () => {
|
|
testComponent.datepickerInput.value = 'totally-not-a-date' as any;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.debugElement.nativeElement.querySelector('input').value = 'totally-not-a-date';
|
|
fixture.detectChanges();
|
|
|
|
expect(fixture.debugElement.nativeElement.querySelector('label').classList).toContain(
|
|
'mdc-floating-label--float-above',
|
|
);
|
|
});
|
|
|
|
it('should pass the form field theme color to the overlay', fakeAsync(() => {
|
|
testComponent.formField.color = 'primary';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
let contentEl = document.querySelector('.mat-datepicker-content')!;
|
|
|
|
expect(contentEl.classList).toContain('mat-primary');
|
|
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
testComponent.formField.color = 'warn';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
testComponent.datepicker.open();
|
|
|
|
contentEl = document.querySelector('.mat-datepicker-content')!;
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(contentEl.classList).toContain('mat-warn');
|
|
expect(contentEl.classList).not.toContain('mat-primary');
|
|
}));
|
|
|
|
it('should prefer the datepicker color over the form field one', fakeAsync(() => {
|
|
testComponent.datepicker.color = 'accent';
|
|
testComponent.formField.color = 'warn';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const contentEl = document.querySelector('.mat-datepicker-content')!;
|
|
|
|
expect(contentEl.classList).toContain('mat-accent');
|
|
expect(contentEl.classList).not.toContain('mat-warn');
|
|
}));
|
|
|
|
it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
|
|
const label: HTMLElement = fixture.nativeElement.querySelector('label');
|
|
|
|
expect(label).toBeTruthy();
|
|
expect(label.getAttribute('id')).toBeTruthy();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
const popup = document.querySelector(
|
|
'.cdk-overlay-pane .mat-datepicker-content-container',
|
|
)!;
|
|
expect(popup).toBeTruthy();
|
|
expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with min and max dates and validation', () => {
|
|
let fixture: ComponentFixture<DatepickerWithMinAndMaxValidation>;
|
|
let testComponent: DatepickerWithMinAndMaxValidation;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithMinAndMaxValidation, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
function revalidate() {
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
}
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should use min and max dates specified by the input', () => {
|
|
expect(testComponent.datepicker._getMinDate()).toEqual(new Date(2010, JAN, 1));
|
|
expect(testComponent.datepicker._getMaxDate()).toEqual(new Date(2020, JAN, 1));
|
|
});
|
|
|
|
it('should mark invalid when value is before min', fakeAsync(() => {
|
|
testComponent.date = new Date(2009, DEC, 31);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
revalidate();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should mark invalid when value is after max', fakeAsync(() => {
|
|
testComponent.date = new Date(2020, JAN, 2);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
revalidate();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should not mark invalid when value equals min', fakeAsync(() => {
|
|
testComponent.date = testComponent.datepicker._getMinDate();
|
|
fixture.changeDetectorRef.markForCheck();
|
|
revalidate();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should not mark invalid when value equals max', fakeAsync(() => {
|
|
testComponent.date = testComponent.datepicker._getMaxDate();
|
|
fixture.changeDetectorRef.markForCheck();
|
|
revalidate();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should not mark invalid when value is between min and max', fakeAsync(() => {
|
|
testComponent.date = new Date(2010, JAN, 2);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
revalidate();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should update validity when switching between null and invalid', fakeAsync(() => {
|
|
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
inputEl.value = '';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(true);
|
|
|
|
inputEl.value = 'abcdefg';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(false);
|
|
|
|
inputEl.value = '';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(true);
|
|
}));
|
|
|
|
it('should update validity when a value is assigned', fakeAsync(() => {
|
|
const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
inputEl.value = '';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(true);
|
|
|
|
inputEl.value = 'abcdefg';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(false);
|
|
|
|
const validDate = new Date(2010, JAN, 2);
|
|
|
|
// Assigning through the selection model simulates the user doing it via the calendar.
|
|
const model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get<MatDateSelectionModel<Date>>(MatDateSelectionModel);
|
|
model.updateSelection(validDate, null);
|
|
revalidate();
|
|
|
|
expect(testComponent.model.valid).toBe(true);
|
|
expect(testComponent.date).toBe(validDate);
|
|
}));
|
|
|
|
it('should update the calendar when the min/max dates change', fakeAsync(() => {
|
|
const getDisabledCells = () => {
|
|
return document.querySelectorAll('.mat-calendar-body-disabled').length;
|
|
};
|
|
|
|
testComponent.date = new Date(2020, JAN, 5);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.minDate = new Date(2020, JAN, 3);
|
|
testComponent.maxDate = new Date(2020, JAN, 7);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
let disabledCellCount = getDisabledCells();
|
|
expect(disabledCellCount).not.toBe(0);
|
|
|
|
testComponent.minDate = new Date(2020, JAN, 1);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(getDisabledCells()).not.toBe(disabledCellCount);
|
|
disabledCellCount = getDisabledCells();
|
|
|
|
testComponent.maxDate = new Date(2020, JAN, 10);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(getDisabledCells()).not.toBe(disabledCellCount);
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with filter and validation', () => {
|
|
let fixture: ComponentFixture<DatepickerWithFilterAndValidation>;
|
|
let testComponent: DatepickerWithFilterAndValidation;
|
|
|
|
beforeEach(() => {
|
|
fixture = createComponent(DatepickerWithFilterAndValidation, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
});
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should mark input invalid', fakeAsync(() => {
|
|
testComponent.date = new Date(2017, JAN, 1);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
|
|
'ng-invalid',
|
|
);
|
|
|
|
testComponent.date = new Date(2017, JAN, 2);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
|
|
'ng-invalid',
|
|
);
|
|
}));
|
|
|
|
it('should disable filtered calendar cells', fakeAsync(() => {
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
|
|
let cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
|
|
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
|
|
}));
|
|
|
|
it('should revalidate when a new function is assigned', fakeAsync(() => {
|
|
const classList = fixture.debugElement.query(By.css('input'))!.nativeElement.classList;
|
|
testComponent.date = new Date(2017, JAN, 1);
|
|
testComponent.filter = () => true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(classList).not.toContain('ng-invalid');
|
|
|
|
testComponent.filter = () => false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(classList).toContain('ng-invalid');
|
|
}));
|
|
|
|
it('should not dispatch the change event if a new function with the same result is assigned', fakeAsync(() => {
|
|
const spy = jasmine.createSpy('change spy');
|
|
const subscription = fixture.componentInstance.model.valueChanges?.subscribe(spy);
|
|
testComponent.filter = () => false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
testComponent.filter = () => false;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
subscription?.unsubscribe();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with change and input events', () => {
|
|
let fixture: ComponentFixture<DatepickerWithChangeAndInputEvents>;
|
|
let testComponent: DatepickerWithChangeAndInputEvents;
|
|
let inputEl: HTMLInputElement;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithChangeAndInputEvents, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
|
|
spyOn(testComponent, 'onChange');
|
|
spyOn(testComponent, 'onInput');
|
|
spyOn(testComponent, 'onDateChange');
|
|
spyOn(testComponent, 'onDateInput');
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('should fire input and dateInput events when user types input', () => {
|
|
expect(testComponent.onChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onInput).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
inputEl.value = '2001-01-01';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.onChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onInput).toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should fire change and dateChange events when user commits typed input', () => {
|
|
expect(testComponent.onChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onInput).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
dispatchFakeEvent(inputEl, 'change');
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.onChange).toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).toHaveBeenCalled();
|
|
expect(testComponent.onInput).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should fire dateChange and dateInput events when user selects calendar date', fakeAsync(() => {
|
|
expect(testComponent.onChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onInput).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
|
|
|
|
const cells = document.querySelectorAll('.mat-calendar-body-cell');
|
|
dispatchMouseEvent(cells[0], 'click');
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testComponent.onChange).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateChange).toHaveBeenCalled();
|
|
expect(testComponent.onInput).not.toHaveBeenCalled();
|
|
expect(testComponent.onDateInput).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should not fire the dateInput event if the value has not changed', () => {
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
inputEl.value = '12/12/2012';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.onDateInput).toHaveBeenCalledTimes(1);
|
|
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.onDateInput).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should have updated the native input value when the dateChange event is emitted', () => {
|
|
let valueDuringChangeEvent = '';
|
|
|
|
(testComponent.onDateChange as jasmine.Spy).and.callFake(() => {
|
|
valueDuringChangeEvent = inputEl.value;
|
|
});
|
|
|
|
const model = fixture.debugElement
|
|
.query(By.directive(MatDatepicker))
|
|
.injector.get<MatDateSelectionModel<Date>>(MatDateSelectionModel);
|
|
|
|
model.updateSelection(new Date(2020, 0, 1), null);
|
|
fixture.detectChanges();
|
|
|
|
expect(valueDuringChangeEvent).toBe('1/1/2020');
|
|
});
|
|
|
|
it('should not fire dateInput when typing an invalid value', () => {
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
inputEl.value = 'a';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
|
|
inputEl.value = 'b';
|
|
dispatchFakeEvent(inputEl, 'input');
|
|
fixture.detectChanges();
|
|
expect(testComponent.onDateInput).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('with ISO 8601 strings as input', () => {
|
|
let fixture: ComponentFixture<DatepickerWithISOStrings>;
|
|
let testComponent: DatepickerWithISOStrings;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithISOStrings, [MatNativeDateModule]);
|
|
flush();
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
it('should coerce ISO strings', fakeAsync(() => {
|
|
expect(() => fixture.detectChanges()).not.toThrow();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.datepicker.startAt).toEqual(new Date(2017, JUL, 1));
|
|
expect(testComponent.datepickerInput.value).toEqual(new Date(2017, JUN, 1));
|
|
expect(testComponent.datepickerInput.min).toEqual(new Date(2017, JAN, 1));
|
|
expect(testComponent.datepickerInput.max).toEqual(new Date(2017, DEC, 31));
|
|
}));
|
|
});
|
|
|
|
describe('with events', () => {
|
|
let fixture: ComponentFixture<DatepickerWithEvents>;
|
|
let testComponent: DatepickerWithEvents;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerWithEvents, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
flush();
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
it('should dispatch an event when a datepicker is opened', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
expect(testComponent.openedSpy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should dispatch an event when a datepicker is closed', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
testComponent.datepicker.close();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(testComponent.closedSpy).toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
describe('datepicker that opens on focus', () => {
|
|
let fixture: ComponentFixture<DatepickerOpeningOnFocus>;
|
|
let testComponent: DatepickerOpeningOnFocus;
|
|
let input: HTMLInputElement;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(DatepickerOpeningOnFocus, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
input = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
}));
|
|
|
|
it('should not reopen if the browser fires the focus event asynchronously', fakeAsync(() => {
|
|
// Stub out the real focus method so we can call it reliably.
|
|
spyOn(input, 'focus').and.callFake(() => {
|
|
// Dispatch the event handler async to simulate the IE11 behavior.
|
|
Promise.resolve().then(() => dispatchFakeEvent(input, 'focus'));
|
|
});
|
|
|
|
// Open initially by focusing.
|
|
input.focus();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
|
|
// Due to some browser limitations we can't install a stub on `document.activeElement`
|
|
// so instead we have to override the previously-focused element manually.
|
|
(fixture.componentInstance.datepicker as any)._focusedElementBeforeOpen = input;
|
|
|
|
// Ensure that the datepicker is actually open.
|
|
// Ensure that the datepicker is actually open.
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to be open.')
|
|
.toBe(true);
|
|
|
|
// Close the datepicker.
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
|
|
// Schedule the input to be focused asynchronously.
|
|
input.focus();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
// Flush out the scheduled tasks.
|
|
flush();
|
|
|
|
expect(testComponent.datepicker.opened)
|
|
.withContext('Expected datepicker to be closed.')
|
|
.toBe(false);
|
|
}));
|
|
});
|
|
|
|
describe('datepicker directionality', () => {
|
|
it('should pass along the directionality to the popup', fakeAsync(() => {
|
|
const fixture = createComponent(
|
|
StandardDatepicker,
|
|
[MatNativeDateModule],
|
|
[
|
|
{
|
|
provide: Directionality,
|
|
useValue: {value: 'rtl'},
|
|
},
|
|
],
|
|
);
|
|
|
|
fixture.detectChanges();
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;
|
|
|
|
expect(overlay.getAttribute('dir')).toBe('rtl');
|
|
}));
|
|
|
|
it('should update the popup direction if the directionality value changes', fakeAsync(() => {
|
|
const dirProvider = {value: 'ltr'};
|
|
const fixture = createComponent(
|
|
StandardDatepicker,
|
|
[MatNativeDateModule],
|
|
[
|
|
{
|
|
provide: Directionality,
|
|
useFactory: () => dirProvider,
|
|
},
|
|
],
|
|
);
|
|
|
|
fixture.detectChanges();
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
let overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;
|
|
|
|
expect(overlay.getAttribute('dir')).toBe('ltr');
|
|
|
|
fixture.componentInstance.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
dirProvider.value = 'rtl';
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;
|
|
|
|
expect(overlay.getAttribute('dir')).toBe('rtl');
|
|
}));
|
|
|
|
it('should pass along the directionality to the dialog in touch mode', fakeAsync(() => {
|
|
const fixture = createComponent(
|
|
StandardDatepicker,
|
|
[MatNativeDateModule],
|
|
[
|
|
{
|
|
provide: Directionality,
|
|
useValue: {value: 'rtl'},
|
|
},
|
|
],
|
|
);
|
|
|
|
fixture.componentInstance.touch = true;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
fixture.componentInstance.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlay = document.querySelector('.cdk-global-overlay-wrapper')!;
|
|
|
|
expect(overlay.getAttribute('dir')).toBe('rtl');
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('with missing DateAdapter and MAT_DATE_FORMATS', () => {
|
|
it('should throw when created', () => {
|
|
expect(() => createComponent(StandardDatepicker)).toThrowError(
|
|
/MatDatepicker: No provider found for .*/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('datepicker directives without a datepicker', () => {
|
|
it('should not throw on init if toggle does not have a datepicker', () => {
|
|
expect(() => {
|
|
const fixture = createComponent(DatepickerToggleWithNoDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should not set aria-haspopup if toggle does not have a datepicker', () => {
|
|
const fixture = createComponent(DatepickerToggleWithNoDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
const toggle = fixture.nativeElement.querySelector('.mat-datepicker-toggle button');
|
|
|
|
expect(toggle.hasAttribute('aria-haspopup')).toBe(false);
|
|
});
|
|
|
|
it('should not throw on init if input does not have a datepicker', () => {
|
|
expect(() => {
|
|
const fixture = createComponent(DatepickerInputWithNoDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should not set aria-haspopup if input does not have a datepicker', () => {
|
|
const fixture = createComponent(DatepickerInputWithNoDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
const toggle = fixture.nativeElement.querySelector('input');
|
|
|
|
expect(toggle.hasAttribute('aria-haspopup')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('popup positioning', () => {
|
|
let fixture: ComponentFixture<StandardDatepicker>;
|
|
let testComponent: StandardDatepicker;
|
|
let input: HTMLElement;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
input = fixture.debugElement.query(By.css('input'))!.nativeElement;
|
|
input.style.position = 'fixed';
|
|
}));
|
|
|
|
it('should be below and to the right when there is plenty of space', fakeAsync(() => {
|
|
input.style.top = input.style.left = '20px';
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.top))
|
|
.withContext('Expected popup to align to input bottom.')
|
|
.toBe(Math.floor(inputRect.bottom));
|
|
expect(Math.floor(overlayRect.left))
|
|
.withContext('Expected popup to align to input left.')
|
|
.toBe(Math.floor(inputRect.left));
|
|
}));
|
|
|
|
it('should be above and to the right when there is no space below', fakeAsync(() => {
|
|
input.style.bottom = input.style.left = '20px';
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.bottom))
|
|
.withContext('Expected popup to align to input top.')
|
|
.toBe(Math.floor(inputRect.top));
|
|
expect(Math.floor(overlayRect.left))
|
|
.withContext('Expected popup to align to input left.')
|
|
.toBe(Math.floor(inputRect.left));
|
|
}));
|
|
|
|
it('should be below and to the left when there is no space on the right', fakeAsync(() => {
|
|
input.style.top = input.style.right = '20px';
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.top))
|
|
.withContext('Expected popup to align to input bottom.')
|
|
.toBe(Math.floor(inputRect.bottom));
|
|
expect(Math.floor(overlayRect.right))
|
|
.withContext('Expected popup to align to input right.')
|
|
.toBe(Math.floor(inputRect.right));
|
|
}));
|
|
|
|
it('should be above and to the left when there is no space on the bottom', fakeAsync(() => {
|
|
input.style.bottom = input.style.right = '20px';
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.bottom))
|
|
.withContext('Expected popup to align to input top.')
|
|
.toBe(Math.floor(inputRect.top));
|
|
expect(Math.floor(overlayRect.right))
|
|
.withContext('Expected popup to align to input right.')
|
|
.toBe(Math.floor(inputRect.right));
|
|
}));
|
|
|
|
it('should be able to customize the calendar position along the X axis', fakeAsync(() => {
|
|
input.style.top = input.style.left = '200px';
|
|
testComponent.xPosition = 'end';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.right))
|
|
.withContext('Expected popup to align to input right.')
|
|
.toBe(Math.floor(inputRect.right));
|
|
}));
|
|
|
|
it('should be able to customize the calendar position along the Y axis', fakeAsync(() => {
|
|
input.style.bottom = input.style.left = '100px';
|
|
testComponent.yPosition = 'above';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
|
|
const inputRect = input.getBoundingClientRect();
|
|
|
|
expect(Math.floor(overlayRect.bottom))
|
|
.withContext('Expected popup to align to input top.')
|
|
.toBe(Math.floor(inputRect.top));
|
|
}));
|
|
});
|
|
|
|
describe('internationalization', () => {
|
|
let fixture: ComponentFixture<DatepickerWithi18n>;
|
|
let testComponent: DatepickerWithi18n;
|
|
let input: HTMLInputElement;
|
|
|
|
beforeEach(() => {
|
|
fixture = createComponent(
|
|
DatepickerWithi18n,
|
|
[MatNativeDateModule, NativeDateModule],
|
|
[{provide: MAT_DATE_LOCALE, useValue: 'de-DE'}],
|
|
);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
|
|
});
|
|
|
|
it('should have the correct input value even when inverted date format', fakeAsync(() => {
|
|
if (typeof Intl === 'undefined') {
|
|
// Skip this test if the internationalization API is not supported in the current
|
|
// browser. Browsers like Safari 9 do not support the "Intl" API.
|
|
return;
|
|
}
|
|
|
|
const selected = new Date(2017, SEP, 1);
|
|
testComponent.date = selected;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
// Normally the proper date format would 01.09.2017, but some browsers seem format the
|
|
// date without the leading zero. (e.g. 1.9.2017).
|
|
expect(input.value).toMatch(/0?1\.0?9\.2017/);
|
|
expect(testComponent.datepickerInput.value).toBe(selected);
|
|
}));
|
|
});
|
|
|
|
describe('datepicker with custom header', () => {
|
|
let fixture: ComponentFixture<DatepickerWithCustomHeader>;
|
|
let testComponent: DatepickerWithCustomHeader;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(
|
|
DatepickerWithCustomHeader,
|
|
[MatNativeDateModule],
|
|
[],
|
|
[CustomHeaderForDatepicker],
|
|
);
|
|
fixture.detectChanges();
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
it('should instantiate a datepicker with a custom header', fakeAsync(() => {
|
|
expect(testComponent).toBeTruthy();
|
|
}));
|
|
|
|
it('should find the standard header element', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('mat-calendar-header')).toBeTruthy();
|
|
}));
|
|
|
|
it('should find the custom element', fakeAsync(() => {
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
flush();
|
|
fixture.detectChanges();
|
|
|
|
expect(document.querySelector('.custom-element')).toBeTruthy();
|
|
}));
|
|
});
|
|
|
|
it('should not trigger validators if new date object for same date is set for `min`', () => {
|
|
const fixture = createComponent(
|
|
DatepickerInputWithCustomValidator,
|
|
[MatNativeDateModule],
|
|
undefined,
|
|
[CustomValidator],
|
|
);
|
|
fixture.detectChanges();
|
|
const minDate = new Date(2019, 0, 1);
|
|
const validator = fixture.componentInstance.validator;
|
|
|
|
validator.validate.calls.reset();
|
|
fixture.componentInstance.min = minDate;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(validator.validate).toHaveBeenCalledTimes(1);
|
|
|
|
fixture.componentInstance.min = new Date(minDate);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(validator.validate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not trigger validators if new date object for same date is set for `max`', () => {
|
|
const fixture = createComponent(
|
|
DatepickerInputWithCustomValidator,
|
|
[MatNativeDateModule],
|
|
undefined,
|
|
[CustomValidator],
|
|
);
|
|
fixture.detectChanges();
|
|
const maxDate = new Date(2120, 0, 1);
|
|
const validator = fixture.componentInstance.validator;
|
|
|
|
validator.validate.calls.reset();
|
|
fixture.componentInstance.max = maxDate;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(validator.validate).toHaveBeenCalledTimes(1);
|
|
|
|
fixture.componentInstance.max = new Date(maxDate);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
|
|
expect(validator.validate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not emit to `stateChanges` if new date object for same date is set for `min`', () => {
|
|
const fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
const minDate = new Date(2019, 0, 1);
|
|
const spy = jasmine.createSpy('stateChanges spy');
|
|
const subscription = fixture.componentInstance.datepickerInput.stateChanges.subscribe(spy);
|
|
|
|
fixture.componentInstance.min = minDate;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
fixture.componentInstance.min = new Date(minDate);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
subscription.unsubscribe();
|
|
});
|
|
|
|
it('should not emit to `stateChanges` if new date object for same date is set for `max`', () => {
|
|
const fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
const maxDate = new Date(2120, 0, 1);
|
|
const spy = jasmine.createSpy('stateChanges spy');
|
|
const subscription = fixture.componentInstance.datepickerInput.stateChanges.subscribe(spy);
|
|
|
|
fixture.componentInstance.max = maxDate;
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
fixture.componentInstance.max = new Date(maxDate);
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
subscription.unsubscribe();
|
|
});
|
|
|
|
describe('panelClass input', () => {
|
|
let fixture: ComponentFixture<PanelClassDatepicker>;
|
|
let testComponent: PanelClassDatepicker;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
fixture = createComponent(PanelClassDatepicker, [MatNativeDateModule]);
|
|
fixture.detectChanges();
|
|
|
|
testComponent = fixture.componentInstance;
|
|
}));
|
|
|
|
afterEach(fakeAsync(() => {
|
|
testComponent.datepicker.close();
|
|
fixture.detectChanges();
|
|
flush();
|
|
}));
|
|
|
|
it('should accept a single class', () => {
|
|
testComponent.panelClass = 'foobar';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(testComponent.datepicker.panelClass).toEqual(['foobar']);
|
|
});
|
|
|
|
it('should accept multiple classes', () => {
|
|
testComponent.panelClass = 'foo bar';
|
|
fixture.changeDetectorRef.markForCheck();
|
|
fixture.detectChanges();
|
|
expect(testComponent.datepicker.panelClass).toEqual(['foo', 'bar']);
|
|
});
|
|
|
|
it('should work with ngClass', fakeAsync(() => {
|
|
testComponent.panelClass = ['foo', 'bar'];
|
|
fixture.changeDetectorRef.markForCheck();
|
|
testComponent.datepicker.open();
|
|
fixture.detectChanges();
|
|
tick();
|
|
|
|
const actualClasses = document.querySelector(
|
|
'.mat-datepicker-content .mat-calendar',
|
|
)!.classList;
|
|
expect(actualClasses.contains('foo')).toBe(true);
|
|
expect(actualClasses.contains('bar')).toBe(true);
|
|
}));
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Styles that set input elements to a fixed width. This helps with client rect measurements
|
|
* (i.e. that the datepicker aligns properly). Inputs have different dimensions in different
|
|
* browsers. e.g. in Firefox the input width is uneven, causing unexpected deviations in measuring.
|
|
* Note: The input should be able to shrink as on iOS the viewport width is very little but the
|
|
* datepicker inputs should not leave the viewport (as that throws off measuring too).
|
|
*/
|
|
const inputFixedWidthStyles = `
|
|
input {
|
|
width: 100%;
|
|
max-width: 150px;
|
|
border: none;
|
|
box-sizing: border-box;
|
|
}
|
|
`;
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [value]="date" [min]="min" [max]="max">
|
|
<mat-datepicker
|
|
#d
|
|
[touchUi]="touch"
|
|
[disabled]="disabled"
|
|
[opened]="opened"
|
|
[xPosition]="xPosition"
|
|
[yPosition]="yPosition"></mat-datepicker>
|
|
`,
|
|
styles: inputFixedWidthStyles,
|
|
standalone: false,
|
|
})
|
|
class StandardDatepicker {
|
|
opened = false;
|
|
touch = false;
|
|
disabled = false;
|
|
date: Date | null = new Date(2020, JAN, 1);
|
|
min: Date;
|
|
max: Date;
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
xPosition: DatepickerDropdownPositionX;
|
|
yPosition: DatepickerDropdownPositionY;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d"><input [matDatepicker]="d"><mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class MultiInputDatepicker {}
|
|
|
|
@Component({
|
|
template: `<mat-datepicker #d></mat-datepicker>`,
|
|
standalone: false,
|
|
})
|
|
class NoInputDatepicker {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [value]="date">
|
|
<mat-datepicker #d [startAt]="startDate"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithStartAt {
|
|
date = new Date(2020, JAN, 1);
|
|
startDate = new Date(2010, JAN, 1);
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [value]="date">
|
|
<mat-datepicker #d startView="year" (monthSelected)="onYearSelection()"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithStartViewYear {
|
|
date = new Date(2020, JAN, 1);
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
|
|
onYearSelection() {}
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [value]="date">
|
|
<mat-datepicker #d startView="multi-year"
|
|
(yearSelected)="onMultiYearSelection()"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithStartViewMultiYear {
|
|
date = new Date(2020, JAN, 1);
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
|
|
onMultiYearSelection() {}
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [(ngModel)]="selected" [matDatepicker]="d">
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithNgModel {
|
|
selected: Date | null = null;
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [formControl]="formControl" [matDatepicker]="d">
|
|
<mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithFormControl {
|
|
formControl = new FormControl<Date | null>(null);
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
@ViewChild(MatDatepickerToggle) datepickerToggle: MatDatepickerToggle<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d">
|
|
<mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
|
|
<mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithToggle {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) input: MatDatepickerInput<Date>;
|
|
touchUI = true;
|
|
restoreFocus = true;
|
|
ariaLabel: string;
|
|
}
|
|
|
|
@Component({
|
|
encapsulation: ViewEncapsulation.ShadowDom,
|
|
template: `
|
|
<input [matDatepicker]="d">
|
|
<mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
|
|
<mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithToggleInShadowDom extends DatepickerWithToggle {}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d">
|
|
<mat-datepicker-toggle [for]="d">
|
|
<div class="custom-icon" matDatepickerToggleIcon></div>
|
|
</mat-datepicker-toggle>
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithCustomIcon {}
|
|
|
|
@Component({
|
|
template: `
|
|
<mat-form-field>
|
|
<mat-label>Pick a date</mat-label>
|
|
<input matInput [matDatepicker]="d">
|
|
<mat-datepicker #d></mat-datepicker>
|
|
</mat-form-field>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class FormFieldDatepicker {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
@ViewChild(MatFormField) formField: MatFormField;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [(ngModel)]="date" [min]="minDate" [max]="maxDate">
|
|
<mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithMinAndMaxValidation {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(NgModel) model: NgModel;
|
|
date: Date | null;
|
|
minDate = new Date(2010, JAN, 1);
|
|
maxDate = new Date(2020, JAN, 1);
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [(ngModel)]="date" [matDatepickerFilter]="filter">
|
|
<mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
|
|
<mat-datepicker #d [touchUi]="true"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithFilterAndValidation {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(NgModel) model: NgModel;
|
|
date: Date;
|
|
filter = (date: Date | null) => date?.getDate() != 1;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" (change)="onChange()" (input)="onInput()"
|
|
(dateChange)="onDateChange()" (dateInput)="onDateInput()">
|
|
<mat-datepicker #d [touchUi]="true"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithChangeAndInputEvents {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
|
|
onChange() {}
|
|
|
|
onInput() {}
|
|
|
|
onDateChange() {}
|
|
|
|
onDateInput() {}
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [(ngModel)]="date">
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithi18n {
|
|
date: Date | null = new Date(2010, JAN, 1);
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [(ngModel)]="value" [min]="min" [max]="max">
|
|
<mat-datepicker #d [startAt]="startAt"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithISOStrings {
|
|
value = new Date(2017, JUN, 1).toISOString();
|
|
min = new Date(2017, JAN, 1).toISOString();
|
|
max = new Date(2017, DEC, 31).toISOString();
|
|
startAt = new Date(2017, JUL, 1).toISOString();
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [(ngModel)]="selected" [matDatepicker]="d">
|
|
<mat-datepicker (opened)="openedSpy()" (closed)="closedSpy()" #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithEvents {
|
|
selected: Date | null = null;
|
|
openedSpy = jasmine.createSpy('opened spy');
|
|
closedSpy = jasmine.createSpy('closed spy');
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input (focus)="d.open()" [matDatepicker]="d">
|
|
<mat-datepicker #d="matDatepicker"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerOpeningOnFocus {
|
|
@ViewChild(MatDatepicker) datepicker: MatDatepicker<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="ch">
|
|
<mat-datepicker #ch [calendarHeaderComponent]="customHeaderForDatePicker"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithCustomHeader {
|
|
@ViewChild('ch') datepicker: MatDatepicker<Date>;
|
|
customHeaderForDatePicker = CustomHeaderForDatepicker;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<div class="custom-element">Custom element</div>
|
|
<mat-calendar-header></mat-calendar-header>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class CustomHeaderForDatepicker {}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="assignedDatepicker" [value]="date">
|
|
<mat-datepicker #d [touchUi]="touch"></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DelayedDatepicker {
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
|
|
date: Date | null;
|
|
assignedDatepicker: MatDatepicker<Date>;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d">
|
|
<mat-datepicker-toggle tabIndex="7" [for]="d" [disabled]="disabled">
|
|
<div class="custom-icon" matDatepickerToggleIcon></div>
|
|
</mat-datepicker-toggle>
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerWithTabindexOnToggle {
|
|
disabled = false;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<mat-datepicker-toggle></mat-datepicker-toggle>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerToggleWithNoDatepicker {}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d">
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerInputWithNoDatepicker {}
|
|
|
|
@Directive({
|
|
selector: '[customValidator]',
|
|
providers: [
|
|
{
|
|
provide: NG_VALIDATORS,
|
|
useExisting: CustomValidator,
|
|
multi: true,
|
|
},
|
|
],
|
|
standalone: false,
|
|
})
|
|
class CustomValidator implements Validator {
|
|
validate = jasmine.createSpy('validate spy').and.returnValue(null);
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [(ngModel)]="value" [min]="min" [max]="max" customValidator>
|
|
<mat-datepicker #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class DatepickerInputWithCustomValidator {
|
|
@ViewChild(CustomValidator) validator: CustomValidator;
|
|
value: Date;
|
|
min: Date;
|
|
max: Date;
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<input [matDatepicker]="d" [value]="date">
|
|
<mat-datepicker [panelClass]="panelClass" touchUi #d></mat-datepicker>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
class PanelClassDatepicker {
|
|
date = new Date(0);
|
|
panelClass: any;
|
|
@ViewChild('d') datepicker: MatDatepicker<Date>;
|
|
}
|