import { LOCALE_ID } from '@angular/core' ; import { createHostFactory , SpectatorHost } from '@ngneat/spectator' ; import { format , startOfDay , startOfMonth } from 'date-fns' ; import { zonedTimeToUtc } from 'date-fns-tz' ; import { MockComponent } from 'ng-mocks' ; import { CalendarComponent , IconComponent } from '..' ; import { TestHelper } from '../../testing/test-helper' ; import { WindowRef } from '../../types/window-ref' ; import { CardComponent } from '../card' ; import { DropdownComponent } from '../dropdown/dropdown.component' ; import { ItemComponent } from '../item' ; import { RadioComponent } from '../radio' ; import { CalendarYearNavigatorConfig } from './options/calendar-year-navigator-config' ; describe ( 'CalendarComponent' , ( ) => { let spectator ; const createHost = createHostFactory ( { component : CalendarComponent , declarations : [ CalendarComponent , MockComponent ( IconComponent ) , DropdownComponent , RadioComponent , CardComponent , ItemComponent , ] , providers : [ { provide : LOCALE_ID , useValue : 'en-GB' , } , { provide : WindowRef , useValue : < WindowRef > { nativeWindow : window } , } , ] , imports : [ TestHelper . ionicModuleForTest ] , } ) ; beforeEach ( ( ) => { spectator = createHost ( '<kirby-calendar></kirby-calendar>' ) ; } ) ; it ( 'should create' , ( ) => { expect ( spectator . component ) . toBeTruthy ( ) ; } ) ; it ( 'should initially render the current month if selectedDate is not specified' , ( ) => { const currentDay = startOfDay ( new Date ( ) ) ; const currentMonth = startOfMonth ( new Date ( ) ) ; verifyMonthAndYear ( format ( currentMonth , 'MMMM yyyy' ) ) ; expect ( spectator . query ( '.day.today' ) ) . toHaveText ( format ( currentDay , 'd' ) ) ; } ) ; it ( 'should initially render the month of selectedDate if specified' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; verifyMonthAndYear ( 'August 1997' ) ; const headerTexts = trimmedTexts ( 'th' ) ; expect ( headerTexts ) . toEqual ( [ 'M' , 'T' , 'W' , 'T' , 'F' , 'S' , 'S' ] ) ; const dayTexts = trimmedTexts ( '.day.current-month' ) ; expect ( dayTexts . slice ( 0 , 5 ) ) . toEqual ( [ '1' , '2' , '3' , '4' , '5' ] ) ; expect ( dayTexts . length ) . toEqual ( 31 ) ; } ) ; it ( 'should be possible to specify which day is today by passing todayDate' , ( ) => { spectator . setInput ( 'todayDate' , localMidnightDate ( '1997-08-30' ) ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; expect ( spectator . query ( '.day.today' ) ) . toHaveText ( '30' ) ; } ) ; it ( 'should make it possible to navigate past and future months' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; spectator . click ( SEL_NAV_BACK ) ; verifyMonthAndYear ( 'July 1997' ) ; spectator . click ( SEL_NAV_FORWARD ) ; verifyMonthAndYear ( 'August 1997' ) ; spectator . click ( SEL_NAV_FORWARD ) ; verifyMonthAndYear ( 'September 1997' ) ; spectator . click ( SEL_NAV_FORWARD ) ; spectator . click ( SEL_NAV_FORWARD ) ; spectator . click ( SEL_NAV_FORWARD ) ; spectator . click ( SEL_NAV_FORWARD ) ; verifyMonthAndYear ( 'January 1998' ) ; } ) ; it ( 'should not be possible to navigate to any month preceding minDate, if specified' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; spectator . setInput ( 'minDate' , localMidnightDate ( '1997-06-15' ) ) ; spectator . click ( SEL_NAV_BACK ) ; spectator . click ( SEL_NAV_BACK ) ; verifyMonthAndYear ( 'June 1997' ) ; expect ( spectator . query ( SEL_NAV_BACK ) ) . toBeDisabled ( ) ; spectator . click ( SEL_NAV_BACK ) ; verifyMonthAndYear ( 'June 1997' ) ; } ) ; it ( 'should not be possible to navigate to any month exceeding maxDate, if specified' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; spectator . setInput ( 'maxDate' , localMidnightDate ( '1997-10-01' ) ) ; spectator . click ( SEL_NAV_FORWARD ) ; spectator . click ( SEL_NAV_FORWARD ) ; verifyMonthAndYear ( 'October 1997' ) ; expect ( spectator . query ( SEL_NAV_FORWARD ) ) . toBeDisabled ( ) ; spectator . click ( SEL_NAV_FORWARD ) ; verifyMonthAndYear ( 'October 1997' ) ; } ) ; it ( 'should emit a dateChange event when a valid date is clicked' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 14 ) ; expect ( captured . event ) . toEqual ( localMidnightDate ( '1997-08-14' ) ) ; } ) ; it ( 'should not emit a dateChange event when selectedDate is changed from the outside' , ( ) => { const captured = captureDateChangeEvents ( ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should not emit a dateChange event if disableWeekends is true and a weekend date is clicked' , ( ) => { spectator . setInput ( 'disableWeekends' , true ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 24 ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should not emit a dateChange event if disablePastDates is true and a date in the past is clicked' , ( ) => { spectator . setInput ( 'disablePastDates' , true ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; spectator . setInput ( 'todayDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 1 ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should not emit a dateChange event if disableFutureDates is true and a date in the future is clicked' , ( ) => { spectator . setInput ( 'disableFutureDates' , true ) ; spectator . setInput ( 'todayDate' , localMidnightDate ( '1997-08-29' ) ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 31 ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should not emit a dateChange event if clicking a date that was passed in disabledDates' , ( ) => { spectator . setInput ( 'disabledDates' , [ '1997-08-25' , '1997-08-26' ] . map ( localMidnightDate ) ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 26 ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should not emit a dateChange event if clicking the already selected date' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 29 ) ; expect ( captured . event ) . toBeUndefined ( ) ; } ) ; it ( 'should always emit a dateChange event when clicking today if alwaysEnableToday is set to true' , ( ) => { const today = localMidnightDate ( '1997-08-28' ) ; spectator . setInput ( 'disabledDates' , [ today ] ) ; spectator . setInput ( 'todayDate' , today ) ; spectator . setInput ( 'alwaysEnableToday' , true ) ; spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 28 ) ; expect ( captured . event ) . toEqual ( today ) ; } ) ; it ( 'should emit dateChange event as UTC midnights when timezone is set to UTC' , ( ) => { spectator . setInput ( 'timezone' , 'UTC' ) ; spectator . setInput ( 'selectedDate' , utcMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateChangeEvents ( ) ; clickDayOfMonth ( 14 ) ; expect ( captured . event ) . toEqual ( utcMidnightDate ( '1997-08-14' ) ) ; } ) ; it ( 'should emit dateSelect event when clicking a date that is not already selected' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateSelectEvents ( ) ; clickDayOfMonth ( 14 ) ; expect ( captured . event ) . toEqual ( localMidnightDate ( '1997-08-14' ) ) ; } ) ; it ( 'should emit dateSelect event when clicking the already selected date' , ( ) => { spectator . setInput ( 'selectedDate' , localMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateSelectEvents ( ) ; clickDayOfMonth ( 29 ) ; expect ( captured . event ) . toEqual ( localMidnightDate ( '1997-08-29' ) ) ; } ) ; it ( 'should emit dateSelect event as UTC midnights when timezone is set to UTC' , ( ) => { spectator . setInput ( 'timezone' , 'UTC' ) ; spectator . setInput ( 'selectedDate' , utcMidnightDate ( '1997-08-29' ) ) ; const captured = captureDateSelectEvents ( ) ; clickDayOfMonth ( 14 ) ; expect ( captured . event ) . toEqual ( utcMidnightDate ( '1997-08-14' ) ) ; } ) ; it ( 'should be tolerant of date input (selectedDate, todayDate, minDate, maxDate, and disabledDates) as both UTC midnight and local time midnight' , ( ) => { const localDate = localMidnightDate ( '1997-08-29' ) ; const utcDate = utcMidnightDate ( '1997-08-29' ) ; spectator . setInput ( 'selectedDate' , localDate ) ; expect ( spectator . component . selectedDate ) . toEqual ( localDate ) ; spectator . setInput ( 'selectedDate' , utcDate ) ; expect ( spectator . component . selectedDate ) . toEqual ( localDate ) ; spectator . setInput ( 'todayDate' , localDate ) ; expect ( spectator . component . todayDate ) . toEqual ( localDate ) ; spectator . setInput ( 'todayDate' , utcDate ) ; expect ( spectator . component . todayDate ) . toEqual ( localDate ) ; spectator . setInput ( 'minDate' , localDate ) ; expect ( spectator . component . minDate ) . toEqual ( localDate ) ; spectator . setInput ( 'minDate' , utcDate ) ; expect ( spectator . component . minDate ) . toEqual ( localDate ) ; spectator . setInput ( 'maxDate' , localDate ) ; expect ( spectator . component . maxDate ) . toEqual ( localDate ) ; spectator . setInput ( 'maxDate' , utcDate ) ; expect ( spectator . component . maxDate ) . toEqual ( localDate ) ; const localDates = [ '1997-08-29' , '1997-08-30' ] . map ( localMidnightDate ) ; const utcDates = [ '1997-08-29' , '1997-08-30' ] . map ( utcMidnightDate ) ; spectator . setInput ( 'disabledDates' , localDates ) ; expect ( spectator . component . disabledDates ) . toEqual ( localDates ) ; spectator . setInput ( 'disabledDates' , utcDates ) ; expect ( spectator . component . disabledDates ) . toEqual ( localDates ) ; } ) ; it ( 'should render days from Monday to Sunday' , ( ) => { expect ( spectator . queryAll ( 'th' ) . map ( ( _ ) => _ . textContent ) . join ( ' ' ) ) . toEqual ( 'M T W T F S S' ) ; } ) ; describe ( 'active month' , ( ) => { let todayDate ; beforeEach ( ( ) => { todayDate = new Date ( 2021 , 0 , 1 ) ; spectator . setInput ( 'todayDate' , todayDate ) ; spectator . setInput ( 'selectedDate' , todayDate ) ; } ) ; describe ( 'when `minDate` is set' , ( ) => { let minDate ; const yearsBeforeTodayDate = 4 ; beforeEach ( ( ) => { minDate = new Date ( todayDate . getFullYear ( ) - yearsBeforeTodayDate , 0 , 1 ) ; spectator . setInput ( 'minDate' , minDate ) ; } ) ; describe ( 'when `minDate` changes to a later date than `todayDate`' , ( ) => { let newMinDate ; beforeEach ( ( ) => { newMinDate = new Date ( todayDate . getFullYear ( ) + 1 , todayDate . getMonth ( ) , todayDate . getDay ( ) ) ; spectator . setInput ( 'minDate' , newMinDate ) ; } ) ; it ( 'should set active month based on changed `minDate`' , ( ) => { expect ( spectator . component . activeMonthName ) . toEqual ( format ( newMinDate , 'MMMM' ) ) ; } ) ; } ) ; } ) ; describe ( 'when `maxDate` is set' , ( ) => { let maxDate ; const yearsAfterTodayDate = 2 ; beforeEach ( ( ) => { maxDate = new Date ( todayDate . getFullYear ( ) + yearsAfterTodayDate , 0 , 1 ) ; spectator . setInput ( 'maxDate' , maxDate ) ; } ) ; describe ( 'when `maxDate` changes to an earlier date then `todayDate`' , ( ) => { let newMaxDate ; beforeEach ( ( ) => { newMaxDate = new Date ( todayDate . getFullYear ( ) - 1 , todayDate . getMonth ( ) , todayDate . getDay ( ) ) ; spectator . setInput ( 'maxDate' , newMaxDate ) ; } ) ; it ( 'should set active month based on changed `maxDate`' , ( ) => { expect ( spectator . component . activeMonthName ) . toEqual ( format ( newMaxDate , 'MMMM' ) ) ; } ) ; } ) ; } ) ; } ) ; describe ( 'year navigator' , ( ) => { describe ( 'by default' , ( ) => { it ( 'should not render' , ( ) => { expect ( spectator . element . querySelector ( 'kirby-dropdown' ) ) . toBeNull ( ) ; } ) ; } ) ; describe ( 'when yearNavigatorOptions are set' , ( ) => { let todayDate ; let todayDateYear ; let yearNavigatorOptions ; beforeEach ( ( ) => { todayDate = new Date ( 2021 , 0 , 1 ) ; todayDateYear = todayDate . getFullYear ( ) ; yearNavigatorOptions = { from : - 3 , to : 2 } ; spectator . setInput ( 'todayDate' , todayDate ) ; spectator . setInput ( 'selectedDate' , todayDate ) ; spectator . setInput ( 'yearNavigatorOptions' , yearNavigatorOptions ) ; } ) ; it ( 'should render' , ( ) => { expect ( spectator . element . querySelector ( 'kirby-dropdown' ) ) . not . toBeNull ( ) ; } ) ; it ( 'should change year on navigation' , ( ) => { const firstNavigatedYearIndex = spectator . component . navigatedYear ; spectator . setInput ( 'selectedDate' , new Date ( todayDateYear , 11 , 31 ) ) ; spectator . component . _changeMonth ( 1 ) ; expect ( spectator . component . activeYear ) . toEqual ( '2022' ) ; expect ( spectator . component . navigatedYear ) . toEqual ( firstNavigatedYearIndex + 1 ) ; } ) ; it ( 'should emit selected year on navigation' , ( ) => { const captured = captureYearSelectEvents ( ) ; spectator . triggerEventHandler ( DropdownComponent , 'change' , '2040' ) ; expect ( captured . event ) . toEqual ( 2040 ) ; } ) ; it ( 'should get navigable years based on `from` and `to` when `minDate` and `maxDate` are omitted' , ( ) => { spectator . setInput ( 'minDate' , undefined ) ; spectator . setInput ( 'maxDate' , undefined ) ; expect ( spectator . component . navigableYears ) . toEqual ( [ '2018' , '2019' , '2020' , '2021' , '2022' , '2023' , ] ) ; } ) ; it ( 'should prioritize `minDate` and `maxDate` over `from` and `to` when getting navigable years' , ( ) => { spectator . setInput ( 'minDate' , new Date ( todayDate . getFullYear ( ) - 2 , 0 , 1 ) ) ; spectator . setInput ( 'maxDate' , new Date ( todayDate . getFullYear ( ) + 3 , 11 , 31 ) ) ; expect ( spectator . component . navigableYears ) . toEqual ( [ '2019' , '2020' , '2021' , '2022' , '2023' , '2024' , ] ) ; } ) ; } ) ; } ) ; const SEL_NAV_BACK = '.header button:first-of-type' ; const SEL_NAV_FORWARD = '.header button:last-of-type' ; function localMidnightDate ( yyyyMMdd ) { return startOfDay ( new Date ( yyyyMMdd ) ) ; } function utcMidnightDate ( yyyyMMdd ) { return zonedTimeToUtc ( yyyyMMdd , 'UTC' ) ; } function clickDayOfMonth ( dateOneIndexed ) { spectator . click ( spectator . queryAll ( '.day.current-month' ) [ dateOneIndexed - 1 ] ) ; } function trimmedTexts ( selector ) { return spectator . queryAll < HTMLElement > ( selector ) . map ( ( _ ) => _ . innerText ) ; } function verifyMonthAndYear ( monthAndYearText ) { expect ( spectator . queryAll ( '.month-and-year span' ) . map ( ( _ ) => _ . textContent ) . join ( ' ' ) ) . toEqual ( monthAndYearText ) ; } function captureDateChangeEvents ( ) { let captured = { } ; spectator . output < Date > ( 'dateChange' ) . subscribe ( ( result ) => ( captured . event = result ) ) ; return captured ; } function captureDateSelectEvents ( ) { const captured = { } ; spectator . output < Date > ( 'dateSelect' ) . subscribe ( ( result ) => ( captured . event = result ) ) ; return captured ; } function captureYearSelectEvents ( ) { const captured = { } ; spectator . output < number > ( 'yearSelect' ) . subscribe ( ( result ) => ( captured . event = result ) ) ; return captured ; } } ) ;