import {FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing/private'; import {Component, Directive, ElementRef, Provider, Type, ViewChild} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush, inject, tick} from '@angular/core/testing'; import { FormControl, FormGroup, FormsModule, NG_VALIDATORS, NgModel, ReactiveFormsModule, Validator, Validators, } from '@angular/forms'; import {ErrorStateMatcher, MatNativeDateModule} from '@angular/material/core'; import {MatFormField, MatFormFieldModule, MatLabel} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subscription} from 'rxjs'; import {MatDateRangeInput} from './date-range-input'; import {MatEndDate, MatStartDate} from './date-range-input-parts'; import {MatDateRangePicker} from './date-range-picker'; import {MatDatepickerModule} from './datepicker-module'; describe('MatDateRangeInput', () => { function createComponent(component: Type, providers: Provider[] = []): ComponentFixture { TestBed.configureTestingModule({ imports: [ FormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, NoopAnimationsModule, ReactiveFormsModule, MatNativeDateModule, component, ], providers, }); return TestBed.createComponent(component); } it('should mirror the input value from the start into the mirror element', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const mirror = fixture.nativeElement.querySelector('.mat-date-range-input-mirror'); const startInput = fixture.componentInstance.start.nativeElement; expect(mirror.textContent).toBe('Start Date'); startInput.value = 'hello'; dispatchFakeEvent(startInput, 'input'); fixture.detectChanges(); expect(mirror.textContent).toBe('hello'); startInput.value = 'h'; dispatchFakeEvent(startInput, 'input'); fixture.detectChanges(); expect(mirror.textContent).toBe('h'); startInput.value = ''; dispatchFakeEvent(startInput, 'input'); fixture.detectChanges(); expect(mirror.textContent).toBe('Start Date'); }); it('should hide the mirror value from assistive technology', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const mirror = fixture.nativeElement.querySelector('.mat-date-range-input-mirror'); expect(mirror.getAttribute('aria-hidden')).toBe('true'); }); it('should be able to customize the separator', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const separator = fixture.nativeElement.querySelector('.mat-date-range-input-separator'); expect(separator.textContent).toBe('–'); fixture.componentInstance.separator = '/'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(separator.textContent).toBe('/'); }); it('should set the proper type on the input elements', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); expect(fixture.componentInstance.start.nativeElement.getAttribute('type')).toBe('text'); expect(fixture.componentInstance.end.nativeElement.getAttribute('type')).toBe('text'); }); it('should set the correct role on the range input', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input'); expect(rangeInput.getAttribute('role')).toBe('group'); }); it('should mark the entire range input as disabled if both inputs are disabled', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {rangeInput, range, start, end} = fixture.componentInstance; expect(rangeInput.disabled).toBe(false); expect(start.nativeElement.disabled).toBe(false); expect(end.nativeElement.disabled).toBe(false); range.controls.start.disable(); fixture.detectChanges(); expect(rangeInput.disabled).toBe(false); expect(start.nativeElement.disabled).toBe(true); expect(end.nativeElement.disabled).toBe(false); range.controls.end.disable(); fixture.detectChanges(); expect(rangeInput.disabled).toBe(true); expect(start.nativeElement.disabled).toBe(true); expect(end.nativeElement.disabled).toBe(true); }); it('should disable both inputs if the range is disabled', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance; expect(start.nativeElement.disabled).toBe(false); expect(end.nativeElement.disabled).toBe(false); fixture.componentInstance.rangeDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(start.nativeElement.disabled).toBe(true); expect(end.nativeElement.disabled).toBe(true); }); it('should hide the placeholders once the start input has a value', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const hideClass = 'mat-date-range-input-hide-placeholders'; const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input'); const startInput = fixture.componentInstance.start.nativeElement; expect(rangeInput.classList).not.toContain(hideClass); startInput.value = 'hello'; dispatchFakeEvent(startInput, 'input'); fixture.detectChanges(); expect(rangeInput.classList).toContain(hideClass); }); it('should point the range input aria-labelledby to the form field label', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const labelId = fixture.nativeElement.querySelector('label').id; const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input'); expect(labelId).toBeTruthy(); expect(rangeInput.getAttribute('aria-labelledby')).toBe(labelId); }); it('should point the range input aria-labelledby to the form field hint element', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const labelId = fixture.nativeElement.querySelector('.mat-mdc-form-field-hint').id; const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input'); expect(labelId).toBeTruthy(); expect(rangeInput.getAttribute('aria-describedby')).toBe(labelId); }); it('should not set aria-labelledby if the form field does not have a label', () => { const fixture = createComponent(RangePickerNoLabel); fixture.detectChanges(); const {start, end} = fixture.componentInstance; expect(start.nativeElement.getAttribute('aria-labelledby')).toBeFalsy(); expect(end.nativeElement.getAttribute('aria-labelledby')).toBeFalsy(); }); it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const label: HTMLElement = fixture.nativeElement.querySelector('label'); expect(label).toBeTruthy(); expect(label.getAttribute('id')).toBeTruthy(); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const popup = document.querySelector('.cdk-overlay-pane .mat-datepicker-content-container')!; expect(popup).toBeTruthy(); expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id')); })); it('should float the form field label when either input is focused', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {rangeInput, end} = fixture.componentInstance; let focusMonitor: FocusMonitor; inject([FocusMonitor], (fm: FocusMonitor) => { focusMonitor = fm; })(); expect(rangeInput.shouldLabelFloat).toBe(false); focusMonitor!.focusVia(end, 'keyboard'); fixture.detectChanges(); expect(rangeInput.shouldLabelFloat).toBe(true); }); it('should float the form field label when either input has a value', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {rangeInput, end} = fixture.componentInstance; expect(rangeInput.shouldLabelFloat).toBe(false); end.nativeElement.value = 'hello'; dispatchFakeEvent(end.nativeElement, 'input'); fixture.detectChanges(); expect(rangeInput.shouldLabelFloat).toBe(true); }); it('should consider the entire input as empty if both inputs are empty', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {rangeInput, end} = fixture.componentInstance; expect(rangeInput.empty).toBe(true); end.nativeElement.value = 'hello'; dispatchFakeEvent(end.nativeElement, 'input'); fixture.detectChanges(); expect(rangeInput.empty).toBe(false); }); it('should mark the range controls as invalid if the start value is after the end value', fakeAsync(() => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); tick(); const {start, end} = fixture.componentInstance.range.controls; // The default error state matcher only checks if the controls have been touched. // Set it manually here so we can assert `rangeInput.errorState` correctly. fixture.componentInstance.range.markAllAsTouched(); expect(fixture.componentInstance.rangeInput.errorState).toBe(false); expect(start.errors?.['matStartDateInvalid']).toBeFalsy(); expect(end.errors?.['matEndDateInvalid']).toBeFalsy(); start.setValue(new Date(2020, 2, 2)); end.setValue(new Date(2020, 1, 2)); fixture.detectChanges(); expect(fixture.componentInstance.rangeInput.errorState).toBe(true); expect(start.errors?.['matStartDateInvalid']).toBeTruthy(); expect(end.errors?.['matEndDateInvalid']).toBeTruthy(); end.setValue(new Date(2020, 3, 2)); fixture.detectChanges(); expect(fixture.componentInstance.rangeInput.errorState).toBe(false); expect(start.errors?.['matStartDateInvalid']).toBeFalsy(); expect(end.errors?.['matEndDateInvalid']).toBeFalsy(); })); it('should pass the minimum date from the range input to the inner inputs', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.minDate = new Date(2020, 3, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; expect(start.errors?.['matDatepickerMin']).toBeFalsy(); expect(end.errors?.['matDatepickerMin']).toBeFalsy(); const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); expect(start.errors?.['matDatepickerMin']).toBeTruthy(); expect(end.errors?.['matDatepickerMin']).toBeTruthy(); }); it('should pass the maximum date from the range input to the inner inputs', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.maxDate = new Date(2020, 1, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; expect(start.errors?.['matDatepickerMax']).toBeFalsy(); expect(end.errors?.['matDatepickerMax']).toBeFalsy(); const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); expect(start.errors?.['matDatepickerMax']).toBeTruthy(); expect(end.errors?.['matDatepickerMax']).toBeTruthy(); }); it('should pass the date filter function from the range input to the inner inputs', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.dateFilter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; expect(start.errors?.['matDatepickerFilter']).toBeFalsy(); expect(end.errors?.['matDatepickerFilter']).toBeFalsy(); const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); expect(start.errors?.['matDatepickerFilter']).toBeTruthy(); expect(end.errors?.['matDatepickerFilter']).toBeTruthy(); }); it('should should revalidate when a new date filter function is assigned', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); const spy = jasmine.createSpy('change spy'); const subscription = new Subscription(); subscription.add(start.valueChanges.subscribe(spy)); subscription.add(end.valueChanges.subscribe(spy)); fixture.componentInstance.dateFilter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(2); fixture.componentInstance.dateFilter = () => true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(4); subscription.unsubscribe(); }); it( 'should not dispatch the change event if a new filter function with the same result ' + 'is assigned', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); const spy = jasmine.createSpy('change spy'); const subscription = new Subscription(); subscription.add(start.valueChanges.subscribe(spy)); subscription.add(end.valueChanges.subscribe(spy)); fixture.componentInstance.dateFilter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(2); fixture.componentInstance.dateFilter = () => false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(2); subscription.unsubscribe(); }, ); it('should throw if there is no start input', () => { expect(() => { const fixture = createComponent(RangePickerNoStart); fixture.detectChanges(); }).toThrowError('mat-date-range-input must contain a matStartDate input'); }); it('should throw if there is no end input', () => { expect(() => { const fixture = createComponent(RangePickerNoEnd); fixture.detectChanges(); }).toThrowError('mat-date-range-input must contain a matEndDate input'); }); it('should focus the start input when clicking on the form field', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const startInput = fixture.componentInstance.start.nativeElement; const formFieldContainer = fixture.nativeElement.querySelector('.mat-mdc-text-field-wrapper'); spyOn(startInput, 'focus').and.callThrough(); formFieldContainer.click(); fixture.detectChanges(); expect(startInput.focus).toHaveBeenCalled(); }); it('should focus the end input when clicking on the form field when start has a value', fakeAsync(() => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); tick(); const endInput = fixture.componentInstance.end.nativeElement; const formFieldContainer = fixture.nativeElement.querySelector('.mat-mdc-text-field-wrapper'); spyOn(endInput, 'focus').and.callThrough(); fixture.componentInstance.range.controls.start.setValue(new Date()); fixture.detectChanges(); formFieldContainer.click(); fixture.detectChanges(); tick(); expect(endInput.focus).toHaveBeenCalled(); })); it('should revalidate if a validation field changes', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.minDate = new Date(2020, 3, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {start, end} = fixture.componentInstance.range.controls; const date = new Date(2020, 2, 2); start.setValue(date); end.setValue(date); fixture.detectChanges(); expect(start.errors?.['matDatepickerMin']).toBeTruthy(); expect(end.errors?.['matDatepickerMin']).toBeTruthy(); fixture.componentInstance.minDate = new Date(2019, 3, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(start.errors?.['matDatepickerMin']).toBeFalsy(); expect(end.errors?.['matDatepickerMin']).toBeFalsy(); }); it('should set the formatted date value as the input value', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.minDate = new Date(2020, 3, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const date = new Date(2020, 1, 2); const {start, end, range} = fixture.componentInstance; range.controls.start.setValue(date); range.controls.end.setValue(date); fixture.detectChanges(); expect(start.nativeElement.value).toBe('2/2/2020'); expect(end.nativeElement.value).toBe('2/2/2020'); }); it('should parse the value typed into an input to a date', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const expectedDate = new Date(2020, 1, 2); const {start, end, range} = fixture.componentInstance; start.nativeElement.value = '2/2/2020'; dispatchFakeEvent(start.nativeElement, 'input'); fixture.detectChanges(); expect(range.controls.start.value).toEqual(expectedDate); end.nativeElement.value = '2/2/2020'; dispatchFakeEvent(end.nativeElement, 'input'); fixture.detectChanges(); expect(range.controls.end.value).toEqual(expectedDate); }); it('should set the min and max attributes on inputs based on the values from the wrapper', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.minDate = new Date(2020, 1, 2); fixture.componentInstance.maxDate = new Date(2020, 1, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const {start, end} = fixture.componentInstance; // Use `toContain` for the asserts here, because different browsers format the value // differently and we only care that some kind of date value made it to the attribute. expect(start.nativeElement.getAttribute('min')).toContain('2020'); expect(start.nativeElement.getAttribute('max')).toContain('2020'); expect(end.nativeElement.getAttribute('min')).toContain('2020'); expect(end.nativeElement.getAttribute('max')).toContain('2020'); }); it('should pass the range input value through to the calendar', fakeAsync(() => { const fixture = createComponent(StandardRangePicker); const {start, end} = fixture.componentInstance.range.controls; let overlayContainerElement: HTMLElement; start.setValue(new Date(2020, 1, 2)); end.setValue(new Date(2020, 1, 5)); inject([OverlayContainer], (overlayContainer: OverlayContainer) => { overlayContainerElement = overlayContainer.getContainerElement(); })(); fixture.detectChanges(); tick(); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const rangeTexts = Array.from( overlayContainerElement!.querySelectorAll( [ '.mat-calendar-body-range-start', '.mat-calendar-body-in-range', '.mat-calendar-body-range-end', ].join(','), ), ).map(cell => cell.textContent!.trim()); expect(rangeTexts).toEqual(['2', '3', '4', '5']); })); it("should have aria-desciredby on start and end date cells that point to the 's accessible name", fakeAsync(() => { const fixture = createComponent(StandardRangePicker); const {start, end} = fixture.componentInstance.range.controls; let overlayContainerElement: HTMLElement; start.setValue(new Date(2020, 1, 2)); end.setValue(new Date(2020, 1, 5)); inject([OverlayContainer], (overlayContainer: OverlayContainer) => { overlayContainerElement = overlayContainer.getContainerElement(); })(); fixture.detectChanges(); tick(); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const rangeStart = overlayContainerElement!.querySelector('.mat-calendar-body-range-start'); const rangeEnd = overlayContainerElement!.querySelector('.mat-calendar-body-range-end'); // query for targets of `aria-describedby`. Query from document instead of fixture.nativeElement as calendar UI is rendered in an overlay. const rangeStartDescriptions = Array.from( document.querySelectorAll( rangeStart! .getAttribute('aria-describedby')! .split(/\s+/g) .map(x => `#${x}`) .join(' '), ), ); const rangeEndDescriptions = Array.from( document.querySelectorAll( rangeEnd! .getAttribute('aria-describedby')! .split(/\s+/g) .map(x => `#${x}`) .join(' '), ), ); expect(rangeStartDescriptions) .withContext('target of aria-descriedby should exist') .not.toBeNull(); expect(rangeEndDescriptions) .withContext('target of aria-descriedby should exist') .not.toBeNull(); expect( rangeStartDescriptions .map(x => x.textContent) .join(' ') .trim(), ).toEqual('Start date'); expect( rangeEndDescriptions .map(x => x.textContent) .join(' ') .trim(), ).toEqual('End date'); })); it('should pass the comparison range through to the calendar', fakeAsync(() => { const fixture = createComponent(StandardRangePicker); let overlayContainerElement: HTMLElement; // Set startAt to guarantee that the calendar opens on the proper month. fixture.componentInstance.comparisonStart = fixture.componentInstance.startAt = new Date( 2020, 1, 2, ); fixture.componentInstance.comparisonEnd = new Date(2020, 1, 5); fixture.changeDetectorRef.markForCheck(); inject([OverlayContainer], (overlayContainer: OverlayContainer) => { overlayContainerElement = overlayContainer.getContainerElement(); })(); fixture.detectChanges(); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const rangeTexts = Array.from( overlayContainerElement!.querySelectorAll( [ '.mat-calendar-body-comparison-start', '.mat-calendar-body-in-comparison-range', '.mat-calendar-body-comparison-end', ].join(','), ), ).map(cell => cell.textContent!.trim()); expect(rangeTexts).toEqual(['2', '3', '4', '5']); })); it('should preserve the preselected values when assigning through ngModel', fakeAsync(() => { const start = new Date(2020, 1, 2); const end = new Date(2020, 1, 2); const fixture = createComponent(RangePickerNgModel); fixture.componentInstance.start = start; fixture.componentInstance.end = end; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(fixture.componentInstance.start).toBe(start); expect(fixture.componentInstance.end).toBe(end); })); it('should preserve the values when assigning both together through ngModel', fakeAsync(() => { const assignAndAssert = (start: Date, end: Date) => { fixture.componentInstance.start = start; fixture.componentInstance.end = end; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(fixture.componentInstance.start).toBe(start); expect(fixture.componentInstance.end).toBe(end); }; const fixture = createComponent(RangePickerNgModel); fixture.detectChanges(); assignAndAssert(new Date(2020, 1, 2), new Date(2020, 1, 5)); assignAndAssert(new Date(2020, 2, 2), new Date(2020, 2, 5)); })); it('should not be dirty on init when there is no value', fakeAsync(() => { const fixture = createComponent(RangePickerNgModel); fixture.detectChanges(); flush(); const {startModel, endModel} = fixture.componentInstance; expect(startModel.dirty).toBe(false); expect(startModel.touched).toBe(false); expect(endModel.dirty).toBe(false); expect(endModel.touched).toBe(false); })); it('should not be dirty on init when there is a value', fakeAsync(() => { const fixture = createComponent(RangePickerNgModel); fixture.componentInstance.start = new Date(2020, 1, 2); fixture.componentInstance.end = new Date(2020, 2, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); const {startModel, endModel} = fixture.componentInstance; expect(startModel.dirty).toBe(false); expect(startModel.touched).toBe(false); expect(endModel.dirty).toBe(false); expect(endModel.touched).toBe(false); })); it('should mark the input as dirty once the user types in it', fakeAsync(() => { const fixture = createComponent(RangePickerNgModel); fixture.componentInstance.start = new Date(2020, 1, 2); fixture.componentInstance.end = new Date(2020, 2, 2); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); const {startModel, endModel, startInput, endInput} = fixture.componentInstance; expect(startModel.dirty).toBe(false); expect(endModel.dirty).toBe(false); endInput.nativeElement.value = '30/12/2020'; dispatchFakeEvent(endInput.nativeElement, 'input'); fixture.detectChanges(); flush(); fixture.detectChanges(); expect(startModel.dirty).toBe(false); expect(endModel.dirty).toBe(true); startInput.nativeElement.value = '12/12/2020'; dispatchFakeEvent(startInput.nativeElement, 'input'); fixture.detectChanges(); flush(); fixture.detectChanges(); expect(startModel.dirty).toBe(true); expect(endModel.dirty).toBe(true); })); it('should mark both inputs as touched when the range picker is closed', fakeAsync(() => { const fixture = createComponent(RangePickerNgModel); fixture.detectChanges(); flush(); const {startModel, endModel, rangePicker} = fixture.componentInstance; expect(startModel.dirty).toBe(false); expect(startModel.touched).toBe(false); expect(endModel.dirty).toBe(false); expect(endModel.touched).toBe(false); rangePicker.open(); fixture.detectChanges(); tick(); flush(); expect(startModel.dirty).toBe(false); expect(startModel.touched).toBe(false); expect(endModel.dirty).toBe(false); expect(endModel.touched).toBe(false); rangePicker.close(); fixture.detectChanges(); flush(); expect(startModel.dirty).toBe(false); expect(startModel.touched).toBe(true); expect(endModel.dirty).toBe(false); expect(endModel.touched).toBe(true); })); it('should move focus to the start input when pressing backspace on an empty end input', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance; spyOn(start.nativeElement, 'focus').and.callThrough(); end.nativeElement.value = ''; dispatchKeyboardEvent(end.nativeElement, 'keydown', BACKSPACE); fixture.detectChanges(); expect(start.nativeElement.focus).toHaveBeenCalled(); }); it('should move not move focus when pressing backspace if the end input has a value', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance; spyOn(start.nativeElement, 'focus').and.callThrough(); end.nativeElement.value = '10/10/2020'; dispatchKeyboardEvent(end.nativeElement, 'keydown', BACKSPACE); fixture.detectChanges(); expect(start.nativeElement.focus).not.toHaveBeenCalled(); }); it('moves focus between fields with arrow keys when cursor is at edge (LTR)', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, end} = fixture.componentInstance; start.nativeElement.value = '09/10/2020'; end.nativeElement.value = '10/10/2020'; start.nativeElement.focus(); start.nativeElement.setSelectionRange(9, 9); dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); start.nativeElement.setSelectionRange(10, 10); dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); start.nativeElement.setSelectionRange(10, 10); dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(1, 1); dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(0, 0); dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(0, 0); dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); }); it('moves focus between fields with arrow keys when cursor is at edge (RTL)', () => { class RTL extends Directionality { override readonly value = 'rtl'; } const fixture = createComponent(StandardRangePicker, [ { provide: Directionality, useFactory: () => new RTL(null), }, ]); fixture.detectChanges(); const {start, end} = fixture.componentInstance; start.nativeElement.value = '09/10/2020'; end.nativeElement.value = '10/10/2020'; start.nativeElement.focus(); start.nativeElement.setSelectionRange(9, 9); dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); start.nativeElement.setSelectionRange(10, 10); dispatchKeyboardEvent(start.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); start.nativeElement.setSelectionRange(10, 10); dispatchKeyboardEvent(start.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(1, 1); dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(0, 0); dispatchKeyboardEvent(end.nativeElement, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(end.nativeElement); end.nativeElement.setSelectionRange(0, 0); dispatchKeyboardEvent(end.nativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(start.nativeElement); }); it('should be able to get the input placeholder', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); expect(fixture.componentInstance.rangeInput.placeholder).toBe('Start Date – End Date'); }); it('should emit to the stateChanges stream when typing a value into an input', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {start, rangeInput} = fixture.componentInstance; const spy = jasmine.createSpy('stateChanges spy'); const subscription = rangeInput.stateChanges.subscribe(spy); start.nativeElement.value = '10/10/2020'; dispatchFakeEvent(start.nativeElement, 'input'); fixture.detectChanges(); expect(spy).toHaveBeenCalled(); subscription.unsubscribe(); }); it('should emit to the dateChange event only when typing in the relevant input', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {startInput, endInput, start, end} = fixture.componentInstance; const startSpy = jasmine.createSpy('matStartDate spy'); const endSpy = jasmine.createSpy('matEndDate spy'); const startSubscription = startInput.dateChange.subscribe(startSpy); const endSubscription = endInput.dateChange.subscribe(endSpy); start.nativeElement.value = '10/10/2020'; dispatchFakeEvent(start.nativeElement, 'change'); fixture.detectChanges(); expect(startSpy).toHaveBeenCalledTimes(1); expect(endSpy).not.toHaveBeenCalled(); start.nativeElement.value = '11/10/2020'; dispatchFakeEvent(start.nativeElement, 'change'); fixture.detectChanges(); expect(startSpy).toHaveBeenCalledTimes(2); expect(endSpy).not.toHaveBeenCalled(); end.nativeElement.value = '11/10/2020'; dispatchFakeEvent(end.nativeElement, 'change'); fixture.detectChanges(); expect(startSpy).toHaveBeenCalledTimes(2); expect(endSpy).toHaveBeenCalledTimes(1); end.nativeElement.value = '12/10/2020'; dispatchFakeEvent(end.nativeElement, 'change'); fixture.detectChanges(); expect(startSpy).toHaveBeenCalledTimes(2); expect(endSpy).toHaveBeenCalledTimes(2); startSubscription.unsubscribe(); endSubscription.unsubscribe(); }); it('should emit to the dateChange event when setting the value programmatically', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const {startInput, endInput} = fixture.componentInstance; const {start, end} = fixture.componentInstance.range.controls; const startSpy = jasmine.createSpy('matStartDate spy'); const endSpy = jasmine.createSpy('matEndDate spy'); const startSubscription = startInput.dateChange.subscribe(startSpy); const endSubscription = endInput.dateChange.subscribe(endSpy); start.setValue(new Date(2020, 1, 2)); end.setValue(new Date(2020, 2, 2)); fixture.detectChanges(); expect(startSpy).not.toHaveBeenCalled(); expect(endSpy).not.toHaveBeenCalled(); start.setValue(new Date(2020, 3, 2)); end.setValue(new Date(2020, 4, 2)); fixture.detectChanges(); expect(startSpy).not.toHaveBeenCalled(); expect(endSpy).not.toHaveBeenCalled(); startSubscription.unsubscribe(); endSubscription.unsubscribe(); }); it('should not trigger validators if new date object for same date is set for `min`', () => { const fixture = createComponent(RangePickerWithCustomValidator, [CustomValidator]); fixture.detectChanges(); const minDate = new Date(2019, 0, 1); const validator = fixture.componentInstance.validator; validator.validate.calls.reset(); fixture.componentInstance.min = minDate; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(validator.validate).toHaveBeenCalledTimes(1); fixture.componentInstance.min = new Date(minDate); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(validator.validate).toHaveBeenCalledTimes(1); }); it('should not trigger validators if new date object for same date is set for `max`', () => { const fixture = createComponent(RangePickerWithCustomValidator, [CustomValidator]); fixture.detectChanges(); const maxDate = new Date(2120, 0, 1); const validator = fixture.componentInstance.validator; validator.validate.calls.reset(); fixture.componentInstance.max = maxDate; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(validator.validate).toHaveBeenCalledTimes(1); fixture.componentInstance.max = new Date(maxDate); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(validator.validate).toHaveBeenCalledTimes(1); }); it('should not emit to `stateChanges` if new date object for same date is set for `min`', () => { const fixture = createComponent(StandardRangePicker); fixture.detectChanges(); const minDate = new Date(2019, 0, 1); const spy = jasmine.createSpy('stateChanges spy'); const subscription = fixture.componentInstance.rangeInput.stateChanges.subscribe(spy); fixture.componentInstance.minDate = minDate; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); fixture.componentInstance.minDate = 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(StandardRangePicker); fixture.detectChanges(); const maxDate = new Date(2120, 0, 1); const spy = jasmine.createSpy('stateChanges spy'); const subscription = fixture.componentInstance.rangeInput.stateChanges.subscribe(spy); fixture.componentInstance.maxDate = maxDate; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); fixture.componentInstance.maxDate = new Date(maxDate); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); subscription.unsubscribe(); }); it('should be able to pass in a different error state matcher through an input', () => { const fixture = createComponent(RangePickerErrorStateMatcher); fixture.detectChanges(); const {startInput, endInput, matcher} = fixture.componentInstance; expect(startInput.errorStateMatcher).toBe(matcher); expect(endInput.errorStateMatcher).toBe(matcher); }); it('should only update model for input that changed', fakeAsync(() => { const fixture = createComponent(RangePickerNgModel); fixture.detectChanges(); tick(); expect(fixture.componentInstance.startDateModelChangeCount).toBe(0); expect(fixture.componentInstance.endDateModelChangeCount).toBe(0); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const fromDate = new Date(2020, 0, 1); const toDate = new Date(2020, 0, 2); fixture.componentInstance.rangePicker.select(fromDate); fixture.detectChanges(); tick(); expect(fixture.componentInstance.startDateModelChangeCount) .withContext('Start Date set once') .toBe(1); expect(fixture.componentInstance.endDateModelChangeCount) .withContext('End Date not set') .toBe(0); fixture.componentInstance.rangePicker.select(toDate); fixture.detectChanges(); tick(); expect(fixture.componentInstance.startDateModelChangeCount) .withContext('Start Date unchanged (set once)') .toBe(1); expect(fixture.componentInstance.endDateModelChangeCount) .withContext('End Date set once') .toBe(1); fixture.componentInstance.rangePicker.open(); fixture.detectChanges(); tick(); const fromDate2 = new Date(2021, 0, 1); const toDate2 = new Date(2021, 0, 2); fixture.componentInstance.rangePicker.select(fromDate2); fixture.detectChanges(); tick(); expect(fixture.componentInstance.startDateModelChangeCount) .withContext('Start Date set twice') .toBe(2); expect(fixture.componentInstance.endDateModelChangeCount) .withContext('End Date set twice (nulled)') .toBe(2); fixture.componentInstance.rangePicker.select(toDate2); fixture.detectChanges(); tick(); expect(fixture.componentInstance.startDateModelChangeCount) .withContext('Start Date unchanged (set twice)') .toBe(2); expect(fixture.componentInstance.endDateModelChangeCount) .withContext('End date set three times') .toBe(3); })); it('should mark the range picker as required when the entire group has the required validator', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.range = new FormGroup( { start: new FormControl(null), end: new FormControl(null), }, Validators.required, ); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.rangeInput.required).toBe(true); }); it('should mark the range picker as required when one part is required', () => { const fixture = createComponent(StandardRangePicker); fixture.componentInstance.range = new FormGroup({ start: new FormControl(null, Validators.required), end: new FormControl(null), }); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.componentInstance.rangeInput.required).toBe(true); }); }); @Component({ template: ` Enter a date `, standalone: true, imports: [ MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatLabel, MatDateRangePicker, ReactiveFormsModule, ], }) class StandardRangePicker { @ViewChild('start') start: ElementRef; @ViewChild('end') end: ElementRef; @ViewChild(MatStartDate) startInput: MatStartDate; @ViewChild(MatEndDate) endInput: MatEndDate; @ViewChild(MatDateRangeInput) rangeInput: MatDateRangeInput; @ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker; separator = '–'; rangeDisabled = false; minDate: Date | null = null; maxDate: Date | null = null; comparisonStart: Date | null = null; comparisonEnd: Date | null = null; startAt: Date | null = null; dateFilter = () => true; range = new FormGroup({ start: new FormControl(null), end: new FormControl(null), }); } @Component({ template: ` `, standalone: true, imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker], }) class RangePickerNoStart {} @Component({ template: ` `, standalone: true, imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker], }) class RangePickerNoEnd {} @Component({ template: ` `, standalone: true, imports: [ MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker, FormsModule, ], }) class RangePickerNgModel { @ViewChild(MatStartDate, {read: NgModel}) startModel: NgModel; @ViewChild(MatEndDate, {read: NgModel}) endModel: NgModel; @ViewChild(MatStartDate, {read: ElementRef}) startInput: ElementRef; @ViewChild(MatEndDate, {read: ElementRef}) endInput: ElementRef; @ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker; private _start: Date | null = null; get start(): Date | null { return this._start; } set start(aStart: Date | null) { this.startDateModelChangeCount++; this._start = aStart; } private _end: Date | null = null; get end(): Date | null { return this._end; } set end(anEnd: Date | null) { this.endDateModelChangeCount++; this._end = anEnd; } startDateModelChangeCount = 0; endDateModelChangeCount = 0; } @Component({ template: ` `, standalone: true, imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker], }) class RangePickerNoLabel { @ViewChild('start') start: ElementRef; @ViewChild('end') end: ElementRef; } @Directive({ selector: '[customValidator]', providers: [ { provide: NG_VALIDATORS, useExisting: CustomValidator, multi: true, }, ], standalone: true, }) class CustomValidator implements Validator { validate = jasmine.createSpy('validate spy').and.returnValue(null); } @Component({ template: ` `, standalone: true, imports: [ MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker, CustomValidator, FormsModule, ], }) class RangePickerWithCustomValidator { @ViewChild(CustomValidator) validator: CustomValidator; start: Date | null = null; end: Date | null = null; min: Date; max: Date; } @Component({ template: ` `, standalone: true, imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker], }) class RangePickerErrorStateMatcher { @ViewChild(MatStartDate) startInput: MatStartDate; @ViewChild(MatEndDate) endInput: MatEndDate; matcher: ErrorStateMatcher = {isErrorState: () => false}; }