sass-references/angular-material/material/datepicker/calendar.spec.ts

720 lines
24 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
import {Directionality} from '@angular/cdk/bidi';
import {ENTER, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
} from '@angular/cdk/testing/private';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed, inject, waitForAsync} from '@angular/core/testing';
import {DateAdapter, MatNativeDateModule} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {DEC, FEB, JAN, JUL, NOV} from '../testing';
import {MatCalendar} from './calendar';
import {MatDatepickerIntl} from './datepicker-intl';
import {MatDatepickerModule} from './datepicker-module';
describe('MatCalendar', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatNativeDateModule, MatDatepickerModule],
providers: [MatDatepickerIntl, {provide: Directionality, useFactory: () => ({value: 'ltr'})}],
declarations: [
// Test components.
StandardCalendar,
CalendarWithMinMax,
CalendarWithDateFilter,
CalendarWithSelectableMinDate,
],
});
}));
describe('standard calendar', () => {
let fixture: ComponentFixture<StandardCalendar>;
let testComponent: StandardCalendar;
let calendarElement: HTMLElement;
let periodButton: HTMLElement;
let calendarInstance: MatCalendar<Date>;
beforeEach(() => {
fixture = TestBed.createComponent(StandardCalendar);
fixture.detectChanges();
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar))!;
calendarElement = calendarDebugElement.nativeElement;
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it(`should update today's date`, inject([DateAdapter], (adapter: DateAdapter<Date>) => {
let fakeToday = new Date(2018, 0, 1);
spyOn(adapter, 'today').and.callFake(() => fakeToday);
calendarInstance.activeDate = fakeToday;
calendarInstance.updateTodaysDate();
fixture.detectChanges();
let todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('1');
fakeToday = new Date(2018, 0, 10);
calendarInstance.updateTodaysDate();
fixture.detectChanges();
todayCell = calendarElement.querySelector('.mat-calendar-body-today')!;
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('10');
}));
it('should be in month view with specified month active', () => {
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should select date in month view', () => {
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
});
it('should emit the selected month on cell clicked in year view', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
const normalizedMonth: Date = fixture.componentInstance.selectedMonth;
expect(normalizedMonth.getMonth()).toEqual(0);
});
it('should emit the selected year on cell clicked in multiyear view', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
const normalizedYear: Date = fixture.componentInstance.selectedYear;
expect(normalizedYear.getFullYear()).toEqual(2017);
});
it('should re-render when the i18n labels have changed', inject(
[MatDatepickerIntl],
(intl: MatDatepickerIntl) => {
const button = fixture.debugElement.nativeElement.querySelector(
'.mat-calendar-period-button',
);
intl.switchToMultiYearViewLabel = 'Go to multi-year view?';
intl.changes.next();
fixture.detectChanges();
expect(button.getAttribute('aria-label')).toBe('Go to multi-year view?');
},
));
it('should set all buttons to be `type="button"`', () => {
const invalidButtons = calendarElement.querySelectorAll('button:not([type="button"])');
expect(invalidButtons.length).toBe(0);
});
it('should complete the stateChanges stream', () => {
const spy = jasmine.createSpy('complete spy');
const subscription = calendarInstance.stateChanges.subscribe({complete: spy});
fixture.destroy();
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
describe('a11y', () => {
describe('calendar body', () => {
let calendarBodyEl: HTMLElement;
beforeEach(() => {
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should initially set start date active', () => {
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should make the calendar body focusable', () => {
expect(calendarBodyEl.getAttribute('tabindex')).toBe('-1');
});
it('should not move focus to the active cell on init', waitForAsync(async () => {
const activeCell = calendarBodyEl.querySelector(
'.mat-calendar-body-active',
)! as HTMLElement;
spyOn(activeCell, 'focus').and.callThrough();
fixture.detectChanges();
await new Promise(resolve => setTimeout(resolve));
expect(activeCell.focus).not.toHaveBeenCalled();
}));
it('should move focus to the active cell when the view changes', waitForAsync(async () => {
calendarInstance.currentView = 'multi-year';
fixture.detectChanges();
const activeCell = calendarBodyEl.querySelector(
'.mat-calendar-body-active',
)! as HTMLElement;
spyOn(activeCell, 'focus').and.callThrough();
await new Promise(resolve => setTimeout(resolve));
expect(activeCell.focus).toHaveBeenCalled();
}));
describe('year view', () => {
beforeEach(() => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
(calendarBodyEl.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
});
it('should return to month view on enter', () => {
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
expect(testComponent.selected).toBeUndefined();
});
it('should return to month view on space', () => {
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
expect(testComponent.selected).toBeUndefined();
});
});
describe('multi-year view', () => {
beforeEach(() => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('multi-year');
});
it('should go to year view on enter', () => {
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
expect(testComponent.selected).toBeUndefined();
});
it('should go to year view on space', () => {
const tableBodyEl = calendarBodyEl.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
expect(testComponent.selected).toBeUndefined();
});
});
});
});
it('should re-render the month view when the locale changes', inject(
[DateAdapter],
(adapter: DateAdapter<Date>) => {
fixture.detectChanges();
spyOn(calendarInstance.monthView, '_init').and.callThrough();
adapter.setLocale('bg-BG');
fixture.detectChanges();
expect(calendarInstance.monthView._init).toHaveBeenCalled();
},
));
it('should re-render the year view when the locale changes', inject(
[DateAdapter],
(adapter: DateAdapter<Date>) => {
periodButton.click();
fixture.detectChanges();
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
spyOn(calendarInstance.yearView, '_init').and.callThrough();
adapter.setLocale('bg-BG');
fixture.detectChanges();
expect(calendarInstance.yearView._init).toHaveBeenCalled();
},
));
it('should re-render the multi-year view when the locale changes', inject(
[DateAdapter],
(adapter: DateAdapter<Date>) => {
periodButton.click();
fixture.detectChanges();
spyOn(calendarInstance.multiYearView, '_init').and.callThrough();
adapter.setLocale('bg-BG');
fixture.detectChanges();
expect(calendarInstance.multiYearView._init).toHaveBeenCalled();
},
));
});
describe('calendar with min and max date', () => {
let fixture: ComponentFixture<CalendarWithMinMax>;
let testComponent: CalendarWithMinMax;
let calendarElement: HTMLElement;
let calendarInstance: MatCalendar<Date>;
beforeEach(() => {
fixture = TestBed.createComponent(CalendarWithMinMax);
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar))!;
calendarElement = calendarDebugElement.nativeElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should clamp startAt value below min date', () => {
testComponent.startAt = new Date(2000, JAN, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should clamp startAt value above max date', () => {
testComponent.startAt = new Date(2020, JAN, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should not go back past min date', () => {
testComponent.startAt = new Date(2016, FEB, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let prevButton = calendarElement.querySelector(
'.mat-calendar-previous-button',
) as HTMLButtonElement;
expect(prevButton.disabled).withContext('previous button should not be disabled').toBe(false);
expect(calendarInstance.activeDate).toEqual(new Date(2016, FEB, 1));
prevButton.click();
fixture.detectChanges();
expect(prevButton.disabled).withContext('previous button should be disabled').toBe(true);
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should not go forward past max date', () => {
testComponent.startAt = new Date(2017, DEC, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
let nextButton = calendarElement.querySelector(
'.mat-calendar-next-button',
) as HTMLButtonElement;
expect(nextButton.disabled).withContext('next button should not be disabled').toBe(false);
expect(calendarInstance.activeDate).toEqual(new Date(2017, DEC, 1));
nextButton.click();
fixture.detectChanges();
expect(nextButton.disabled).withContext('next button should be disabled').toBe(true);
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should re-render the month view when the minDate changes', () => {
fixture.detectChanges();
spyOn(calendarInstance.monthView, '_init').and.callThrough();
testComponent.minDate = new Date(2017, NOV, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.monthView._init).toHaveBeenCalled();
});
it('should not re-render the month view when the minDate changes to the same day at a different time', () => {
fixture.detectChanges();
spyOn(calendarInstance.monthView, '_init').and.callThrough();
testComponent.minDate = new Date(2016, JAN, 1, 0, 0, 0, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.monthView._init).not.toHaveBeenCalled();
});
it('should re-render the month view when the maxDate changes', () => {
fixture.detectChanges();
spyOn(calendarInstance.monthView, '_init').and.callThrough();
testComponent.maxDate = new Date(2017, DEC, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.monthView._init).toHaveBeenCalled();
});
it('should re-render the year view when the minDate changes', () => {
fixture.detectChanges();
const periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
periodButton.click();
fixture.detectChanges();
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
spyOn(calendarInstance.yearView, '_init').and.callThrough();
testComponent.minDate = new Date(2017, NOV, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.yearView._init).toHaveBeenCalled();
});
it('should re-render the year view when the maxDate changes', () => {
fixture.detectChanges();
const periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
periodButton.click();
fixture.detectChanges();
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
spyOn(calendarInstance.yearView, '_init').and.callThrough();
testComponent.maxDate = new Date(2017, DEC, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.yearView._init).toHaveBeenCalled();
});
it('should not re-render the year view when the maxDate changes to the same day at a different time', () => {
fixture.detectChanges();
const periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
periodButton.click();
fixture.detectChanges();
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
spyOn(calendarInstance.yearView, '_init').and.callThrough();
testComponent.maxDate = new Date(2018, JAN, 1, 0, 0, 1, 0);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.yearView._init).not.toHaveBeenCalled();
});
it('should re-render the multi-year view when the minDate changes', () => {
fixture.detectChanges();
const periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
periodButton.click();
fixture.detectChanges();
spyOn(calendarInstance.multiYearView, '_init').and.callThrough();
testComponent.minDate = new Date(2017, NOV, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.multiYearView._init).toHaveBeenCalled();
});
it('should re-render the multi-year view when the maxDate changes', () => {
fixture.detectChanges();
const periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
periodButton.click();
fixture.detectChanges();
spyOn(calendarInstance.multiYearView, '_init').and.callThrough();
testComponent.maxDate = new Date(2017, DEC, 1);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(calendarInstance.multiYearView._init).toHaveBeenCalled();
});
it('should update the minDate in the child view if it changed after an interaction', () => {
fixture.destroy();
const dynamicFixture = TestBed.createComponent(CalendarWithSelectableMinDate);
dynamicFixture.detectChanges();
const calendarDebugElement = dynamicFixture.debugElement.query(By.directive(MatCalendar))!;
const disabledClass = 'mat-calendar-body-disabled';
calendarElement = calendarDebugElement.nativeElement;
calendarInstance = calendarDebugElement.componentInstance;
let cells = Array.from(calendarElement.querySelectorAll('.mat-calendar-body-cell'));
expect(cells.slice(0, 9).every(c => c.classList.contains(disabledClass)))
.withContext('Expected dates up to the 10th to be disabled.')
.toBe(true);
expect(cells.slice(9).every(c => c.classList.contains(disabledClass)))
.withContext('Expected dates after the 10th to be enabled.')
.toBe(false);
(cells[14] as HTMLElement).click();
dynamicFixture.detectChanges();
cells = Array.from(calendarElement.querySelectorAll('.mat-calendar-body-cell'));
expect(cells.slice(0, 14).every(c => c.classList.contains(disabledClass)))
.withContext('Expected dates up to the 14th to be disabled.')
.toBe(true);
expect(cells.slice(14).every(c => c.classList.contains(disabledClass)))
.withContext('Expected dates after the 14th to be enabled.')
.toBe(false);
});
});
describe('calendar with date filter', () => {
let fixture: ComponentFixture<CalendarWithDateFilter>;
let testComponent: CalendarWithDateFilter;
let calendarElement: HTMLElement;
let calendarInstance: MatCalendar<Date>;
beforeEach(() => {
fixture = TestBed.createComponent(CalendarWithDateFilter);
fixture.detectChanges();
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar))!;
calendarElement = calendarDebugElement.nativeElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should disable and prevent selection of filtered dates', () => {
let cells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(cells[0] as HTMLElement).click();
fixture.detectChanges();
expect(testComponent.selected).toBeFalsy();
(cells[1] as HTMLElement).click();
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 2));
});
describe('a11y', () => {
let tableBodyEl: HTMLElement;
beforeEach(() => {
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
expect(tableBodyEl).not.toBeNull();
dispatchFakeEvent(tableBodyEl, 'focus');
fixture.detectChanges();
});
it('should not allow selection of disabled date in month view', () => {
expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toBeUndefined();
});
it('should allow entering month view at disabled month', () => {
let periodButton = calendarElement.querySelector(
'.mat-calendar-period-button',
) as HTMLElement;
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
calendarInstance.activeDate = new Date(2017, NOV, 1);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('year');
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();
expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toBeUndefined();
});
});
});
});
@Component({
template: `
<mat-calendar
[startAt]="startDate"
[(selected)]="selected"
(yearSelected)="selectedYear=$event"
(monthSelected)="selectedMonth=$event">
</mat-calendar>`,
standalone: false,
})
class StandardCalendar {
selected: Date;
selectedYear: Date;
selectedMonth: Date;
startDate = new Date(2017, JAN, 31);
}
@Component({
template: `
<mat-calendar [startAt]="startAt" [minDate]="minDate" [maxDate]="maxDate"></mat-calendar>
`,
standalone: false,
})
class CalendarWithMinMax {
startAt: Date;
minDate = new Date(2016, JAN, 1);
maxDate = new Date(2018, JAN, 1);
}
@Component({
template: `
<mat-calendar [startAt]="startDate" [(selected)]="selected" [dateFilter]="dateFilter">
</mat-calendar>
`,
standalone: false,
})
class CalendarWithDateFilter {
selected: Date;
startDate = new Date(2017, JAN, 1);
dateFilter(date: Date) {
return !(date.getDate() % 2) && date.getMonth() !== NOV;
}
}
@Component({
template: `
<mat-calendar
[startAt]="startAt"
(selectedChange)="select($event)"
[selected]="selected"
[minDate]="selected">
</mat-calendar>
`,
standalone: false,
})
class CalendarWithSelectableMinDate {
startAt = new Date(2018, JUL, 0);
selected: Date;
minDate: Date;
constructor() {
this.select(new Date(2018, JUL, 10));
}
select(value: Date) {
this.minDate = this.selected = value;
}
}