import {Directionality} from '@angular/cdk/bidi';
import {
  DOWN_ARROW,
  ENTER,
  ESCAPE,
  LEFT_ARROW,
  PAGE_DOWN,
  PAGE_UP,
  RIGHT_ARROW,
  UP_ARROW,
} from '@angular/cdk/keycodes';
import {Overlay} from '@angular/cdk/overlay';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
  createKeyboardEvent,
  dispatchEvent,
  dispatchFakeEvent,
  dispatchKeyboardEvent,
  dispatchMouseEvent,
  typeInElement,
} from '@angular/cdk/testing/private';
import {Component, Directive, Provider, Type, ViewChild, ViewEncapsulation} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, inject, tick} from '@angular/core/testing';
import {
  FormControl,
  FormsModule,
  NG_VALIDATORS,
  NgModel,
  ReactiveFormsModule,
  Validator,
} from '@angular/forms';
import {MAT_DATE_LOCALE, MatNativeDateModule, NativeDateModule} from '@angular/material/core';
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Subject} from 'rxjs';
import {DEC, JAN, JUL, JUN, SEP} from '../testing';
import {MatDatepicker} from './datepicker';
import {DatepickerDropdownPositionX, DatepickerDropdownPositionY} from './datepicker-base';
import {MatDatepickerInput} from './datepicker-input';
import {MatDatepickerToggle} from './datepicker-toggle';
import {
  MAT_DATEPICKER_SCROLL_STRATEGY,
  MatDateSelectionModel,
  MatDatepickerIntl,
  MatDatepickerModule,
} from './index';

