UNPKG

ipsos-components

Version:

Material Design components for Angular

1,370 lines (1,060 loc) 48 kB
import {ENTER, ESCAPE, RIGHT_ARROW} from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import { createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, } from '@angular/cdk/testing'; import {Component, ViewChild} from '@angular/core'; import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import { DEC, JAN, JUL, JUN, MAT_DATE_LOCALE, MatNativeDateModule, NativeDateModule, SEP, } from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MatInputModule} from '../input/index'; import {MatDatepicker} from './datepicker'; import {MatDatepickerInput} from './datepicker-input'; import {MatDatepickerToggle} from './datepicker-toggle'; import {MatDatepickerIntl, MatDatepickerModule} from './index'; describe('MatDatepicker', () => { const SUPPORTS_INTL = typeof Intl != 'undefined'; afterEach(inject([OverlayContainer], (container: OverlayContainer) => { container.getContainerElement().parentNode!.removeChild(container.getContainerElement()); })); describe('with MatNativeDateModule', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ FormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatNativeDateModule, NoopAnimationsModule, ReactiveFormsModule, ], declarations: [ DatepickerWithChangeAndInputEvents, DatepickerWithFilterAndValidation, DatepickerWithFormControl, DatepickerWithISOStrings, DatepickerWithMinAndMaxValidation, DatepickerWithNgModel, DatepickerWithStartAt, DatepickerWithStartView, DatepickerWithToggle, FormFieldDatepicker, MultiInputDatepicker, NoInputDatepicker, StandardDatepicker, DatepickerWithEvents, ], }); TestBed.compileComponents(); })); describe('standard datepicker', () => { let fixture: ComponentFixture<StandardDatepicker>; let testComponent: StandardDatepicker; beforeEach(async(() => { fixture = TestBed.createComponent(StandardDatepicker); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { 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('open touch should open dialog', () => { testComponent.touch = true; fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog mat-dialog-container')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-dialog mat-dialog-container')) .not.toBeNull(); }); it('should open datepicker if opened input is set to true', () => { testComponent.opened = true; fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-content')).not.toBeNull(); testComponent.opened = false; fixture.detectChanges(); expect(document.querySelector('.mat-datepicker-content')).toBeNull(); }); it('open in disabled mode should not open the calendar', () => { testComponent.disabled = true; fixture.detectChanges(); expect(document.querySelector('.cdk-overlay-pane')).toBeNull(); expect(document.querySelector('mat-dialog-container')).toBeNull(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('.cdk-overlay-pane')).toBeNull(); expect(document.querySelector('mat-dialog-container')).toBeNull(); }); it('disabled datepicker input should open the calendar if datepicker is enabled', () => { testComponent.datepicker.disabled = false; testComponent.datepickerInput.disabled = true; 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(); let popup = document.querySelector('.cdk-overlay-pane')!; expect(popup).not.toBeNull(); expect(parseInt(getComputedStyle(popup).height as string)).not.toBe(0); testComponent.datepicker.close(); fixture.detectChanges(); expect(parseInt(getComputedStyle(popup).height as string)).toBe(0); }); it('should close the popup when pressing ESCAPE', () => { testComponent.datepicker.open(); fixture.detectChanges(); let content = document.querySelector('.cdk-overlay-pane mat-datepicker-content')!; expect(content).toBeTruthy('Expected datepicker to be open.'); const keyboardEvent = createKeyboardEvent('keydown', ESCAPE); const stopPropagationSpy = spyOn(keyboardEvent, 'stopPropagation').and.callThrough(); dispatchEvent(content, keyboardEvent); fixture.detectChanges(); content = document.querySelector('.cdk-overlay-pane mat-datepicker-content')!; expect(content).toBeFalsy('Expected datepicker to be closed.'); expect(stopPropagationSpy).toHaveBeenCalled(); expect(keyboardEvent.defaultPrevented) .toBe(true, 'Expected default ESCAPE action to be prevented.'); }); it('close should close dialog', async(() => { testComponent.touch = true; fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).not.toBeNull(); testComponent.datepicker.close(); fixture.detectChanges(); fixture.whenStable().then(() => { expect(document.querySelector('mat-dialog-container')).toBeNull(); }); })); it('setting selected via click should update input and close calendar', async(() => { testComponent.touch = true; fixture.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).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(); fixture.whenStable().then(() => { expect(document.querySelector('mat-dialog-container')).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.detectChanges(); testComponent.datepicker.open(); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).not.toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); let calendarBodyEl = document.querySelector('.mat-calendar-content') as HTMLElement; dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); fixture.detectChanges(); dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); fixture.detectChanges(); fixture.whenStable().then(() => { expect(document.querySelector('mat-dialog-container')).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 selectedChangedSpy = spyOn(testComponent.datepicker.selectedChanged, 'emit').and.callThrough(); 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(selectedChangedSpy.calls.count()).toEqual(1); expect(document.querySelector('mat-dialog-container')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2)); }); it('pressing enter on the currently selected date should close the calendar without ' + 'firing selectedChanged', () => { const selectedChangedSpy = spyOn(testComponent.datepicker.selectedChanged, 'emit').and.callThrough(); testComponent.datepicker.open(); fixture.detectChanges(); let calendarBodyEl = document.querySelector('.mat-calendar-content') as HTMLElement; expect(calendarBodyEl).not.toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER); fixture.detectChanges(); fixture.whenStable().then(() => { expect(selectedChangedSpy.calls.count()).toEqual(0); expect(document.querySelector('mat-dialog-container')).toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1)); }); }); 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.getPopupConnectionElementRef(); expect(attachToRef.nativeElement.tagName.toLowerCase()) .toBe('input', 'popup should be attached to native 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.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; expect(() => fixture.detectChanges()).not.toThrow(); }); }); describe('datepicker with too many inputs', () => { it('should throw when multiple inputs registered', async(() => { let fixture = TestBed.createComponent(MultiInputDatepicker); expect(() => fixture.detectChanges()).toThrow(); })); }); describe('datepicker with no inputs', () => { let fixture: ComponentFixture<NoInputDatepicker>; let testComponent: NoInputDatepicker; beforeEach(async(() => { fixture = TestBed.createComponent(NoInputDatepicker); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { 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', async(() => { expect(() => testComponent.datepicker.open()).toThrow(); })); }); describe('datepicker with startAt', () => { let fixture: ComponentFixture<DatepickerWithStartAt>; let testComponent: DatepickerWithStartAt; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithStartAt); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { 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', () => { let fixture: ComponentFixture<DatepickerWithStartView>; let testComponent: DatepickerWithStartView; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithStartView); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { 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. expect(firstCalendarCell.textContent) .toBe('JAN', 'Expected the calendar to be in year-view'); }); }); describe('datepicker with ngModel', () => { let fixture: ComponentFixture<DatepickerWithNgModel>; let testComponent: DatepickerWithNgModel; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithNgModel); fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); testComponent = fixture.componentInstance; }); })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should update datepicker when model changes', async(() => { expect(testComponent.datepickerInput.value).toBeNull(); expect(testComponent.datepicker._selected).toBeNull(); let selected = new Date(2017, JAN, 1); testComponent.selected = selected; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); expect(testComponent.datepickerInput.value).toEqual(selected); expect(testComponent.datepicker._selected).toEqual(selected); }); })); it('should update model when date is selected', async(() => { expect(testComponent.selected).toBeNull(); expect(testComponent.datepickerInput.value).toBeNull(); let selected = new Date(2017, JAN, 1); testComponent.datepicker._select(selected); fixture.detectChanges(); fixture.whenStable().then(() => { 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'); dispatchFakeEvent(inputEl, 'input'); fixture.detectChanges(); expect(inputEl.classList).toContain('ng-dirty'); }); it('should mark input dirty after date selected', async(() => { 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(); fixture.whenStable().then(() => { fixture.detectChanges(); expect(inputEl.classList).toContain('ng-dirty'); }); })); it('should not mark dirty after model change', async(() => { let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; expect(inputEl.classList).toContain('ng-pristine'); testComponent.selected = new Date(2017, JAN, 1); fixture.detectChanges(); fixture.whenStable().then(() => { 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 touched on calendar selection', async(() => { 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(); fixture.whenStable().then(() => { fixture.detectChanges(); expect(inputEl.classList).toContain('ng-touched'); }); })); }); describe('datepicker with formControl', () => { let fixture: ComponentFixture<DatepickerWithFormControl>; let testComponent: DatepickerWithFormControl; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithFormControl); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should update datepicker when formControl changes', () => { expect(testComponent.datepickerInput.value).toBeNull(); expect(testComponent.datepicker._selected).toBeNull(); let selected = new Date(2017, JAN, 1); testComponent.formControl.setValue(selected); fixture.detectChanges(); expect(testComponent.datepickerInput.value).toEqual(selected); expect(testComponent.datepicker._selected).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); }); }); describe('datepicker with mat-datepicker-toggle', () => { let fixture: ComponentFixture<DatepickerWithToggle>; let testComponent: DatepickerWithToggle; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithToggle); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should open calendar when toggle clicked', () => { expect(document.querySelector('mat-dialog-container')).toBeNull(); let toggle = fixture.debugElement.query(By.css('button')); dispatchMouseEvent(toggle.nativeElement, 'click'); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).not.toBeNull(); }); it('should not open calendar when toggle clicked if datepicker is disabled', () => { testComponent.datepicker.disabled = true; fixture.detectChanges(); const toggle = fixture.debugElement.query(By.css('button')).nativeElement; expect(toggle.hasAttribute('disabled')).toBe(true); expect(document.querySelector('mat-dialog-container')).toBeNull(); dispatchMouseEvent(toggle, 'click'); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).toBeNull(); }); it('should not open calendar when toggle clicked if input is disabled', () => { expect(testComponent.datepicker.disabled).toBe(false); testComponent.input.disabled = true; fixture.detectChanges(); const toggle = fixture.debugElement.query(By.css('button')).nativeElement; expect(toggle.hasAttribute('disabled')).toBe(true); expect(document.querySelector('mat-dialog-container')).toBeNull(); dispatchMouseEvent(toggle, 'click'); fixture.detectChanges(); expect(document.querySelector('mat-dialog-container')).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', () => { let toggle = fixture.debugElement.query(By.css('button')).nativeElement; fixture.componentInstance.touchUI = false; fixture.detectChanges(); toggle.focus(); expect(document.activeElement).toBe(toggle, 'Expected toggle to be focused.'); fixture.componentInstance.datepicker.open(); fixture.detectChanges(); let pane = document.querySelector('.cdk-overlay-pane')!; expect(pane).toBeTruthy('Expected calendar to be open.'); expect(pane.contains(document.activeElement)) .toBe(true, 'Expected focus to be inside the calendar.'); fixture.componentInstance.datepicker.close(); fixture.detectChanges(); expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.'); }); 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?'); })); }); describe('datepicker inside mat-form-field', () => { let fixture: ComponentFixture<FormFieldDatepicker>; let testComponent: FormFieldDatepicker; beforeEach(async(() => { fixture = TestBed.createComponent(FormFieldDatepicker); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should attach popup to mat-form-field underline', () => { let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef(); expect(attachToRef.nativeElement.classList.contains('mat-form-field-underline')) .toBe(true, 'popup should be attached to mat-form-field underline'); }); it('should float the placeholder when an invalid value is entered', () => { testComponent.datepickerInput.value = 'totally-not-a-date' as any; fixture.debugElement.nativeElement.querySelector('input').value = 'totally-not-a-date'; fixture.detectChanges(); expect(fixture.debugElement.nativeElement.querySelector('mat-form-field').classList) .toContain('mat-form-field-should-float'); }); }); describe('datepicker with min and max dates and validation', () => { let fixture: ComponentFixture<DatepickerWithMinAndMaxValidation>; let testComponent: DatepickerWithMinAndMaxValidation; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithMinAndMaxValidation); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should use min and max dates specified by the input', () => { expect(testComponent.datepicker._minDate).toEqual(new Date(2010, JAN, 1)); expect(testComponent.datepicker._maxDate).toEqual(new Date(2020, JAN, 1)); }); it('should mark invalid when value is before min', async(() => { testComponent.date = new Date(2009, DEC, 31); fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); 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.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); 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._minDate; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); 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._maxDate; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); 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.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); expect(fixture.debugElement.query(By.css('input')).nativeElement.classList) .not.toContain('ng-invalid'); }); })); }); describe('datepicker with filter and validation', () => { let fixture: ComponentFixture<DatepickerWithFilterAndValidation>; let testComponent: DatepickerWithFilterAndValidation; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithFilterAndValidation); fixture.detectChanges(); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should mark input invalid', async(() => { testComponent.date = new Date(2017, JAN, 1); fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); expect(fixture.debugElement.query(By.css('input')).nativeElement.classList) .toContain('ng-invalid'); testComponent.date = new Date(2017, JAN, 2); fixture.detectChanges(); fixture.whenStable().then(() => { 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-dialog-container')).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'); }); }); describe('datepicker with change and input events', () => { let fixture: ComponentFixture<DatepickerWithChangeAndInputEvents>; let testComponent: DatepickerWithChangeAndInputEvents; let inputEl: HTMLInputElement; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithChangeAndInputEvents); 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(async(() => { 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(); 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-dialog-container')).not.toBeNull(); let 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(); }); }); describe('with ISO 8601 strings as input', () => { let fixture: ComponentFixture<DatepickerWithISOStrings>; let testComponent: DatepickerWithISOStrings; beforeEach(async(() => { fixture = TestBed.createComponent(DatepickerWithISOStrings); testComponent = fixture.componentInstance; })); afterEach(async(() => { testComponent.datepicker.close(); fixture.detectChanges(); })); it('should coerce ISO strings', async(() => { expect(() => fixture.detectChanges()).not.toThrow(); fixture.whenStable().then(() => { 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(async(() => { fixture = TestBed.createComponent(DatepickerWithEvents); 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('with missing DateAdapter and MAT_DATE_FORMATS', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ FormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, NoopAnimationsModule, ReactiveFormsModule, ], declarations: [StandardDatepicker], }); TestBed.compileComponents(); })); it('should throw when created', () => { expect(() => TestBed.createComponent(StandardDatepicker)) .toThrowError(/MatDatepicker: No provider found for .*/); }); }); describe('popup positioning', () => { let fixture: ComponentFixture<StandardDatepicker>; let testComponent: StandardDatepicker; let input: HTMLElement; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ MatDatepickerModule, MatFormFieldModule, MatInputModule, MatNativeDateModule, NoopAnimationsModule ], declarations: [StandardDatepicker], }).compileComponents(); fixture = TestBed.createComponent(StandardDatepicker); 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)) .toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.'); expect(Math.floor(overlayRect.left)) .toBe(Math.floor(inputRect.left), 'Expected popup to align to input 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)) .toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.'); expect(Math.floor(overlayRect.left)) .toBe(Math.floor(inputRect.left), 'Expected popup to align to input 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)) .toBe(Math.floor(inputRect.bottom), 'Expected popup to align to input bottom.'); expect(Math.floor(overlayRect.right)) .toBe(Math.floor(inputRect.right), 'Expected popup to align to input 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)) .toBe(Math.floor(inputRect.top), 'Expected popup to align to input top.'); expect(Math.floor(overlayRect.right)) .toBe(Math.floor(inputRect.right), 'Expected popup to align to input right.'); }); }); describe('internationalization', () => { let fixture: ComponentFixture<DatepickerWithi18n>; let testComponent: DatepickerWithi18n; let input: HTMLInputElement; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ MatDatepickerModule, MatFormFieldModule, MatInputModule, MatNativeDateModule, NoopAnimationsModule, NativeDateModule, FormsModule ], providers: [{provide: MAT_DATE_LOCALE, useValue: 'de-DE'}], declarations: [DatepickerWithi18n], }).compileComponents(); fixture = TestBed.createComponent(DatepickerWithi18n); 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.detectChanges(); fixture.whenStable().then(() => { 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); }); })); }); }); @Component({ template: ` <input [matDatepicker]="d" [value]="date"> <mat-datepicker #d [touchUi]="touch" [disabled]="disabled" [opened]="opened"></mat-datepicker> `, }) class StandardDatepicker { opened = false; touch = false; disabled = false; date: Date | null = new Date(2020, JAN, 1); @ViewChild('d') datepicker: MatDatepicker<Date>; @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>; } @Component({ template: ` <input [matDatepicker]="d"><input [matDatepicker]="d"><mat-datepicker #d></mat-datepicker> `, }) class MultiInputDatepicker {} @Component({ template: `<mat-datepicker #d></mat-datepicker>`, }) class NoInputDatepicker { @ViewChild('d') datepicker: MatDatepicker<Date>; } @Component({ template: ` <input [matDatepicker]="d" [value]="date"> <mat-datepicker #d [startAt]="startDate"></mat-datepicker> `, }) 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"></mat-datepicker> `, }) class DatepickerWithStartView { date = new Date(2020, JAN, 1); @ViewChild('d') datepicker: MatDatepicker<Date>; } @Component({ template: ` <input [(ngModel)]="selected" [matDatepicker]="d"> <mat-datepicker #d></mat-datepicker> `, }) 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> `, }) class DatepickerWithFormControl { formControl = new FormControl(); @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"></mat-datepicker-toggle> <mat-datepicker #d [touchUi]="touchUI"></mat-datepicker> `, }) class DatepickerWithToggle { @ViewChild('d') datepicker: MatDatepicker<Date>; @ViewChild(MatDatepickerInput) input: MatDatepickerInput<Date>; touchUI = true; } @Component({ template: ` <mat-form-field> <input matInput [matDatepicker]="d"> <mat-datepicker #d></mat-datepicker> </mat-form-field> `, }) class FormFieldDatepicker { @ViewChild('d') datepicker: MatDatepicker<Date>; @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>; } @Component({ template: ` <input [matDatepicker]="d" [(ngModel)]="date" [min]="minDate" [max]="maxDate"> <mat-datepicker-toggle [for]="d"></mat-datepicker-toggle> <mat-datepicker #d></mat-datepicker> `, }) class DatepickerWithMinAndMaxValidation { @ViewChild('d') datepicker: MatDatepicker<Date>; 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> `, }) class DatepickerWithFilterAndValidation { @ViewChild('d') datepicker: MatDatepicker<Date>; date: Date; filter = (date: Date) => date.getDate() != 1; } @Component({ template: ` <input [matDatepicker]="d" (change)="onChange()" (input)="onInput()" (dateChange)="onDateChange()" (dateInput)="onDateInput()"> <mat-datepicker #d [touchUi]="true"></mat-datepicker> ` }) class DatepickerWithChangeAndInputEvents { @ViewChild('d') datepicker: MatDatepicker<Date>; onChange() {} onInput() {} onDateChange() {} onDateInput() {} } @Component({ template: ` <input [matDatepicker]="d" [(ngModel)]="date"> <mat-datepicker #d></mat-datepicker> ` }) 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> ` }) 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> `, }) class DatepickerWithEvents { selected: Date | null = null; openedSpy = jasmine.createSpy('opened spy'); closedSpy = jasmine.createSpy('closed spy'); @ViewChild('d') datepicker: MatDatepicker<Date>; }