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

1372 lines
47 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
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};
}