/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/material/datepicker/datepicker.spec.ts
2 811 строк
97 KB
Kristiyan Kostadinov
build: add explicit change detection to tests (#33010)
30 мар 2026, 10:52
Не верифицирован
30 мар 2026, 10:52
7806398
Код
Авторство
О чём код?
import {Direction} from '@angular/cdk/bidi'; import { DOWN_ARROW, ENTER, ESCAPE, LEFT_ARROW, PAGE_DOWN, PAGE_UP, RIGHT_ARROW, UP_ARROW, } from '@angular/cdk/keycodes'; import {createCloseScrollStrategy} from '@angular/cdk/overlay'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import { createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, provideFakeDirectionality, typeInElement, } from '@angular/cdk/testing/private'; import { Component, Directive, Injector, Provider, Type, ViewChild, ViewEncapsulation, signal, ChangeDetectionStrategy, } from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import { FormControl, FormsModule, NG_VALIDATORS, NgModel, ReactiveFormsModule, Validator, } from '@angular/forms'; import {By} from '@angular/platform-browser'; import {Subject} from 'rxjs'; import {MATERIAL_ANIMATIONS, MAT_DATE_LOCALE, provideNativeDateAdapter} from '../core'; import {MatFormField} from '../form-field'; import {MatInputModule} from '../input'; 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, MatDatepickerToggleIcon} from './datepicker-toggle'; import { MAT_DATEPICKER_SCROLL_STRATEGY, MatCalendarHeader, MatDateSelectionModel, MatDatepickerIntl, } from './index'; describe('MatDatepicker', () => { const SUPPORTS_INTL = typeof Intl != 'undefined'; // Creates a test component fixture. function createComponent<T>(component: Type<T>, providers: Provider[] = []): ComponentFixture<T> { TestBed.configureTestingModule({ providers: [ ...providers, {provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: true}}, ], }); return TestBed.createComponent(component); } function wait(milliseconds: number) { return new Promise(resolve => setTimeout(resolve, milliseconds)); } describe('with native adapter', () => { describe('standard datepicker', () => { let fixture: ComponentFixture<StandardDatepicker>; let testComponent: StandardDatepicker; let model: MatDateSelectionModel<Date | null, Date>; beforeEach(() => { fixture = createComponent(StandardDatepicker, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; model = fixture.debugElement .query(By.directive(MatDatepicker)) .injector.get(MatDateSelectionModel); }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); 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', () => { expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).not.toBeNull(); }); it('touch should open dialog', () => { testComponent.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); }); it('should not be able to open more than one dialog', () => { testComponent.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(0); testComponent.datepicker.open(); fixture.detectChanges(); dispatchKeyboardEvent(document.querySelector('.mat-calendar-body')!, 'keydown', ENTER); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(1); }); it('should open datepicker if opened input is set to true', () => { testComponent.opened = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-content')).not.toBeNull(); testComponent.opened = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-content')).toBeNull(); }); it('open in disabled mode should not open the calendar', () => { 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(); fixture.detectChanges(); 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', () => { 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(); expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull(); }); it('close should close popup', () => { testComponent.datepicker.open(); fixture.detectChanges(); const popup = document.querySelector('.cdk-overlay-pane')!; expect(popup).not.toBeNull(); expect(popup.getBoundingClientRect().height).toBeGreaterThan(0); testComponent.datepicker.close(); fixture.detectChanges(); expect(popup.getBoundingClientRect().height).toBe(0); }); it('should close the popup when pressing ESCAPE', () => { testComponent.datepicker.open(); fixture.detectChanges(); expect(testComponent.datepicker.opened) .withContext('Expected datepicker to be open.') .toBe(true); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); fixture.detectChanges(); 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', () => { testComponent.datepicker.open(); fixture.detectChanges(); expect(testComponent.datepicker.opened) .withContext('Expected datepicker to be open.') .toBe(true); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, { alt: true, }); fixture.detectChanges(); 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', () => { testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { 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(); 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', () => { testComponent.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); testComponent.datepicker.close(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog')).toBeNull(); }); it('setting selected via click should update input and close calendar', () => { testComponent.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); 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(); 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', () => { testComponent.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); 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(); dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); fixture.detectChanges(); dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER); fixture.detectChanges(); 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', () => { 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(); 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(); } 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', () => { const spy = jasmine.createSpy('selectionChanged spy'); const selectedSubscription = model.selectionChanged.subscribe(spy); testComponent.datepicker.open(); fixture.detectChanges(); 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(); 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', () => { let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement; expect(inputEl.getAttribute('aria-owns')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { 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(); 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', () => { for (let i = 0; i < 3; i++) { testComponent.datepicker.open(); fixture.detectChanges(); testComponent.datepicker.close(); fixture.detectChanges(); } testComponent.datepicker.open(); fixture.detectChanges(); 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(); expect(spy).toHaveBeenCalledTimes(1); expect(testComponent.datepicker.opened).toBe(false); subscription.unsubscribe(); }); it('should reset the datepicker when it is closed externally', async () => { TestBed.resetTestingModule(); const scrolledSubject = new Subject(); // Stub out a `CloseScrollStrategy` so we can trigger a detachment via the `OverlayRef`. fixture = createComponent(StandardDatepicker, [ provideNativeDateAdapter(), { provide: ScrollDispatcher, useValue: {scrolled: () => scrolledSubject}, }, { provide: MAT_DATEPICKER_SCROLL_STRATEGY, useFactory: () => () => createCloseScrollStrategy(TestBed.inject(Injector)), }, ]); fixture.detectChanges(); testComponent = fixture.componentInstance; testComponent.datepicker.open(); fixture.detectChanges(); expect(testComponent.datepicker.opened).toBe(true); scrolledSubject.next(); // The scroll strategy has some debouncing we need to wait for. await wait(50); fixture.detectChanges(); expect(testComponent.datepicker.opened).toBe(false); }); it('should close the datepicker using ALT + UP_ARROW', () => { testComponent.datepicker.open(); fixture.detectChanges(); expect(testComponent.datepicker.opened).toBe(true); const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true}); dispatchEvent(document.body, event); fixture.detectChanges(); expect(testComponent.datepicker.opened).toBe(false); }); it('should not close the datepicker when using CTRL + SHIFT + ALT + UP_ARROW', () => { testComponent.datepicker.open(); fixture.detectChanges(); 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(); expect(testComponent.datepicker.opened).toBe(true); }); it('should open the datepicker using ALT + DOWN_ARROW', () => { expect(testComponent.datepicker.opened).toBe(false); const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true}); dispatchEvent(fixture.nativeElement.querySelector('input'), event); fixture.detectChanges(); expect(testComponent.datepicker.opened).toBe(true); expect(event.defaultPrevented).toBe(true); }); it('should not open for ALT + DOWN_ARROW on readonly input', () => { 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(); expect(testComponent.datepicker.opened).toBe(false); expect(event.defaultPrevented).toBe(false); }); it('should not open the datepicker using SHIFT + CTRL + ALT + DOWN_ARROW', () => { 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(); expect(testComponent.datepicker.opened).toBe(false); expect(event.defaultPrevented).toBe(false); }); it('should show the invisible close button on focus', () => { testComponent.opened = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); 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', () => { testComponent.opened = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement; expect(document.querySelector('.mat-datepicker-content')).not.toBeNull(); button.click(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-content')).toBeNull(); }); it('should prevent the default action of navigation keys before the focus timeout has elapsed', () => { 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); }); }); }); describe('datepicker with too many inputs', () => { it('should throw when multiple inputs registered', () => { const fixture = createComponent(MultiInputDatepicker, [provideNativeDateAdapter()]); 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', () => { const fixture = createComponent(DelayedDatepicker, [provideNativeDateAdapter()]); fixture.detectChanges(); expect(() => { const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true}); dispatchEvent(fixture.nativeElement.querySelector('input'), event); fixture.detectChanges(); }).not.toThrow(); }); it('should handle value changes when a datepicker is assigned after init', () => { const fixture = createComponent(DelayedDatepicker, [provideNativeDateAdapter()]); 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(); expect(testComponent.datepickerInput.value).toEqual(toSelect); expect(model.selection).toEqual(toSelect); }); }); describe('datepicker with no inputs', () => { let fixture: ComponentFixture<NoInputDatepicker>; let testComponent: NoInputDatepicker; beforeEach(() => { fixture = createComponent(NoInputDatepicker, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { 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', () => { expect(() => testComponent.datepicker.open()).toThrow(); }); }); describe('datepicker with startAt', () => { let fixture: ComponentFixture<DatepickerWithStartAt>; let testComponent: DatepickerWithStartAt; beforeEach(() => { fixture = createComponent(DatepickerWithStartAt, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { 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(() => { fixture = createComponent(DatepickerWithStartViewYear, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); it('should start at the specified view', () => { testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { spyOn(testComponent, 'onYearSelection'); expect(testComponent.onYearSelection).not.toHaveBeenCalled(); testComponent.datepicker.open(); fixture.detectChanges(); const cells = document.querySelectorAll('.mat-calendar-body-cell'); dispatchMouseEvent(cells[0], 'click'); fixture.detectChanges(); expect(testComponent.onYearSelection).toHaveBeenCalled(); }); }); describe('datepicker with startView set to multiyear', () => { let fixture: ComponentFixture<DatepickerWithStartViewMultiYear>; let testComponent: DatepickerWithStartViewMultiYear; beforeEach(() => { fixture = createComponent(DatepickerWithStartViewMultiYear, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; spyOn(testComponent, 'onMultiYearSelection'); }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); it('should start at the specified view', () => { testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { expect(testComponent.onMultiYearSelection).not.toHaveBeenCalled(); testComponent.datepicker.open(); fixture.detectChanges(); const cells = document.querySelectorAll('.mat-calendar-body-cell'); dispatchMouseEvent(cells[0], 'click'); fixture.detectChanges(); expect(testComponent.onMultiYearSelection).toHaveBeenCalled(); }); }); describe('datepicker with ngModel', () => { let fixture: ComponentFixture<DatepickerWithNgModel>; let testComponent: DatepickerWithNgModel; let model: MatDateSelectionModel<Date | null, Date>; beforeEach(() => { fixture = createComponent(DatepickerWithNgModel, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; model = fixture.debugElement .query(By.directive(MatDatepicker)) .injector.get(MatDateSelectionModel); }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); it('should update datepicker when model changes', async () => { expect(testComponent.datepickerInput.value).toBeNull(); expect(model.selection).toBeNull(); let selected = new Date(2017, JAN, 1); testComponent.selected = selected; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); expect(testComponent.datepickerInput.value).toEqual(selected); expect(model.selection).toEqual(selected); }); it('should update model when date is selected', () => { expect(testComponent.selected).toBeNull(); expect(testComponent.datepickerInput.value).toBeNull(); let selected = new Date(2017, JAN, 1); testComponent.datepicker.select(selected); 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', () => { 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(); 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', () => { 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(); 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', () => { let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement; expect(inputEl.classList).toContain('ng-untouched'); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); expect(inputEl.classList).toContain('ng-untouched'); fixture.componentInstance.datepicker.close(); 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', () => { 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(); expect(inputEl.classList).toContain('ng-touched'); }); }); describe('datepicker with formControl', () => { let fixture: ComponentFixture<DatepickerWithFormControl>; let testComponent: DatepickerWithFormControl; let model: MatDateSelectionModel<Date | null, Date>; beforeEach(() => { fixture = createComponent(DatepickerWithFormControl, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; model = fixture.debugElement .query(By.directive(MatDatepicker)) .injector.get(MatDateSelectionModel); }); afterEach(() => { 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', () => { 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(); 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(); 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); }); it('should not re-format the input value if the forms module re-assigns the same date', () => { const input = fixture.nativeElement.querySelector('input'); const date = new Date(2017, JAN, 1); testComponent.formControl.setValue(date); fixture.detectChanges(); expect(input.value).toContain('2017'); // Note: this isn't how users would behave, but it captures // the sequence of events with signal forms. input.value = 'foo'; testComponent.formControl.setValue(date); fixture.detectChanges(); expect(input.value).toBe('foo'); }); it('should not re-format the input value if the forms module re-assigns null', () => { const input = fixture.nativeElement.querySelector('input'); testComponent.formControl.setValue(null); fixture.detectChanges(); expect(input.value).toBe(''); // Note: this isn't how users would behave, but it captures // the sequence of events with signal forms. input.value = 'foo'; testComponent.formControl.setValue(null); fixture.detectChanges(); expect(input.value).toBe('foo'); }); }); describe('datepicker with mat-datepicker-toggle', () => { let fixture: ComponentFixture<DatepickerWithToggle>; let testComponent: DatepickerWithToggle; beforeEach(() => { fixture = createComponent(DatepickerWithToggle, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); 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', async () => { 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(); await wait(50); // Wait for focus to move to calendar fixture.detectChanges(); let pane = document.querySelector('.cdk-overlay-pane')!; expect(pane.contains(document.activeElement)) .withContext('Expected focus to be inside the calendar.') .toBe(true); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); await wait(200); expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to the toggle.'); }); it('should restore focus when placed inside a shadow root', () => { if (!_supportsShadowDom()) { return; } fixture.destroy(); TestBed.resetTestingModule(); fixture = createComponent(DatepickerWithToggleInShadowDom, [provideNativeDateAdapter()]); 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(); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); // 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', async () => { 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(); await wait(50); // Wait for focus to move to calendar fixture.detectChanges(); let pane = document.querySelector('.cdk-overlay-pane')!; expect(pane.contains(document.activeElement)) .withContext('Expected focus to be inside the calendar.') .toBe(true); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); await wait(200); expect(document.activeElement).not.toBe( toggle, 'Expected focus not to be restored to the toggle.', ); }); it('should not override focus if it was moved inside the closed event in touchUI mode', async () => { 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(); await wait(500); fixture.detectChanges(); expect(document.activeElement).not.toBe( input, 'Expected input not to be focused while dialog is open.', ); datepicker.close(); fixture.detectChanges(); await wait(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', () => { const intl = TestBed.inject(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', () => { 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(); expect(toggle.classList).toContain('mat-datepicker-toggle-active'); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); expect(toggle.classList).not.toContain('mat-datepicker-toggle-active'); }); it('should set aria-expanded on the toggle', () => { const button = fixture.nativeElement.querySelector('mat-datepicker-toggle button'); expect(button.getAttribute('aria-expanded')).toBe('false'); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); expect(button.getAttribute('aria-expanded')).toBe('true'); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); expect(button.getAttribute('aria-expanded')).toBe('false'); }); }); describe('datepicker with custom mat-datepicker-toggle icon', () => { it('should be able to override the mat-datepicker-toggle icon', () => { const fixture = createComponent(DatepickerWithCustomIcon, [provideNativeDateAdapter()]); 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, [ provideNativeDateAdapter(), ]); 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, [ provideNativeDateAdapter(), ]); 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(() => { fixture = createComponent(FormFieldDatepicker, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); 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', () => { testComponent.formField.color = 'primary'; fixture.changeDetectorRef.markForCheck(); testComponent.datepicker.open(); fixture.detectChanges(); let contentEl = document.querySelector('.mat-datepicker-content')!; expect(contentEl.classList).toContain('mat-primary'); testComponent.datepicker.close(); fixture.detectChanges(); testComponent.formField.color = 'warn'; fixture.changeDetectorRef.markForCheck(); testComponent.datepicker.open(); contentEl = document.querySelector('.mat-datepicker-content')!; fixture.detectChanges(); expect(contentEl.classList).toContain('mat-warn'); expect(contentEl.classList).not.toContain('mat-primary'); }); it('should prefer the datepicker color over the form field one', () => { testComponent.datepicker.color = 'accent'; testComponent.formField.color = 'warn'; fixture.changeDetectorRef.markForCheck(); testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { const label: HTMLElement = fixture.nativeElement.querySelector('label'); expect(label).toBeTruthy(); expect(label.getAttribute('id')).toBeTruthy(); testComponent.datepicker.open(); fixture.detectChanges(); 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(() => { fixture = createComponent(DatepickerWithMinAndMaxValidation, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); async function revalidate() { fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); } afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); 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', async () => { testComponent.date = new Date(2009, DEC, 31); fixture.changeDetectorRef.markForCheck(); await revalidate(); expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain( 'ng-invalid', ); }); it('should mark invalid when value is after max', async () => { testComponent.date = new Date(2020, JAN, 2); fixture.changeDetectorRef.markForCheck(); await revalidate(); expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain( 'ng-invalid', ); }); it('should not mark invalid when value equals min', async () => { testComponent.date = testComponent.datepicker._getMinDate(); fixture.changeDetectorRef.markForCheck(); await revalidate(); expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain( 'ng-invalid', ); }); it('should not mark invalid when value equals max', async () => { testComponent.date = testComponent.datepicker._getMaxDate(); fixture.changeDetectorRef.markForCheck(); await 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', async () => { testComponent.date = new Date(2010, JAN, 2); fixture.changeDetectorRef.markForCheck(); await revalidate(); expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain( 'ng-invalid', ); }); it('should update validity when switching between null and invalid', async () => { const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement; inputEl.value = ''; dispatchFakeEvent(inputEl, 'input'); await revalidate(); expect(testComponent.model.valid).toBe(true); inputEl.value = 'abcdefg'; dispatchFakeEvent(inputEl, 'input'); await revalidate(); expect(testComponent.model.valid).toBe(false); inputEl.value = ''; dispatchFakeEvent(inputEl, 'input'); await revalidate(); expect(testComponent.model.valid).toBe(true); }); it('should update validity when a value is assigned', async () => { const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement; inputEl.value = ''; dispatchFakeEvent(inputEl, 'input'); await revalidate(); expect(testComponent.model.valid).toBe(true); inputEl.value = 'abcdefg'; dispatchFakeEvent(inputEl, 'input'); await 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); await revalidate(); expect(testComponent.model.valid).toBe(true); expect(testComponent.date).toBe(validDate); }); it('should update the calendar when the min/max dates change', () => { 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(); let disabledCellCount = getDisabledCells(); expect(disabledCellCount).not.toBe(0); testComponent.minDate = new Date(2020, JAN, 1); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getDisabledCells()).not.toBe(disabledCellCount); disabledCellCount = getDisabledCells(); testComponent.maxDate = new Date(2020, JAN, 10); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(getDisabledCells()).not.toBe(disabledCellCount); }); }); describe('datepicker with filter and validation', () => { let fixture: ComponentFixture<DatepickerWithFilterAndValidation>; let testComponent: DatepickerWithFilterAndValidation; beforeEach(() => { fixture = createComponent(DatepickerWithFilterAndValidation, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); it('should mark input invalid', async () => { testComponent.date = new Date(2017, JAN, 1); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); 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(); await fixture.whenStable(); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain( 'ng-invalid', ); }); it('should disable filtered calendar cells', () => { fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); 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', async () => { 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(); await fixture.whenStable(); fixture.detectChanges(); expect(classList).not.toContain('ng-invalid'); testComponent.filter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); expect(classList).toContain('ng-invalid'); }); it('should not dispatch the change event if a new function with the same result is assigned', () => { const spy = jasmine.createSpy('change spy'); const subscription = fixture.componentInstance.model.valueChanges?.subscribe(spy); testComponent.filter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); testComponent.filter = () => false; fixture.changeDetectorRef.markForCheck(); 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(() => { fixture = createComponent(DatepickerWithChangeAndInputEvents, [provideNativeDateAdapter()]); 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(() => { 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', () => { 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(); expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull(); const cells = document.querySelectorAll('.mat-calendar-body-cell'); dispatchMouseEvent(cells[0], 'click'); fixture.detectChanges(); 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(() => { fixture = createComponent(DatepickerWithISOStrings, [provideNativeDateAdapter()]); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); it('should coerce ISO strings', async () => { expect(() => fixture.detectChanges()).not.toThrow(); await fixture.whenStable(); 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(() => { fixture = createComponent(DatepickerWithEvents, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); it('should dispatch an event when a datepicker is opened', () => { testComponent.datepicker.open(); fixture.detectChanges(); expect(testComponent.openedSpy).toHaveBeenCalled(); }); it('should dispatch an event when a datepicker is closed', () => { testComponent.datepicker.open(); fixture.detectChanges(); testComponent.datepicker.close(); fixture.detectChanges(); expect(testComponent.closedSpy).toHaveBeenCalled(); }); }); describe('datepicker that opens on focus', () => { let fixture: ComponentFixture<DatepickerOpeningOnFocus>; let testComponent: DatepickerOpeningOnFocus; let input: HTMLInputElement; beforeEach(() => { fixture = createComponent(DatepickerOpeningOnFocus, [provideNativeDateAdapter()]); 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', async () => { // Stub out the real focus method so we can call it reliably. spyOn(input, 'focus').and.callFake(() => { // Dispatch the event handler to simulate the IE11 behavior. Promise.resolve().then(() => dispatchFakeEvent(input, 'focus')); }); // Open initially by focusing. input.focus(); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); // 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. 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(); // Flush out the scheduled tasks. await wait(200); fixture.detectChanges(); expect(testComponent.datepicker.opened) .withContext('Expected datepicker to be closed.') .toBe(false); }); }); describe('datepicker directionality', () => { it('should pass along the directionality to the popup', () => { const fixture = createComponent(StandardDatepicker, [ provideNativeDateAdapter(), provideFakeDirectionality('rtl'), ]); fixture.detectChanges(); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); 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', () => { const dir = signal<Direction>('ltr'); const fixture = createComponent(StandardDatepicker, [ provideNativeDateAdapter(), provideFakeDirectionality(dir), ]); fixture.detectChanges(); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); let overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!; expect(overlay.getAttribute('dir')).toBe('ltr'); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); dir.set('rtl'); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); 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', () => { const fixture = createComponent(StandardDatepicker, [ provideNativeDateAdapter(), provideFakeDirectionality('rtl'), ]); fixture.componentInstance.touch = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); 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, [ provideNativeDateAdapter(), ]); fixture.detectChanges(); }).not.toThrow(); }); it('should not set aria-haspopup if toggle does not have a datepicker', () => { const fixture = createComponent(DatepickerToggleWithNoDatepicker, [ provideNativeDateAdapter(), ]); fixture.detectChanges(); const toggle = fixture.nativeElement.querySelector('.mat-datepicker-toggle button'); expect(toggle.hasAttribute('aria-haspopup')).toBe(false); }); it('should not set aria-expanded if toggle does not have a datepicker', () => { const fixture = createComponent(DatepickerToggleWithNoDatepicker, [ provideNativeDateAdapter(), ]); fixture.detectChanges(); const toggle = fixture.nativeElement.querySelector('.mat-datepicker-toggle button'); expect(toggle.hasAttribute('aria-expanded')).toBe(false); }); it('should not throw on init if input does not have a datepicker', () => { expect(() => { const fixture = createComponent(DatepickerInputWithNoDatepicker, [ provideNativeDateAdapter(), ]); fixture.detectChanges(); }).not.toThrow(); }); it('should not set aria-haspopup if input does not have a datepicker', () => { const fixture = createComponent(DatepickerInputWithNoDatepicker, [ provideNativeDateAdapter(), ]); 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(() => { fixture = createComponent(StandardDatepicker, [provideNativeDateAdapter()]); 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', () => { input.style.top = input.style.left = '20px'; testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { input.style.bottom = input.style.left = '20px'; testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { input.style.top = input.style.right = '20px'; testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { input.style.bottom = input.style.right = '20px'; testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { input.style.top = input.style.left = '200px'; testComponent.xPosition = 'end'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); 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', () => { input.style.bottom = input.style.left = '100px'; testComponent.yPosition = 'above'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); 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, [ provideNativeDateAdapter(), {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', async () => { 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(); await fixture.whenStable(); 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(() => { fixture = createComponent(DatepickerWithCustomHeader, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); it('should instantiate a datepicker with a custom header', () => { expect(testComponent).toBeTruthy(); }); it('should find the standard header element', () => { testComponent.datepicker.open(); fixture.detectChanges(); fixture.detectChanges(); expect(document.querySelector('mat-calendar-header')).toBeTruthy(); }); it('should find the custom element', () => { testComponent.datepicker.open(); fixture.detectChanges(); 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, [ provideNativeDateAdapter(), ]); 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, [ provideNativeDateAdapter(), ]); 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, [provideNativeDateAdapter()]); 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, [provideNativeDateAdapter()]); 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(() => { fixture = createComponent(PanelClassDatepicker, [provideNativeDateAdapter()]); fixture.detectChanges(); testComponent = fixture.componentInstance; }); afterEach(() => { testComponent.datepicker.close(); fixture.detectChanges(); }); 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', () => { testComponent.panelClass = ['foo', 'bar']; fixture.changeDetectorRef.markForCheck(); testComponent.datepicker.open(); fixture.detectChanges(); 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, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class MultiInputDatepicker {} @Component({ template: `<mat-datepicker #d></mat-datepicker>`, imports: [MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class NoInputDatepicker { @ViewChild('d') datepicker!: MatDatepicker<Date>; } @Component({ template: ` <input [matDatepicker]="d" [value]="date"> <mat-datepicker #d [startAt]="startDate"></mat-datepicker> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatInputModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerWithMinAndMaxValidation { @ViewChild('d') datepicker!: MatDatepicker<Date>; @ViewChild(NgModel) model!: NgModel; date: Date | null = 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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerWithChangeAndInputEvents { @ViewChild('d') datepicker!: MatDatepicker<Date>; onChange() {} onInput() {} onDateChange() {} onDateInput() {} } @Component({ template: ` <input [matDatepicker]="d" [(ngModel)]="date"> <mat-datepicker #d></mat-datepicker> `, imports: [MatDatepickerInput, MatDatepicker, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerOpeningOnFocus { @ViewChild(MatDatepicker) datepicker!: MatDatepicker<Date>; } @Component({ template: ` <input [matDatepicker]="ch"> <mat-datepicker #ch [calendarHeaderComponent]="customHeaderForDatePicker"></mat-datepicker> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerWithCustomHeader { @ViewChild('ch') datepicker!: MatDatepicker<Date>; customHeaderForDatePicker = CustomHeaderForDatepicker; } @Component({ template: ` <div class="custom-element">Custom element</div> <mat-calendar-header></mat-calendar-header> `, imports: [MatCalendarHeader], changeDetection: ChangeDetectionStrategy.Eager, }) class CustomHeaderForDatepicker {} @Component({ template: ` <input [matDatepicker]="assignedDatepicker" [value]="date"> <mat-datepicker #d [touchUi]="touch"></mat-datepicker> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class DelayedDatepicker { @ViewChild('d') datepicker!: MatDatepicker<Date>; @ViewChild(MatDatepickerInput) datepickerInput!: MatDatepickerInput<Date>; date: Date | null = null; assignedDatepicker!: MatDatepicker<Date>; touch = false; } @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> `, imports: [MatDatepickerInput, MatDatepicker, MatDatepickerToggle, MatDatepickerToggleIcon], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerWithTabindexOnToggle { disabled = false; } @Component({ template: `<mat-datepicker-toggle/>`, imports: [MatDatepickerToggle], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerToggleWithNoDatepicker {} @Component({ template: `<input [matDatepicker]="null!">`, imports: [MatDatepickerInput], changeDetection: ChangeDetectionStrategy.Eager, }) class DatepickerInputWithNoDatepicker {} @Directive({ selector: '[customValidator]', providers: [ { provide: NG_VALIDATORS, useExisting: CustomValidator, multi: true, }, ], }) 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> `, imports: [MatDatepickerInput, MatDatepicker, CustomValidator, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) 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> `, imports: [MatDatepickerInput, MatDatepicker], changeDetection: ChangeDetectionStrategy.Eager, }) class PanelClassDatepicker { date = new Date(0); panelClass!: string | string[]; @ViewChild('d') datepicker!: MatDatepicker<Date>; }