describe('MatDatepicker', () => {
  const SUPPORTS_INTL = typeof Intl != 'undefined';

  // Creates a test component fixture.
  function createComponent<T>(
    component: Type<T>,
    imports: Type<any>[] = [],
    providers: Provider[] = [],
    declarations: Type<any>[] = [],
  ): ComponentFixture<T> {
    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        MatDatepickerModule,
        MatFormFieldModule,
        MatInputModule,
        NoopAnimationsModule,
        ReactiveFormsModule,
        ...imports,
      ],
      providers,
      declarations: [component, ...declarations],
    });

    return TestBed.createComponent(component);
  }

  describe('with MatNativeDateModule', () => {
    describe('standard datepicker', () => {
      let fixture: ComponentFixture<StandardDatepicker>;
      let testComponent: StandardDatepicker;
      let model: MatDateSelectionModel<Date | null, Date>;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
        model = fixture.debugElement
          .query(By.directive(MatDatepicker))
          .injector.get(MatDateSelectionModel);
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should initialize with correct value shown in input', () => {
        if (SUPPORTS_INTL) {
          expect(fixture.nativeElement.querySelector('input').value).toBe('1/1/2020');
        }
      });

      it('open non-touch should open popup', fakeAsync(() => {
        expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).toBeNull();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.cdk-overlay-pane.mat-datepicker-popup')).not.toBeNull();
      }));

      it('touch should open dialog', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
      }));

      it('should not be able to open more than one dialog', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(0);

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick(500);
        fixture.detectChanges();

        dispatchKeyboardEvent(document.querySelector('.mat-calendar-body')!, 'keydown', ENTER);
        fixture.detectChanges();
        tick(100);

        testComponent.datepicker.open();
        tick(500);
        fixture.detectChanges();
        flush();

        expect(document.querySelectorAll('.mat-datepicker-dialog').length).toBe(1);
      }));

      it('should open datepicker if opened input is set to true', fakeAsync(() => {
        testComponent.opened = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();

        testComponent.opened = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.mat-datepicker-content')).toBeNull();
      }));

      it('open in disabled mode should not open the calendar', fakeAsync(() => {
        testComponent.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

        testComponent.datepicker.open();
        tick();
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
      }));

      it('disabled datepicker input should open the calendar if datepicker is enabled', fakeAsync(() => {
        testComponent.datepicker.disabled = false;
        testComponent.datepickerInput.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(document.querySelector('.cdk-overlay-pane')).toBeNull();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
      }));

      it('close should close popup', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        const popup = document.querySelector('.cdk-overlay-pane')!;
        expect(popup).not.toBeNull();
        expect(popup.getBoundingClientRect().height).toBeGreaterThan(0);

        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();

        expect(popup.getBoundingClientRect().height).toBe(0);
      }));

      it('should close the popup when pressing ESCAPE', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to be open.')
          .toBe(true);

        const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
        fixture.detectChanges();
        flush();

        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to be closed.')
          .toBe(false);
        expect(event.defaultPrevented).toBe(true);
      }));

      it('should not close the popup when pressing ESCAPE with a modifier key', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to be open.')
          .toBe(true);

        const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, {
          alt: true,
        });
        fixture.detectChanges();
        flush();

        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to stay open.')
          .toBe(true);
        expect(event.defaultPrevented).toBe(false);
      }));

      it('should set the proper role on the popup', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        const popup = document.querySelector('.mat-datepicker-content-container')!;
        expect(popup).toBeTruthy();
        expect(popup.getAttribute('role')).toBe('dialog');
      }));

      it(
        'should set aria-labelledby to the one from the input, if not placed inside ' +
          'a mat-form-field',
        fakeAsync(() => {
          expect(fixture.nativeElement.querySelector('mat-form-field')).toBeFalsy();

          const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
          input.setAttribute('aria-labelledby', 'test-label');

          testComponent.datepicker.open();
          fixture.detectChanges();
          tick();
          flush();

          const popup = document.querySelector(
            '.cdk-overlay-pane .mat-datepicker-content-container',
          )!;
          expect(popup).toBeTruthy();
          expect(popup.getAttribute('aria-labelledby')).toBe('test-label');
        }),
      );

      it('close should close dialog', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();

        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
      }));

      it('setting selected via click should update input and close calendar', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
        expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));

        let cells = document.querySelectorAll('.mat-calendar-body-cell');
        dispatchMouseEvent(cells[1], 'click');
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
        expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
      }));

      it('setting selected via enter press should update input and close calendar', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
        expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));

        let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;

        dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
        fixture.detectChanges();
        tick();
        flush();
        dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
        fixture.detectChanges();
        dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
        expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
      }));

      it(
        'clicking the currently selected date should close the calendar ' +
          'without firing selectedChanged',
        fakeAsync(() => {
          const spy = jasmine.createSpy('selectionChanged spy');
          const selectedSubscription = model.selectionChanged.subscribe(spy);

          for (let changeCount = 1; changeCount < 3; changeCount++) {
            const currentDay = changeCount;
            testComponent.datepicker.open();
            fixture.detectChanges();
            tick();

            expect(document.querySelector('mat-datepicker-content')).not.toBeNull();
            expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, currentDay));

            let cells = document.querySelectorAll('.mat-calendar-body-cell');
            dispatchMouseEvent(cells[1], 'click');
            fixture.detectChanges();
            flush();
          }

          expect(spy).toHaveBeenCalledTimes(1);
          expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
          expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
          selectedSubscription.unsubscribe();
        }),
      );

      it(
        'pressing enter on the currently selected date should close the calendar without ' +
          'firing selectedChanged',
        fakeAsync(() => {
          const spy = jasmine.createSpy('selectionChanged spy');
          const selectedSubscription = model.selectionChanged.subscribe(spy);

          testComponent.datepicker.open();
          fixture.detectChanges();
          tick();
          flush();

          let calendarBodyEl = document.querySelector('.mat-calendar-body') as HTMLElement;
          expect(calendarBodyEl).not.toBeNull();
          expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));

          dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
          fixture.detectChanges();
          flush();

          fixture.whenStable().then(() => {
            expect(spy).not.toHaveBeenCalled();
            expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
            expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
            selectedSubscription.unsubscribe();
          });
        }),
      );

      it('startAt should fallback to input value', () => {
        expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
      });

      it('should attach popup to native input', () => {
        let attachToRef = testComponent.datepickerInput.getConnectedOverlayOrigin();
        expect(attachToRef.nativeElement.tagName.toLowerCase())
          .withContext('popup should be attached to native input')
          .toBe('input');
      });

      it('input should aria-owns calendar after opened in non-touch mode', fakeAsync(() => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
        expect(inputEl.getAttribute('aria-owns')).toBeNull();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        let ownedElementId = inputEl.getAttribute('aria-owns');
        expect(ownedElementId).not.toBeNull();

        let ownedElement = document.getElementById(ownedElementId);
        expect(ownedElement).not.toBeNull();
        expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
      }));

      it('input should aria-owns calendar after opened in touch mode', fakeAsync(() => {
        testComponent.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
        expect(inputEl.getAttribute('aria-owns')).toBeNull();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        let ownedElementId = inputEl.getAttribute('aria-owns');
        expect(ownedElementId).not.toBeNull();

        let ownedElement = document.getElementById(ownedElementId);
        expect(ownedElement).not.toBeNull();
        expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
      }));

      it('should not throw when given wrong data type', () => {
        testComponent.date = '1/1/2017' as any;
        fixture.changeDetectorRef.markForCheck();

        expect(() => fixture.detectChanges()).not.toThrow();
      });

      it('should clear out the backdrop subscriptions on close', fakeAsync(() => {
        for (let i = 0; i < 3; i++) {
          testComponent.datepicker.open();
          fixture.detectChanges();
          tick();

          testComponent.datepicker.close();
          fixture.detectChanges();
          tick();
        }

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        const spy = jasmine.createSpy('close event spy');
        const subscription = testComponent.datepicker.closedStream.subscribe(spy);
        const backdrop = document.querySelector('.cdk-overlay-backdrop')! as HTMLElement;

        backdrop.click();
        fixture.detectChanges();
        flush();

        expect(spy).toHaveBeenCalledTimes(1);
        expect(testComponent.datepicker.opened).toBe(false);
        subscription.unsubscribe();
      }));

      it('should reset the datepicker when it is closed externally', fakeAsync(() => {
        TestBed.resetTestingModule();

        const scrolledSubject = new Subject();

        // Stub out a `CloseScrollStrategy` so we can trigger a detachment via the `OverlayRef`.
        fixture = createComponent(
          StandardDatepicker,
          [MatNativeDateModule],
          [
            {
              provide: ScrollDispatcher,
              useValue: {scrolled: () => scrolledSubject},
            },
            {
              provide: MAT_DATEPICKER_SCROLL_STRATEGY,
              deps: [Overlay],
              useFactory: (overlay: Overlay) => () => overlay.scrollStrategies.close(),
            },
          ],
        );

        fixture.detectChanges();
        testComponent = fixture.componentInstance;

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        expect(testComponent.datepicker.opened).toBe(true);

        scrolledSubject.next();
        flush();
        fixture.detectChanges();

        expect(testComponent.datepicker.opened).toBe(false);
      }));

      it('should close the datepicker using ALT + UP_ARROW', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.datepicker.opened).toBe(true);

        const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true});

        dispatchEvent(document.body, event);
        fixture.detectChanges();
        flush();

        expect(testComponent.datepicker.opened).toBe(false);
      }));

      it('should not close the datepicker when using CTRL + SHIFT + ALT + UP_ARROW', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.datepicker.opened).toBe(true);

        const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {
          alt: true,
          shift: true,
          control: true,
        });

        dispatchEvent(document.body, event);
        fixture.detectChanges();
        flush();

        expect(testComponent.datepicker.opened).toBe(true);
      }));

      it('should open the datepicker using ALT + DOWN_ARROW', fakeAsync(() => {
        expect(testComponent.datepicker.opened).toBe(false);

        const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});

        dispatchEvent(fixture.nativeElement.querySelector('input'), event);
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.datepicker.opened).toBe(true);
        expect(event.defaultPrevented).toBe(true);
      }));

      it('should not open for ALT + DOWN_ARROW on readonly input', fakeAsync(() => {
        const input = fixture.nativeElement.querySelector('input');

        expect(testComponent.datepicker.opened).toBe(false);

        input.setAttribute('readonly', 'true');

        const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});

        dispatchEvent(input, event);
        fixture.detectChanges();
        flush();

        expect(testComponent.datepicker.opened).toBe(false);
        expect(event.defaultPrevented).toBe(false);
      }));

      it('should not open the datepicker using SHIFT + CTRL + ALT + DOWN_ARROW', fakeAsync(() => {
        expect(testComponent.datepicker.opened).toBe(false);

        const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {
          alt: true,
          shift: true,
          control: true,
        });

        dispatchEvent(fixture.nativeElement.querySelector('input'), event);
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.datepicker.opened).toBe(false);
        expect(event.defaultPrevented).toBe(false);
      }));

      it('should show the invisible close button on focus', fakeAsync(() => {
        testComponent.opened = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();
        flush();

        const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
        expect(button.classList).toContain('cdk-visually-hidden');

        dispatchFakeEvent(button, 'focus');
        fixture.detectChanges();
        expect(button.classList).not.toContain('cdk-visually-hidden');

        dispatchFakeEvent(button, 'blur');
        fixture.detectChanges();
        expect(button.classList).toContain('cdk-visually-hidden');
      }));

      it('should close the overlay when clicking on the invisible close button', fakeAsync(() => {
        testComponent.opened = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();
        flush();

        const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
        expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();

        button.click();
        fixture.detectChanges();
        flush();

        expect(document.querySelector('.mat-datepicker-content')).toBeNull();
      }));

      it('should prevent the default action of navigation keys before the focus timeout has elapsed', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();

        // Do the assertions before flushing the delays since we want
        // to check specifically what happens before they have fired.
        [UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, PAGE_UP, PAGE_DOWN].forEach(keyCode => {
          const event = dispatchKeyboardEvent(document.body, 'keydown', keyCode);
          fixture.detectChanges();
          expect(event.defaultPrevented)
            .withContext(`Expected default action to be prevented for key code ${keyCode}`)
            .toBe(true);
        });

        tick();
        flush();
      }));
    });

    describe('datepicker with too many inputs', () => {
      it('should throw when multiple inputs registered', fakeAsync(() => {
        const fixture = createComponent(MultiInputDatepicker, [MatNativeDateModule]);
        expect(() => fixture.detectChanges()).toThrow();
      }));
    });

    describe('datepicker that is assigned to input at a later point', () => {
      it('should not throw on ALT + DOWN_ARROW for input without datepicker', fakeAsync(() => {
        const fixture = createComponent(DelayedDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();

        expect(() => {
          const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true});
          dispatchEvent(fixture.nativeElement.querySelector('input'), event);
          fixture.detectChanges();
          flush();
        }).not.toThrow();
      }));

      it('should handle value changes when a datepicker is assigned after init', fakeAsync(() => {
        const fixture = createComponent(DelayedDatepicker, [MatNativeDateModule]);
        const testComponent: DelayedDatepicker = fixture.componentInstance;
        const toSelect = new Date(2017, JAN, 1);
        fixture.detectChanges();

        const model = fixture.debugElement
          .query(By.directive(MatDatepicker))
          .injector.get(MatDateSelectionModel);

        expect(testComponent.datepickerInput.value).toBeNull();
        expect(model.selection).toBeNull();

        testComponent.assignedDatepicker = testComponent.datepicker;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.assignedDatepicker.select(toSelect);
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(testComponent.datepickerInput.value).toEqual(toSelect);
        expect(model.selection).toEqual(toSelect);
      }));
    });

    describe('datepicker with no inputs', () => {
      let fixture: ComponentFixture<NoInputDatepicker>;
      let testComponent: NoInputDatepicker;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(NoInputDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('should not throw when accessing disabled property', () => {
        expect(() => testComponent.datepicker.disabled).not.toThrow();
      });

      it('should throw when opened with no registered inputs', fakeAsync(() => {
        expect(() => testComponent.datepicker.open()).toThrow();
      }));
    });

    describe('datepicker with startAt', () => {
      let fixture: ComponentFixture<DatepickerWithStartAt>;
      let testComponent: DatepickerWithStartAt;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithStartAt, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('explicit startAt should override input value', () => {
        expect(testComponent.datepicker.startAt).toEqual(new Date(2010, JAN, 1));
      });
    });

    describe('datepicker with startView set to year', () => {
      let fixture: ComponentFixture<DatepickerWithStartViewYear>;
      let testComponent: DatepickerWithStartViewYear;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithStartViewYear, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should start at the specified view', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;

        // When the calendar is in year view, the first cell should be for a month rather than
        // for a date.
        // When the calendar is in year view, the first cell should be for a month rather than
        // for a date.
        expect(firstCalendarCell.textContent!.trim())
          .withContext('Expected the calendar to be in year-view')
          .toBe('JAN');
      }));

      it('should fire yearSelected when user selects calendar year in year view', fakeAsync(() => {
        spyOn(testComponent, 'onYearSelection');
        expect(testComponent.onYearSelection).not.toHaveBeenCalled();

        testComponent.datepicker.open();
        tick();
        fixture.detectChanges();
        flush();

        const cells = document.querySelectorAll('.mat-calendar-body-cell');

        dispatchMouseEvent(cells[0], 'click');
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.onYearSelection).toHaveBeenCalled();
      }));
    });

    describe('datepicker with startView set to multiyear', () => {
      let fixture: ComponentFixture<DatepickerWithStartViewMultiYear>;
      let testComponent: DatepickerWithStartViewMultiYear;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithStartViewMultiYear, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;

        spyOn(testComponent, 'onMultiYearSelection');
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should start at the specified view', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;

        // When the calendar is in year view, the first cell should be for a month rather than
        // for a date.
        // When the calendar is in year view, the first cell should be for a month rather than
        // for a date.
        expect(firstCalendarCell.textContent!.trim())
          .withContext('Expected the calendar to be in multi-year-view')
          .toBe('2016');
      }));

      it('should fire yearSelected when user selects calendar year in multiyear view', fakeAsync(() => {
        expect(testComponent.onMultiYearSelection).not.toHaveBeenCalled();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        const cells = document.querySelectorAll('.mat-calendar-body-cell');

        dispatchMouseEvent(cells[0], 'click');
        fixture.detectChanges();
        tick();
        flush();

        expect(testComponent.onMultiYearSelection).toHaveBeenCalled();
      }));
    });

    describe('datepicker with ngModel', () => {
      let fixture: ComponentFixture<DatepickerWithNgModel>;
      let testComponent: DatepickerWithNgModel;
      let model: MatDateSelectionModel<Date | null, Date>;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithNgModel, [MatNativeDateModule]);
        fixture.detectChanges();

        fixture.whenStable().then(() => {
          fixture.detectChanges();

          testComponent = fixture.componentInstance;
          model = fixture.debugElement
            .query(By.directive(MatDatepicker))
            .injector.get(MatDateSelectionModel);
        });
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('should update datepicker when model changes', fakeAsync(() => {
        expect(testComponent.datepickerInput.value).toBeNull();
        expect(model.selection).toBeNull();

        let selected = new Date(2017, JAN, 1);
        testComponent.selected = selected;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(testComponent.datepickerInput.value).toEqual(selected);
        expect(model.selection).toEqual(selected);
      }));

      it('should update model when date is selected', fakeAsync(() => {
        expect(testComponent.selected).toBeNull();
        expect(testComponent.datepickerInput.value).toBeNull();

        let selected = new Date(2017, JAN, 1);
        testComponent.datepicker.select(selected);
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(testComponent.selected).toEqual(selected);
        expect(testComponent.datepickerInput.value).toEqual(selected);
      }));

      it('should mark input dirty after input event', () => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-pristine');

        inputEl.value = '2001-01-01';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-dirty');
      });

      it('should mark input dirty after date selected', fakeAsync(() => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-pristine');

        testComponent.datepicker.select(new Date(2017, JAN, 1));
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-dirty');
      }));

      it('should mark input dirty after invalid value is typed in', () => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-pristine');

        inputEl.value = 'hello there';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-dirty');
      });

      it('should not mark dirty after model change', fakeAsync(() => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-pristine');

        testComponent.selected = new Date(2017, JAN, 1);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-pristine');
      }));

      it('should mark input touched on blur', () => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-untouched');

        dispatchFakeEvent(inputEl, 'focus');
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-untouched');

        dispatchFakeEvent(inputEl, 'blur');
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-touched');
      });

      it('should mark input as touched when the datepicker is closed', fakeAsync(() => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-untouched');

        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-untouched');

        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-touched');
      }));

      it('should reformat the input value on blur', () => {
        if (SUPPORTS_INTL) {
          // Skip this test if the internationalization API is not supported in the current
          // browser. Browsers like Safari 9 do not support the "Intl" API.
          return;
        }

        const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        inputEl.value = '2001-01-01';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        dispatchFakeEvent(inputEl, 'blur');
        fixture.detectChanges();

        expect(inputEl.value).toBe('1/1/2001');
      });

      it('should not reformat invalid dates on blur', () => {
        const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        inputEl.value = 'very-valid-date';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        dispatchFakeEvent(inputEl, 'blur');
        fixture.detectChanges();

        expect(inputEl.value).toBe('very-valid-date');
      });

      it('should mark input touched on calendar selection', fakeAsync(() => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.classList).toContain('ng-untouched');

        testComponent.datepicker.select(new Date(2017, JAN, 1));
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(inputEl.classList).toContain('ng-touched');
      }));
    });

    describe('datepicker with formControl', () => {
      let fixture: ComponentFixture<DatepickerWithFormControl>;
      let testComponent: DatepickerWithFormControl;
      let model: MatDateSelectionModel<Date | null, Date>;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithFormControl, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
        model = fixture.debugElement
          .query(By.directive(MatDatepicker))
          .injector.get(MatDateSelectionModel);
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('should update datepicker when formControl changes', () => {
        expect(testComponent.datepickerInput.value).toBeNull();
        expect(model.selection).toBeNull();

        let selected = new Date(2017, JAN, 1);
        testComponent.formControl.setValue(selected);
        fixture.detectChanges();

        expect(testComponent.datepickerInput.value).toEqual(selected);
        expect(model.selection).toEqual(selected);
      });

      it('should update formControl when date is selected', () => {
        expect(testComponent.formControl.value).toBeNull();
        expect(testComponent.datepickerInput.value).toBeNull();

        let selected = new Date(2017, JAN, 1);
        testComponent.datepicker.select(selected);
        fixture.detectChanges();

        expect(testComponent.formControl.value).toEqual(selected);
        expect(testComponent.datepickerInput.value).toEqual(selected);
      });

      it('should disable input when form control disabled', () => {
        let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        expect(inputEl.disabled).toBe(false);

        testComponent.formControl.disable();
        fixture.detectChanges();

        expect(inputEl.disabled).toBe(true);
      });

      it('should disable toggle when form control disabled', () => {
        expect(testComponent.datepickerToggle.disabled).toBe(false);

        testComponent.formControl.disable();
        fixture.detectChanges();

        expect(testComponent.datepickerToggle.disabled).toBe(true);
      });

      it(
        'should not dispatch FormControl change event for invalid values on input when set ' +
          'to update on blur',
        fakeAsync(() => {
          const formControl = new FormControl({value: null} as unknown as Date, {updateOn: 'blur'});
          const spy = jasmine.createSpy('change spy');
          const subscription = formControl.valueChanges.subscribe(spy);
          const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
          const setValue = (value: string) => {
            inputEl.value = value;
            dispatchFakeEvent(inputEl, 'input');
            fixture.detectChanges();
            flush();
            fixture.detectChanges();
          };

          fixture.componentInstance.formControl = formControl;
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          expect(spy).not.toHaveBeenCalled();

          setValue('10/10/2010');
          expect(spy).not.toHaveBeenCalled();

          setValue('10/10/');
          expect(spy).not.toHaveBeenCalled();

          setValue('10/10');
          expect(spy).not.toHaveBeenCalled();

          dispatchFakeEvent(inputEl, 'blur');
          fixture.detectChanges();
          flush();
          fixture.detectChanges();

          expect(spy).toHaveBeenCalledTimes(1);
          subscription.unsubscribe();
        }),
      );

      it('should set the matDatepickerParse error when an invalid value is typed for the first time', () => {
        const formControl = fixture.componentInstance.formControl;

        expect(formControl.hasError('matDatepickerParse')).toBe(false);

        typeInElement(fixture.nativeElement.querySelector('input'), 'Today');
        fixture.detectChanges();

        expect(formControl.hasError('matDatepickerParse')).toBe(true);
      });
    });

    describe('datepicker with mat-datepicker-toggle', () => {
      let fixture: ComponentFixture<DatepickerWithToggle>;
      let testComponent: DatepickerWithToggle;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithToggle, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should set `aria-haspopup` on the toggle button', () => {
        const button = fixture.debugElement.query(By.css('button'))!;

        expect(button).toBeTruthy();
        expect(button.nativeElement.getAttribute('aria-haspopup')).toBe('dialog');
      });

      it('should set a default `aria-label` on the toggle button', () => {
        const button = fixture.debugElement.query(By.css('button'))!;

        expect(button).toBeTruthy();
        expect(button.nativeElement.getAttribute('aria-label')).toBe('Open calendar');
      });

      it('should be able to change the button `aria-label`', () => {
        fixture.componentInstance.ariaLabel = 'Toggle the datepicker';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        const button = fixture.debugElement.query(By.css('button'))!;

        expect(button).toBeTruthy();
        expect(button.nativeElement.getAttribute('aria-label')).toBe('Toggle the datepicker');
      });

      it('should open calendar when toggle clicked', () => {
        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

        let toggle = fixture.debugElement.query(By.css('button'))!;
        dispatchMouseEvent(toggle.nativeElement, 'click');
        fixture.detectChanges();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();
      });

      it('should not open calendar when toggle clicked if datepicker is disabled', () => {
        testComponent.datepicker.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

        expect(toggle.hasAttribute('disabled')).toBe(true);
        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

        dispatchMouseEvent(toggle, 'click');
        fixture.detectChanges();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
      });

      it('should not open calendar when toggle clicked if input is disabled', () => {
        expect(testComponent.datepicker.disabled).toBe(false);

        testComponent.input.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

        expect(toggle.hasAttribute('disabled')).toBe(true);
        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

        dispatchMouseEvent(toggle, 'click');
        fixture.detectChanges();

        expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
      });

      it('should set the `button` type on the trigger to prevent form submissions', () => {
        let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
        expect(toggle.getAttribute('type')).toBe('button');
      });

      it('should remove the underlying SVG icon from the tab order', () => {
        const icon = fixture.debugElement.nativeElement.querySelector('svg');
        expect(icon.getAttribute('focusable')).toBe('false');
      });

      it('should restore focus to the toggle after the calendar is closed', fakeAsync(() => {
        let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

        fixture.componentInstance.touchUI = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        toggle.focus();
        expect(document.activeElement).withContext('Expected toggle to be focused.').toBe(toggle);

        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        let pane = document.querySelector('.cdk-overlay-pane')!;

        expect(pane).withContext('Expected calendar to be open.').toBeTruthy();
        expect(pane.contains(document.activeElement))
          .withContext('Expected focus to be inside the calendar.')
          .toBe(true);

        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();

        expect(document.activeElement)
          .withContext('Expected focus to be restored to toggle.')
          .toBe(toggle);
      }));

      it('should restore focus when placed inside a shadow root', fakeAsync(() => {
        if (!_supportsShadowDom()) {
          return;
        }

        fixture.destroy();
        TestBed.resetTestingModule();
        fixture = createComponent(DatepickerWithToggleInShadowDom, [MatNativeDateModule]);
        fixture.detectChanges();
        testComponent = fixture.componentInstance;

        const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
        fixture.componentInstance.touchUI = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        toggle.focus();
        spyOn(toggle, 'focus').and.callThrough();
        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();
        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();

        // We have to assert by looking at the `focus` method, because
        // `document.activeElement` will return the shadow root.
        expect(toggle.focus).toHaveBeenCalled();
      }));

      it('should allow for focus restoration to be disabled', fakeAsync(() => {
        let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

        fixture.componentInstance.touchUI = false;
        fixture.componentInstance.restoreFocus = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        toggle.focus();
        expect(document.activeElement).withContext('Expected toggle to be focused.').toBe(toggle);

        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        let pane = document.querySelector('.cdk-overlay-pane')!;

        expect(pane).withContext('Expected calendar to be open.').toBeTruthy();
        expect(pane.contains(document.activeElement))
          .withContext('Expected focus to be inside the calendar.')
          .toBe(true);

        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();

        expect(document.activeElement)
          .not.withContext('Expected focus not to be restored to toggle.')
          .toBe(toggle);
      }));

      it('should not override focus if it was moved inside the closed event in touchUI mode', fakeAsync(() => {
        const focusTarget = document.createElement('button');
        const datepicker = fixture.componentInstance.datepicker;
        const subscription = datepicker.closedStream.subscribe(() => focusTarget.focus());
        const input = fixture.nativeElement.querySelector('input');

        focusTarget.setAttribute('tabindex', '0');
        document.body.appendChild(focusTarget);

        // Important: we're testing the touchUI behavior on particular.
        fixture.componentInstance.touchUI = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        // Focus the input before opening so that the datepicker restores focus to it on close.
        input.focus();

        expect(document.activeElement)
          .withContext('Expected input to be focused on init.')
          .toBe(input);

        datepicker.open();
        fixture.detectChanges();
        tick(500);
        fixture.detectChanges();

        expect(document.activeElement).not.toBe(
          input,
          'Expected input not to be focused while dialog is open.',
        );

        datepicker.close();
        fixture.detectChanges();
        tick(500);
        fixture.detectChanges();

        expect(document.activeElement)
          .withContext('Expected alternate focus target to be focused after closing.')
          .toBe(focusTarget);

        focusTarget.remove();
        subscription.unsubscribe();
      }));

      it('should re-render when the i18n labels change', inject(
        [MatDatepickerIntl],
        (intl: MatDatepickerIntl) => {
          const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

          intl.openCalendarLabel = 'Open the calendar, perhaps?';
          intl.changes.next();
          fixture.detectChanges();

          expect(toggle.getAttribute('aria-label')).toBe('Open the calendar, perhaps?');
        },
      ));

      it('should toggle the active state of the datepicker toggle', fakeAsync(() => {
        const toggle = fixture.debugElement.query(By.css('mat-datepicker-toggle'))!.nativeElement;

        expect(toggle.classList).not.toContain('mat-datepicker-toggle-active');

        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(toggle.classList).toContain('mat-datepicker-toggle-active');

        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(toggle.classList).not.toContain('mat-datepicker-toggle-active');
      }));
    });

    describe('datepicker with custom mat-datepicker-toggle icon', () => {
      it('should be able to override the mat-datepicker-toggle icon', fakeAsync(() => {
        const fixture = createComponent(DatepickerWithCustomIcon, [MatNativeDateModule]);
        fixture.detectChanges();

        expect(fixture.nativeElement.querySelector('.mat-datepicker-toggle .custom-icon'))
          .withContext('Expected custom icon to be rendered.')
          .toBeTruthy();

        expect(fixture.nativeElement.querySelector('.mat-datepicker-toggle mat-icon'))
          .withContext('Expected default icon to be removed.')
          .toBeFalsy();
      }));
    });

    describe('datepicker with tabindex on mat-datepicker-toggle', () => {
      it('should forward the tabindex to the underlying button', () => {
        const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]);
        fixture.detectChanges();

        const button = fixture.nativeElement.querySelector('.mat-datepicker-toggle button');

        expect(button.getAttribute('tabindex')).toBe('7');
      });

      it('should remove the tabindex from the mat-datepicker-toggle host', () => {
        const fixture = createComponent(DatepickerWithTabindexOnToggle, [MatNativeDateModule]);
        fixture.detectChanges();

        const host = fixture.nativeElement.querySelector('.mat-datepicker-toggle');

        expect(host.hasAttribute('tabindex')).toBe(false);
      });
    });

    describe('datepicker inside mat-form-field', () => {
      let fixture: ComponentFixture<FormFieldDatepicker>;
      let testComponent: FormFieldDatepicker;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(FormFieldDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();
        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should float the placeholder when an invalid value is entered', () => {
        testComponent.datepickerInput.value = 'totally-not-a-date' as any;
        fixture.changeDetectorRef.markForCheck();
        fixture.debugElement.nativeElement.querySelector('input').value = 'totally-not-a-date';
        fixture.detectChanges();

        expect(fixture.debugElement.nativeElement.querySelector('label').classList).toContain(
          'mdc-floating-label--float-above',
        );
      });

      it('should pass the form field theme color to the overlay', fakeAsync(() => {
        testComponent.formField.color = 'primary';
        fixture.changeDetectorRef.markForCheck();
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        let contentEl = document.querySelector('.mat-datepicker-content')!;

        expect(contentEl.classList).toContain('mat-primary');

        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();

        testComponent.formField.color = 'warn';
        fixture.changeDetectorRef.markForCheck();
        testComponent.datepicker.open();

        contentEl = document.querySelector('.mat-datepicker-content')!;
        fixture.detectChanges();
        tick();
        flush();

        expect(contentEl.classList).toContain('mat-warn');
        expect(contentEl.classList).not.toContain('mat-primary');
      }));

      it('should prefer the datepicker color over the form field one', fakeAsync(() => {
        testComponent.datepicker.color = 'accent';
        testComponent.formField.color = 'warn';
        fixture.changeDetectorRef.markForCheck();
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        const contentEl = document.querySelector('.mat-datepicker-content')!;

        expect(contentEl.classList).toContain('mat-accent');
        expect(contentEl.classList).not.toContain('mat-warn');
      }));

      it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
        const label: HTMLElement = fixture.nativeElement.querySelector('label');

        expect(label).toBeTruthy();
        expect(label.getAttribute('id')).toBeTruthy();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        const popup = document.querySelector(
          '.cdk-overlay-pane .mat-datepicker-content-container',
        )!;
        expect(popup).toBeTruthy();
        expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
      }));
    });

    describe('datepicker with min and max dates and validation', () => {
      let fixture: ComponentFixture<DatepickerWithMinAndMaxValidation>;
      let testComponent: DatepickerWithMinAndMaxValidation;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithMinAndMaxValidation, [MatNativeDateModule]);
        fixture.detectChanges();
        flush();

        testComponent = fixture.componentInstance;
      }));

      function revalidate() {
        fixture.detectChanges();
        flush();
        fixture.detectChanges();
      }

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should use min and max dates specified by the input', () => {
        expect(testComponent.datepicker._getMinDate()).toEqual(new Date(2010, JAN, 1));
        expect(testComponent.datepicker._getMaxDate()).toEqual(new Date(2020, JAN, 1));
      });

      it('should mark invalid when value is before min', fakeAsync(() => {
        testComponent.date = new Date(2009, DEC, 31);
        fixture.changeDetectorRef.markForCheck();
        revalidate();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
          'ng-invalid',
        );
      }));

      it('should mark invalid when value is after max', fakeAsync(() => {
        testComponent.date = new Date(2020, JAN, 2);
        fixture.changeDetectorRef.markForCheck();
        revalidate();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
          'ng-invalid',
        );
      }));

      it('should not mark invalid when value equals min', fakeAsync(() => {
        testComponent.date = testComponent.datepicker._getMinDate();
        fixture.changeDetectorRef.markForCheck();
        revalidate();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
          'ng-invalid',
        );
      }));

      it('should not mark invalid when value equals max', fakeAsync(() => {
        testComponent.date = testComponent.datepicker._getMaxDate();
        fixture.changeDetectorRef.markForCheck();
        revalidate();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
          'ng-invalid',
        );
      }));

      it('should not mark invalid when value is between min and max', fakeAsync(() => {
        testComponent.date = new Date(2010, JAN, 2);
        fixture.changeDetectorRef.markForCheck();
        revalidate();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
          'ng-invalid',
        );
      }));

      it('should update validity when switching between null and invalid', fakeAsync(() => {
        const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
        inputEl.value = '';
        dispatchFakeEvent(inputEl, 'input');
        revalidate();

        expect(testComponent.model.valid).toBe(true);

        inputEl.value = 'abcdefg';
        dispatchFakeEvent(inputEl, 'input');
        revalidate();

        expect(testComponent.model.valid).toBe(false);

        inputEl.value = '';
        dispatchFakeEvent(inputEl, 'input');
        revalidate();

        expect(testComponent.model.valid).toBe(true);
      }));

      it('should update validity when a value is assigned', fakeAsync(() => {
        const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
        inputEl.value = '';
        dispatchFakeEvent(inputEl, 'input');
        revalidate();

        expect(testComponent.model.valid).toBe(true);

        inputEl.value = 'abcdefg';
        dispatchFakeEvent(inputEl, 'input');
        revalidate();

        expect(testComponent.model.valid).toBe(false);

        const validDate = new Date(2010, JAN, 2);

        // Assigning through the selection model simulates the user doing it via the calendar.
        const model = fixture.debugElement
          .query(By.directive(MatDatepicker))
          .injector.get<MatDateSelectionModel<Date>>(MatDateSelectionModel);
        model.updateSelection(validDate, null);
        revalidate();

        expect(testComponent.model.valid).toBe(true);
        expect(testComponent.date).toBe(validDate);
      }));

      it('should update the calendar when the min/max dates change', fakeAsync(() => {
        const getDisabledCells = () => {
          return document.querySelectorAll('.mat-calendar-body-disabled').length;
        };

        testComponent.date = new Date(2020, JAN, 5);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.minDate = new Date(2020, JAN, 3);
        testComponent.maxDate = new Date(2020, JAN, 7);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        let disabledCellCount = getDisabledCells();
        expect(disabledCellCount).not.toBe(0);

        testComponent.minDate = new Date(2020, JAN, 1);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();

        expect(getDisabledCells()).not.toBe(disabledCellCount);
        disabledCellCount = getDisabledCells();

        testComponent.maxDate = new Date(2020, JAN, 10);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();

        expect(getDisabledCells()).not.toBe(disabledCellCount);
      }));
    });

    describe('datepicker with filter and validation', () => {
      let fixture: ComponentFixture<DatepickerWithFilterAndValidation>;
      let testComponent: DatepickerWithFilterAndValidation;

      beforeEach(() => {
        fixture = createComponent(DatepickerWithFilterAndValidation, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
      });

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
        flush();
      }));

      it('should mark input invalid', fakeAsync(() => {
        testComponent.date = new Date(2017, JAN, 1);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).toContain(
          'ng-invalid',
        );

        testComponent.date = new Date(2017, JAN, 2);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(fixture.debugElement.query(By.css('input'))!.nativeElement.classList).not.toContain(
          'ng-invalid',
        );
      }));

      it('should disable filtered calendar cells', fakeAsync(() => {
        fixture.detectChanges();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();
        flush();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();

        let cells = document.querySelectorAll('.mat-calendar-body-cell');
        expect(cells[0].classList).toContain('mat-calendar-body-disabled');
        expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
      }));

      it('should revalidate when a new function is assigned', fakeAsync(() => {
        const classList = fixture.debugElement.query(By.css('input'))!.nativeElement.classList;
        testComponent.date = new Date(2017, JAN, 1);
        testComponent.filter = () => true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(classList).not.toContain('ng-invalid');

        testComponent.filter = () => false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(classList).toContain('ng-invalid');
      }));

      it('should not dispatch the change event if a new function with the same result is assigned', fakeAsync(() => {
        const spy = jasmine.createSpy('change spy');
        const subscription = fixture.componentInstance.model.valueChanges?.subscribe(spy);
        testComponent.filter = () => false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(spy).toHaveBeenCalledTimes(1);

        testComponent.filter = () => false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
        fixture.detectChanges();

        expect(spy).toHaveBeenCalledTimes(1);
        subscription?.unsubscribe();
      }));
    });

    describe('datepicker with change and input events', () => {
      let fixture: ComponentFixture<DatepickerWithChangeAndInputEvents>;
      let testComponent: DatepickerWithChangeAndInputEvents;
      let inputEl: HTMLInputElement;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithChangeAndInputEvents, [MatNativeDateModule]);
        fixture.detectChanges();

        testComponent = fixture.componentInstance;
        inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;

        spyOn(testComponent, 'onChange');
        spyOn(testComponent, 'onInput');
        spyOn(testComponent, 'onDateChange');
        spyOn(testComponent, 'onDateInput');
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('should fire input and dateInput events when user types input', () => {
        expect(testComponent.onChange).not.toHaveBeenCalled();
        expect(testComponent.onDateChange).not.toHaveBeenCalled();
        expect(testComponent.onInput).not.toHaveBeenCalled();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        inputEl.value = '2001-01-01';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        expect(testComponent.onChange).not.toHaveBeenCalled();
        expect(testComponent.onDateChange).not.toHaveBeenCalled();
        expect(testComponent.onInput).toHaveBeenCalled();
        expect(testComponent.onDateInput).toHaveBeenCalled();
      });

      it('should fire change and dateChange events when user commits typed input', () => {
        expect(testComponent.onChange).not.toHaveBeenCalled();
        expect(testComponent.onDateChange).not.toHaveBeenCalled();
        expect(testComponent.onInput).not.toHaveBeenCalled();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        dispatchFakeEvent(inputEl, 'change');
        fixture.detectChanges();

        expect(testComponent.onChange).toHaveBeenCalled();
        expect(testComponent.onDateChange).toHaveBeenCalled();
        expect(testComponent.onInput).not.toHaveBeenCalled();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();
      });

      it('should fire dateChange and dateInput events when user selects calendar date', fakeAsync(() => {
        expect(testComponent.onChange).not.toHaveBeenCalled();
        expect(testComponent.onDateChange).not.toHaveBeenCalled();
        expect(testComponent.onInput).not.toHaveBeenCalled();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        expect(document.querySelector('.mat-datepicker-dialog')).not.toBeNull();

        const cells = document.querySelectorAll('.mat-calendar-body-cell');
        dispatchMouseEvent(cells[0], 'click');
        fixture.detectChanges();
        flush();

        expect(testComponent.onChange).not.toHaveBeenCalled();
        expect(testComponent.onDateChange).toHaveBeenCalled();
        expect(testComponent.onInput).not.toHaveBeenCalled();
        expect(testComponent.onDateInput).toHaveBeenCalled();
      }));

      it('should not fire the dateInput event if the value has not changed', () => {
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        inputEl.value = '12/12/2012';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        expect(testComponent.onDateInput).toHaveBeenCalledTimes(1);

        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();

        expect(testComponent.onDateInput).toHaveBeenCalledTimes(1);
      });

      it('should have updated the native input value when the dateChange event is emitted', () => {
        let valueDuringChangeEvent = '';

        (testComponent.onDateChange as jasmine.Spy).and.callFake(() => {
          valueDuringChangeEvent = inputEl.value;
        });

        const model = fixture.debugElement
          .query(By.directive(MatDatepicker))
          .injector.get<MatDateSelectionModel<Date>>(MatDateSelectionModel);

        model.updateSelection(new Date(2020, 0, 1), null);
        fixture.detectChanges();

        expect(valueDuringChangeEvent).toBe('1/1/2020');
      });

      it('should not fire dateInput when typing an invalid value', () => {
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        inputEl.value = 'a';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();

        inputEl.value = 'b';
        dispatchFakeEvent(inputEl, 'input');
        fixture.detectChanges();
        expect(testComponent.onDateInput).not.toHaveBeenCalled();
      });
    });

    describe('with ISO 8601 strings as input', () => {
      let fixture: ComponentFixture<DatepickerWithISOStrings>;
      let testComponent: DatepickerWithISOStrings;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithISOStrings, [MatNativeDateModule]);
        flush();
        testComponent = fixture.componentInstance;
      }));

      afterEach(fakeAsync(() => {
        testComponent.datepicker.close();
        fixture.detectChanges();
      }));

      it('should coerce ISO strings', fakeAsync(() => {
        expect(() => fixture.detectChanges()).not.toThrow();
        flush();
        fixture.detectChanges();

        expect(testComponent.datepicker.startAt).toEqual(new Date(2017, JUL, 1));
        expect(testComponent.datepickerInput.value).toEqual(new Date(2017, JUN, 1));
        expect(testComponent.datepickerInput.min).toEqual(new Date(2017, JAN, 1));
        expect(testComponent.datepickerInput.max).toEqual(new Date(2017, DEC, 31));
      }));
    });

    describe('with events', () => {
      let fixture: ComponentFixture<DatepickerWithEvents>;
      let testComponent: DatepickerWithEvents;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerWithEvents, [MatNativeDateModule]);
        fixture.detectChanges();
        flush();
        testComponent = fixture.componentInstance;
      }));

      it('should dispatch an event when a datepicker is opened', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        expect(testComponent.openedSpy).toHaveBeenCalled();
      }));

      it('should dispatch an event when a datepicker is closed', fakeAsync(() => {
        testComponent.datepicker.open();
        fixture.detectChanges();
        tick();

        testComponent.datepicker.close();
        flush();
        fixture.detectChanges();

        expect(testComponent.closedSpy).toHaveBeenCalled();
      }));
    });

    describe('datepicker that opens on focus', () => {
      let fixture: ComponentFixture<DatepickerOpeningOnFocus>;
      let testComponent: DatepickerOpeningOnFocus;
      let input: HTMLInputElement;

      beforeEach(fakeAsync(() => {
        fixture = createComponent(DatepickerOpeningOnFocus, [MatNativeDateModule]);
        fixture.detectChanges();
        testComponent = fixture.componentInstance;
        input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      }));

      it('should not reopen if the browser fires the focus event asynchronously', fakeAsync(() => {
        // Stub out the real focus method so we can call it reliably.
        spyOn(input, 'focus').and.callFake(() => {
          // Dispatch the event handler async to simulate the IE11 behavior.
          Promise.resolve().then(() => dispatchFakeEvent(input, 'focus'));
        });

        // Open initially by focusing.
        input.focus();
        fixture.detectChanges();
        tick();
        flush();

        // Due to some browser limitations we can't install a stub on `document.activeElement`
        // so instead we have to override the previously-focused element manually.
        (fixture.componentInstance.datepicker as any)._focusedElementBeforeOpen = input;

        // Ensure that the datepicker is actually open.
        // Ensure that the datepicker is actually open.
        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to be open.')
          .toBe(true);

        // Close the datepicker.
        testComponent.datepicker.close();
        fixture.detectChanges();

        // Schedule the input to be focused asynchronously.
        input.focus();
        fixture.detectChanges();
        tick();

        // Flush out the scheduled tasks.
        flush();

        expect(testComponent.datepicker.opened)
          .withContext('Expected datepicker to be closed.')
          .toBe(false);
      }));
    });

    describe('datepicker directionality', () => {
      it('should pass along the directionality to the popup', fakeAsync(() => {
        const fixture = createComponent(
          StandardDatepicker,
          [MatNativeDateModule],
          [
            {
              provide: Directionality,
              useValue: {value: 'rtl'},
            },
          ],
        );

        fixture.detectChanges();
        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        const overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;

        expect(overlay.getAttribute('dir')).toBe('rtl');
      }));

      it('should update the popup direction if the directionality value changes', fakeAsync(() => {
        const dirProvider = {value: 'ltr'};
        const fixture = createComponent(
          StandardDatepicker,
          [MatNativeDateModule],
          [
            {
              provide: Directionality,
              useFactory: () => dirProvider,
            },
          ],
        );

        fixture.detectChanges();
        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        let overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;

        expect(overlay.getAttribute('dir')).toBe('ltr');

        fixture.componentInstance.datepicker.close();
        fixture.detectChanges();
        flush();

        dirProvider.value = 'rtl';
        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        overlay = document.querySelector('.cdk-overlay-connected-position-bounding-box')!;

        expect(overlay.getAttribute('dir')).toBe('rtl');
      }));

      it('should pass along the directionality to the dialog in touch mode', fakeAsync(() => {
        const fixture = createComponent(
          StandardDatepicker,
          [MatNativeDateModule],
          [
            {
              provide: Directionality,
              useValue: {value: 'rtl'},
            },
          ],
        );

        fixture.componentInstance.touch = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        fixture.componentInstance.datepicker.open();
        fixture.detectChanges();
        tick();

        const overlay = document.querySelector('.cdk-global-overlay-wrapper')!;

        expect(overlay.getAttribute('dir')).toBe('rtl');
      }));
    });
  });

  describe('with missing DateAdapter and MAT_DATE_FORMATS', () => {
    it('should throw when created', () => {
      expect(() => createComponent(StandardDatepicker)).toThrowError(
        /MatDatepicker: No provider found for .*/,
      );
    });
  });

  describe('datepicker directives without a datepicker', () => {
    it('should not throw on init if toggle does not have a datepicker', () => {
      expect(() => {
        const fixture = createComponent(DatepickerToggleWithNoDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();
      }).not.toThrow();
    });

    it('should not set aria-haspopup if toggle does not have a datepicker', () => {
      const fixture = createComponent(DatepickerToggleWithNoDatepicker, [MatNativeDateModule]);
      fixture.detectChanges();
      const toggle = fixture.nativeElement.querySelector('.mat-datepicker-toggle button');

      expect(toggle.hasAttribute('aria-haspopup')).toBe(false);
    });

    it('should not throw on init if input does not have a datepicker', () => {
      expect(() => {
        const fixture = createComponent(DatepickerInputWithNoDatepicker, [MatNativeDateModule]);
        fixture.detectChanges();
      }).not.toThrow();
    });

    it('should not set aria-haspopup if input does not have a datepicker', () => {
      const fixture = createComponent(DatepickerInputWithNoDatepicker, [MatNativeDateModule]);
      fixture.detectChanges();
      const toggle = fixture.nativeElement.querySelector('input');

      expect(toggle.hasAttribute('aria-haspopup')).toBe(false);
    });
  });

  describe('popup positioning', () => {
    let fixture: ComponentFixture<StandardDatepicker>;
    let testComponent: StandardDatepicker;
    let input: HTMLElement;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(StandardDatepicker, [MatNativeDateModule]);
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
      input = fixture.debugElement.query(By.css('input'))!.nativeElement;
      input.style.position = 'fixed';
    }));

    it('should be below and to the right when there is plenty of space', fakeAsync(() => {
      input.style.top = input.style.left = '20px';
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.top))
        .withContext('Expected popup to align to input bottom.')
        .toBe(Math.floor(inputRect.bottom));
      expect(Math.floor(overlayRect.left))
        .withContext('Expected popup to align to input left.')
        .toBe(Math.floor(inputRect.left));
    }));

    it('should be above and to the right when there is no space below', fakeAsync(() => {
      input.style.bottom = input.style.left = '20px';
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.bottom))
        .withContext('Expected popup to align to input top.')
        .toBe(Math.floor(inputRect.top));
      expect(Math.floor(overlayRect.left))
        .withContext('Expected popup to align to input left.')
        .toBe(Math.floor(inputRect.left));
    }));

    it('should be below and to the left when there is no space on the right', fakeAsync(() => {
      input.style.top = input.style.right = '20px';
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.top))
        .withContext('Expected popup to align to input bottom.')
        .toBe(Math.floor(inputRect.bottom));
      expect(Math.floor(overlayRect.right))
        .withContext('Expected popup to align to input right.')
        .toBe(Math.floor(inputRect.right));
    }));

    it('should be above and to the left when there is no space on the bottom', fakeAsync(() => {
      input.style.bottom = input.style.right = '20px';
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.bottom))
        .withContext('Expected popup to align to input top.')
        .toBe(Math.floor(inputRect.top));
      expect(Math.floor(overlayRect.right))
        .withContext('Expected popup to align to input right.')
        .toBe(Math.floor(inputRect.right));
    }));

    it('should be able to customize the calendar position along the X axis', fakeAsync(() => {
      input.style.top = input.style.left = '200px';
      testComponent.xPosition = 'end';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.right))
        .withContext('Expected popup to align to input right.')
        .toBe(Math.floor(inputRect.right));
    }));

    it('should be able to customize the calendar position along the Y axis', fakeAsync(() => {
      input.style.bottom = input.style.left = '100px';
      testComponent.yPosition = 'above';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const overlayRect = document.querySelector('.cdk-overlay-pane')!.getBoundingClientRect();
      const inputRect = input.getBoundingClientRect();

      expect(Math.floor(overlayRect.bottom))
        .withContext('Expected popup to align to input top.')
        .toBe(Math.floor(inputRect.top));
    }));
  });

  describe('internationalization', () => {
    let fixture: ComponentFixture<DatepickerWithi18n>;
    let testComponent: DatepickerWithi18n;
    let input: HTMLInputElement;

    beforeEach(() => {
      fixture = createComponent(
        DatepickerWithi18n,
        [MatNativeDateModule, NativeDateModule],
        [{provide: MAT_DATE_LOCALE, useValue: 'de-DE'}],
      );
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
      input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
    });

    it('should have the correct input value even when inverted date format', fakeAsync(() => {
      if (typeof Intl === 'undefined') {
        // Skip this test if the internationalization API is not supported in the current
        // browser. Browsers like Safari 9 do not support the "Intl" API.
        return;
      }

      const selected = new Date(2017, SEP, 1);
      testComponent.date = selected;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      // Normally the proper date format would 01.09.2017, but some browsers seem format the
      // date without the leading zero. (e.g. 1.9.2017).
      expect(input.value).toMatch(/0?1\.0?9\.2017/);
      expect(testComponent.datepickerInput.value).toBe(selected);
    }));
  });

  describe('datepicker with custom header', () => {
    let fixture: ComponentFixture<DatepickerWithCustomHeader>;
    let testComponent: DatepickerWithCustomHeader;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(
        DatepickerWithCustomHeader,
        [MatNativeDateModule],
        [],
        [CustomHeaderForDatepicker],
      );
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
    }));

    it('should instantiate a datepicker with a custom header', fakeAsync(() => {
      expect(testComponent).toBeTruthy();
    }));

    it('should find the standard header element', fakeAsync(() => {
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();
      flush();
      fixture.detectChanges();

      expect(document.querySelector('mat-calendar-header')).toBeTruthy();
    }));

    it('should find the custom element', fakeAsync(() => {
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();
      flush();
      fixture.detectChanges();

      expect(document.querySelector('.custom-element')).toBeTruthy();
    }));
  });

  it('should not trigger validators if new date object for same date is set for `min`', () => {
    const fixture = createComponent(
      DatepickerInputWithCustomValidator,
      [MatNativeDateModule],
      undefined,
      [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(
      DatepickerInputWithCustomValidator,
      [MatNativeDateModule],
      undefined,
      [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(StandardDatepicker, [MatNativeDateModule]);
    fixture.detectChanges();

    const minDate = new Date(2019, 0, 1);
    const spy = jasmine.createSpy('stateChanges spy');
    const subscription = fixture.componentInstance.datepickerInput.stateChanges.subscribe(spy);

    fixture.componentInstance.min = minDate;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    expect(spy).toHaveBeenCalledTimes(1);

    fixture.componentInstance.min = 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(StandardDatepicker, [MatNativeDateModule]);
    fixture.detectChanges();

    const maxDate = new Date(2120, 0, 1);
    const spy = jasmine.createSpy('stateChanges spy');
    const subscription = fixture.componentInstance.datepickerInput.stateChanges.subscribe(spy);

    fixture.componentInstance.max = maxDate;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    expect(spy).toHaveBeenCalledTimes(1);

    fixture.componentInstance.max = new Date(maxDate);
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    expect(spy).toHaveBeenCalledTimes(1);

    subscription.unsubscribe();
  });

  describe('panelClass input', () => {
    let fixture: ComponentFixture<PanelClassDatepicker>;
    let testComponent: PanelClassDatepicker;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(PanelClassDatepicker, [MatNativeDateModule]);
      fixture.detectChanges();

      testComponent = fixture.componentInstance;
    }));

    afterEach(fakeAsync(() => {
      testComponent.datepicker.close();
      fixture.detectChanges();
      flush();
    }));

    it('should accept a single class', () => {
      testComponent.panelClass = 'foobar';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(testComponent.datepicker.panelClass).toEqual(['foobar']);
    });

    it('should accept multiple classes', () => {
      testComponent.panelClass = 'foo bar';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(testComponent.datepicker.panelClass).toEqual(['foo', 'bar']);
    });

    it('should work with ngClass', fakeAsync(() => {
      testComponent.panelClass = ['foo', 'bar'];
      fixture.changeDetectorRef.markForCheck();
      testComponent.datepicker.open();
      fixture.detectChanges();
      tick();

      const actualClasses = document.querySelector(
        '.mat-datepicker-content .mat-calendar',
      )!.classList;
      expect(actualClasses.contains('foo')).toBe(true);
      expect(actualClasses.contains('bar')).toBe(true);
    }));
  });
});

