sass-references/angular-material/material/datepicker/date-range-input.spec.ts

1372 lines
47 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<T>(component: Type<T>, providers: Provider[] = []): ComponentFixture<T> {
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 <input/>'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<Date | null>(null),
end: new FormControl<Date | null>(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<Date | null>(null, Validators.required),
end: new FormControl<Date | null>(null),
});
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.rangeInput.required).toBe(true);
});
});
@Component({
template: `
<mat-form-field hintLabel="Pick between a start and an end">
<mat-label>Enter a date</mat-label>
<mat-date-range-input
[rangePicker]="rangePicker"
[formGroup]="range"
[disabled]="rangeDisabled"
[separator]="separator"
[min]="minDate"
[max]="maxDate"
[dateFilter]="dateFilter"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd">
<input #start formControlName="start" matStartDate aria-label="Start date"
placeholder="Start Date"/>
<input #end formControlName="end" matEndDate aria-labelledby="end-date-label-1 end-date-label-2"
placeholder="End Date"/>
</mat-date-range-input>
<label id='end-date-label-1' class="cdk-visually-hidden">End</label>
<label id='end-date-label-2' class="cdk-visually-hidden">date</label>
<mat-date-range-picker
[startAt]="startAt"
#rangePicker></mat-date-range-picker>
</mat-form-field>
`,
standalone: true,
imports: [
MatDateRangeInput,
MatStartDate,
MatEndDate,
MatFormField,
MatLabel,
MatDateRangePicker,
ReactiveFormsModule,
],
})
class StandardRangePicker {
@ViewChild('start') start: ElementRef<HTMLInputElement>;
@ViewChild('end') end: ElementRef<HTMLInputElement>;
@ViewChild(MatStartDate) startInput: MatStartDate<Date>;
@ViewChild(MatEndDate) endInput: MatEndDate<Date>;
@ViewChild(MatDateRangeInput) rangeInput: MatDateRangeInput<Date>;
@ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker<Date>;
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<Date | null>(null),
end: new FormControl<Date | null>(null),
});
}
@Component({
template: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker">
<input matEndDate/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
standalone: true,
imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker],
})
class RangePickerNoStart {}
@Component({
template: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker">
<input matStartDate/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
standalone: true,
imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker],
})
class RangePickerNoEnd {}
@Component({
template: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker">
<input matStartDate [(ngModel)]="start"/>
<input matEndDate [(ngModel)]="end"/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
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<HTMLInputElement>;
@ViewChild(MatEndDate, {read: ElementRef}) endInput: ElementRef<HTMLInputElement>;
@ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker<Date>;
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: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker">
<input #start matStartDate/>
<input #end matEndDate/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
standalone: true,
imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker],
})
class RangePickerNoLabel {
@ViewChild('start') start: ElementRef<HTMLInputElement>;
@ViewChild('end') end: ElementRef<HTMLInputElement>;
}
@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: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker" [min]="min" [max]="max">
<input matStartDate [(ngModel)]="start" customValidator/>
<input matEndDate [(ngModel)]="end" customValidator/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
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: `
<mat-form-field>
<mat-date-range-input [rangePicker]="rangePicker">
<input matStartDate [errorStateMatcher]="matcher"/>
<input matEndDate [errorStateMatcher]="matcher"/>
</mat-date-range-input>
<mat-date-range-picker #rangePicker></mat-date-range-picker>
</mat-form-field>
`,
standalone: true,
imports: [MatDateRangeInput, MatStartDate, MatEndDate, MatFormField, MatDateRangePicker],
})
class RangePickerErrorStateMatcher {
@ViewChild(MatStartDate) startInput: MatStartDate<Date>;
@ViewChild(MatEndDate) endInput: MatEndDate<Date>;
matcher: ErrorStateMatcher = {isErrorState: () => false};
}