/**
 * Styles that set input elements to a fixed width. This helps with client rect measurements
 * (i.e. that the datepicker aligns properly). Inputs have different dimensions in different
 * browsers. e.g. in Firefox the input width is uneven, causing unexpected deviations in measuring.
 * Note: The input should be able to shrink as on iOS the viewport width is very little but the
 * datepicker inputs should not leave the viewport (as that throws off measuring too).
 */
const inputFixedWidthStyles = `
  input {
    width: 100%;
    max-width: 150px;
    border: none;
    box-sizing: border-box;
  }
`;

@Component({
  template: `
    <input [matDatepicker]="d" [value]="date" [min]="min" [max]="max">
    <mat-datepicker
      #d
      [touchUi]="touch"
      [disabled]="disabled"
      [opened]="opened"
      [xPosition]="xPosition"
      [yPosition]="yPosition"></mat-datepicker>
  `,
  styles: inputFixedWidthStyles,
  standalone: false,
})
class StandardDatepicker {
  opened = false;
  touch = false;
  disabled = false;
  date: Date | null = new Date(2020, JAN, 1);
  min: Date;
  max: Date;
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
  xPosition: DatepickerDropdownPositionX;
  yPosition: DatepickerDropdownPositionY;
}

@Component({
  template: `
    <input [matDatepicker]="d"><input [matDatepicker]="d"><mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class MultiInputDatepicker {}

@Component({
  template: `<mat-datepicker #d></mat-datepicker>`,
  standalone: false,
})
class NoInputDatepicker {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="d" [value]="date">
    <mat-datepicker #d [startAt]="startDate"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithStartAt {
  date = new Date(2020, JAN, 1);
  startDate = new Date(2010, JAN, 1);
  @ViewChild('d') datepicker: MatDatepicker<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="d" [value]="date">
    <mat-datepicker #d startView="year" (monthSelected)="onYearSelection()"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithStartViewYear {
  date = new Date(2020, JAN, 1);
  @ViewChild('d') datepicker: MatDatepicker<Date>;

  onYearSelection() {}
}

@Component({
  template: `
    <input [matDatepicker]="d" [value]="date">
    <mat-datepicker #d startView="multi-year"
        (yearSelected)="onMultiYearSelection()"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithStartViewMultiYear {
  date = new Date(2020, JAN, 1);
  @ViewChild('d') datepicker: MatDatepicker<Date>;

  onMultiYearSelection() {}
}

@Component({
  template: `
    <input [(ngModel)]="selected" [matDatepicker]="d">
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithNgModel {
  selected: Date | null = null;
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}

@Component({
  template: `
    <input [formControl]="formControl" [matDatepicker]="d">
    <mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithFormControl {
  formControl = new FormControl<Date | null>(null);
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
  @ViewChild(MatDatepickerToggle) datepickerToggle: MatDatepickerToggle<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="d">
    <mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
    <mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithToggle {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) input: MatDatepickerInput<Date>;
  touchUI = true;
  restoreFocus = true;
  ariaLabel: string;
}

@Component({
  encapsulation: ViewEncapsulation.ShadowDom,
  template: `
    <input [matDatepicker]="d">
    <mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
    <mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithToggleInShadowDom extends DatepickerWithToggle {}

@Component({
  template: `
    <input [matDatepicker]="d">
    <mat-datepicker-toggle [for]="d">
      <div class="custom-icon" matDatepickerToggleIcon></div>
    </mat-datepicker-toggle>
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithCustomIcon {}

@Component({
  template: `
      <mat-form-field>
        <mat-label>Pick a date</mat-label>
        <input matInput [matDatepicker]="d">
        <mat-datepicker #d></mat-datepicker>
      </mat-form-field>
  `,
  standalone: false,
})
class FormFieldDatepicker {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
  @ViewChild(MatFormField) formField: MatFormField;
}

@Component({
  template: `
    <input [matDatepicker]="d" [(ngModel)]="date" [min]="minDate" [max]="maxDate">
    <mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithMinAndMaxValidation {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(NgModel) model: NgModel;
  date: Date | null;
  minDate = new Date(2010, JAN, 1);
  maxDate = new Date(2020, JAN, 1);
}

@Component({
  template: `
    <input [matDatepicker]="d" [(ngModel)]="date" [matDatepickerFilter]="filter">
    <mat-datepicker-toggle [for]="d"></mat-datepicker-toggle>
    <mat-datepicker #d [touchUi]="true"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithFilterAndValidation {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(NgModel) model: NgModel;
  date: Date;
  filter = (date: Date | null) => date?.getDate() != 1;
}

@Component({
  template: `
    <input [matDatepicker]="d" (change)="onChange()" (input)="onInput()"
           (dateChange)="onDateChange()" (dateInput)="onDateInput()">
    <mat-datepicker #d [touchUi]="true"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithChangeAndInputEvents {
  @ViewChild('d') datepicker: MatDatepicker<Date>;

  onChange() {}

  onInput() {}

  onDateChange() {}

  onDateInput() {}
}

@Component({
  template: `
    <input [matDatepicker]="d" [(ngModel)]="date">
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithi18n {
  date: Date | null = new Date(2010, JAN, 1);
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="d" [(ngModel)]="value" [min]="min" [max]="max">
    <mat-datepicker #d [startAt]="startAt"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithISOStrings {
  value = new Date(2017, JUN, 1).toISOString();
  min = new Date(2017, JAN, 1).toISOString();
  max = new Date(2017, DEC, 31).toISOString();
  startAt = new Date(2017, JUL, 1).toISOString();
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}

@Component({
  template: `
    <input [(ngModel)]="selected" [matDatepicker]="d">
    <mat-datepicker (opened)="openedSpy()" (closed)="closedSpy()" #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithEvents {
  selected: Date | null = null;
  openedSpy = jasmine.createSpy('opened spy');
  closedSpy = jasmine.createSpy('closed spy');
  @ViewChild('d') datepicker: MatDatepicker<Date>;
}

@Component({
  template: `
    <input (focus)="d.open()" [matDatepicker]="d">
    <mat-datepicker #d="matDatepicker"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerOpeningOnFocus {
  @ViewChild(MatDatepicker) datepicker: MatDatepicker<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="ch">
    <mat-datepicker #ch [calendarHeaderComponent]="customHeaderForDatePicker"></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithCustomHeader {
  @ViewChild('ch') datepicker: MatDatepicker<Date>;
  customHeaderForDatePicker = CustomHeaderForDatepicker;
}

@Component({
  template: `
    <div class="custom-element">Custom element</div>
    <mat-calendar-header></mat-calendar-header>
  `,
  standalone: false,
})
class CustomHeaderForDatepicker {}

@Component({
  template: `
    <input [matDatepicker]="assignedDatepicker" [value]="date">
    <mat-datepicker #d [touchUi]="touch"></mat-datepicker>
  `,
  standalone: false,
})
class DelayedDatepicker {
  @ViewChild('d') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
  date: Date | null;
  assignedDatepicker: MatDatepicker<Date>;
}

@Component({
  template: `
    <input [matDatepicker]="d">
    <mat-datepicker-toggle tabIndex="7" [for]="d" [disabled]="disabled">
      <div class="custom-icon" matDatepickerToggleIcon></div>
    </mat-datepicker-toggle>
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerWithTabindexOnToggle {
  disabled = false;
}

@Component({
  template: `
    <mat-datepicker-toggle></mat-datepicker-toggle>
  `,
  standalone: false,
})
class DatepickerToggleWithNoDatepicker {}

@Component({
  template: `
    <input [matDatepicker]="d">
  `,
  standalone: false,
})
class DatepickerInputWithNoDatepicker {}

@Directive({
  selector: '[customValidator]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: CustomValidator,
      multi: true,
    },
  ],
  standalone: false,
})
class CustomValidator implements Validator {
  validate = jasmine.createSpy('validate spy').and.returnValue(null);
}

@Component({
  template: `
    <input [matDatepicker]="d" [(ngModel)]="value" [min]="min" [max]="max" customValidator>
    <mat-datepicker #d></mat-datepicker>
  `,
  standalone: false,
})
class DatepickerInputWithCustomValidator {
  @ViewChild(CustomValidator) validator: CustomValidator;
  value: Date;
  min: Date;
  max: Date;
}

@Component({
  template: `
  <input [matDatepicker]="d" [value]="date">
  <mat-datepicker [panelClass]="panelClass" touchUi #d></mat-datepicker>
  `,
  standalone: false,
})
class PanelClassDatepicker {
  date = new Date(0);
  panelClass: any;
  @ViewChild('d') datepicker: MatDatepicker<Date>;
}
