Skip to content

Commit

Permalink
chore: testing calentar and date-time utils more
Browse files Browse the repository at this point in the history
  • Loading branch information
dpitcock committed Dec 8, 2024
1 parent 3c11f26 commit 4874df7
Show file tree
Hide file tree
Showing 24 changed files with 580 additions and 195 deletions.
2 changes: 1 addition & 1 deletion src/calendar/grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import styles from '../styles.css.js';
* - (keyboard navigation) Safari/Chrome+VO - date announcements are not interruptive and can be missed if navigating fast.
*/

interface GridProps {
export interface GridProps {
isDateEnabled: DatePickerProps.IsDateEnabledFunction;
dateDisabledReason: CalendarProps.DateDisabledReasonFunction;
focusedDate: Date | null;
Expand Down
12 changes: 2 additions & 10 deletions src/calendar/grid/use-calendar-grid-keyboard-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,8 @@ import { isSameMonth, isSameYear } from 'date-fns';
import { KeyCode } from '../../internal/keycode';
import handleKey from '../../internal/utils/handle-key';
import { CalendarProps } from '../interfaces';
import {
moveMonthDown,
moveMonthUp,
moveNextDay,
moveNextMonth,
moveNextWeek,
movePrevDay,
movePrevMonth,
movePrevWeek,
} from '../utils/navigation';
import { moveNextDay, moveNextWeek, movePrevDay, movePrevWeek } from '../utils/navigation-day';
import { moveMonthDown, moveMonthUp, moveNextMonth, movePrevMonth } from '../utils/navigation-month';

export default function useCalendarGridKeyboardNavigation({
baseDate,
Expand Down
3 changes: 2 additions & 1 deletion src/calendar/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import useCalendarGridRows from './grid/use-calendar-grid-rows';
import CalendarHeader from './header';
import { CalendarProps } from './interfaces.js';
import useCalendarLabels from './use-calendar-labels';
import { getBaseDay, getBaseMonth } from './utils/navigation';
import { getBaseDay } from './utils/navigation-day';
import { getBaseMonth } from './utils/navigation-month';

import styles from './styles.css.js';

Expand Down
144 changes: 144 additions & 0 deletions src/calendar/utils/__tests__/navigation-day.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { addYears, startOfMonth, subYears } from 'date-fns';

import { getBaseDay, moveDay, moveNextDay, moveNextWeek, movePrevDay, movePrevWeek } from '../navigation-day';

//mocked to avoid complications with timezones in the 'date-fns' package
jest.mock('date-fns', () => ({ ...jest.requireActual('date-fns'), startOfMonth: () => new Date('2025-01-01') }));

const startDate = new Date(`2022-01-15`);

function disableDates(...blockedDates: string[]) {
return (date: Date) => blockedDates.every(blocked => new Date(blocked).getTime() !== date.getTime());
}

describe('getBaseDay', () => {
const baseDate = new Date('2024-04-15');

test('returns first day of month when it is focusable', () => {
const isDateFocusable = () => true; // All days are focusable
const result = getBaseDay(baseDate, isDateFocusable);
expect(result).toEqual(startOfMonth(baseDate));
});

test('returns first focusable day within the same month', () => {
const isDateFocusable = (date: Date) => date.getDate() === 5; // Only the 5th of each month is focusable
const result = getBaseDay(baseDate, isDateFocusable);
expect(result).toEqual(new Date('2025-01-05')); //5th of month after mock
});

test('returns first day of month when no days are focusable', () => {
const isDateFocusable = () => false; // No days are focusable
const result = getBaseDay(baseDate, isDateFocusable);
expect(result).toEqual(new Date(`2025-01-01`)); //start of year of mock
});

test('returns first day of month when first focusable day is in next month', () => {
const isDateFocusable = (date: Date) => date >= new Date('2024-05-01'); // Only days from May 1, 2024 are focusable
const result = getBaseDay(baseDate, isDateFocusable);
expect(result).toEqual(startOfMonth(baseDate));
});

test('handles custom isDateFocusable function', () => {
const isDateFocusable = (date: Date) => date.getDay() === 1; // Only Mondays are focusable
const result = getBaseDay(baseDate, isDateFocusable);
expect(result).toEqual(new Date('2025-01-06')); //first monday after mock
expect(result.getDay()).toEqual(1);
});

test('works with dates at the start of the month', () => {
const startOfMonthDate = new Date('2024-04-01');
const isDateFocusable = (date: Date) => date.getDate() === 3; // Only the 3rd of each month is focusable
const result = getBaseDay(startOfMonthDate, isDateFocusable);
expect(result).toEqual(new Date('2025-01-03')); // first 3rd day after mock
});

test('works with dates at the end of the month', () => {
const endOfMonthDate = new Date('2024-04-30');
const isDateFocusable = (date: Date) => date.getDate() === 28; // Only the 28th of each month is focusable
const result = getBaseDay(endOfMonthDate, isDateFocusable);
expect(result).toEqual(new Date('2025-01-28')); // April 28, 2024
});
});

describe('moveDay', () => {
const baseDate = new Date('2024-01-15');

test('moves forward to the next active day', () => {
const isDateFocusable = (date: Date) => date.getDate() === 20; // Only the 20th of each month is active
const result = moveDay(baseDate, isDateFocusable, 1);
expect(result).toEqual(new Date('2024-01-20'));
});

test('moves backward to the previous active day', () => {
const isDateFocusable = (date: Date) => date.getDate() === 10; // Only the 10th of each month is active
const result = moveDay(baseDate, isDateFocusable, -1);
expect(result).toEqual(new Date('2024-01-10'));
});

test('returns start date if no active day found within 1 year forward', () => {
const isDateFocusable = () => false; // No days are active
const result = moveDay(baseDate, isDateFocusable, 1);
expect(result).toEqual(baseDate);
});

test('returns start date if no active day found within 1 year backward', () => {
const isDateFocusable = () => false; // No days are active
const result = moveDay(baseDate, isDateFocusable, -1);
expect(result).toEqual(baseDate);
});

test('finds active day at the edge of 1 year limit', () => {
const oneYearLater = addYears(baseDate, 1);
const oneYearEarlier = subYears(baseDate, 1);

const isDateFocusable = (date: Date) =>
date.getTime() === oneYearLater.getTime() || date.getTime() === oneYearEarlier.getTime();

const forwardResult = moveDay(baseDate, isDateFocusable, 1);
expect(forwardResult).toEqual(oneYearLater);

const backwardResult = moveDay(baseDate, isDateFocusable, -1);
expect(backwardResult).toEqual(oneYearEarlier);
});

test('handles custom isDateFocusable function', () => {
const isDateFocusable = (date: Date) => date.getDay() === 1; // Only Mondays are active
const result = moveDay(baseDate, isDateFocusable, 1);
expect(result).toEqual(new Date('2024-01-22'));
expect(result.getDay()).toEqual(1);
});

test('moves multiple steps forward', () => {
const isDateFocusable = (date: Date) => date.getDate() % 5 === 0; // Every 5th day is active
const result = moveDay(baseDate, isDateFocusable, 3);
expect(result).toEqual(new Date('2024-01-30'));
});

test('moves multiple steps backward', () => {
const isDateFocusable = (date: Date) => date.getDate() % 5 === 0; // Every 5th day is active
const result = moveDay(baseDate, isDateFocusable, -3);
expect(result).toEqual(new Date('2023-12-25'));
});
});

test('moveNextDay', () => {
expect(moveNextDay(startDate, () => true)).toEqual(new Date('2022-01-16'));
expect(moveNextDay(startDate, disableDates('2022-01-16', '2022-01-17'))).toEqual(new Date('2022-01-18'));
});

test('movePrevDay', () => {
expect(movePrevDay(startDate, () => true)).toEqual(new Date('2022-01-14'));
expect(movePrevDay(startDate, disableDates('2022-01-14', '2022-01-13'))).toEqual(new Date('2022-01-12'));
});

test('moveNextWeek', () => {
expect(moveNextWeek(startDate, () => true)).toEqual(new Date('2022-01-22'));
expect(moveNextWeek(startDate, disableDates('2022-01-22', '2022-01-29'))).toEqual(new Date('2022-02-05'));
});

test('movePrevWeek', () => {
expect(movePrevWeek(startDate, () => true)).toEqual(new Date('2022-01-08'));
expect(movePrevWeek(startDate, disableDates('2022-01-08', '2022-01-01'))).toEqual(new Date('2021-12-25'));
});
67 changes: 67 additions & 0 deletions src/calendar/utils/__tests__/navigation-month.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { addYears, subYears } from 'date-fns';

import { getBaseMonth, moveMonth } from '../navigation-month';

//mocked to avoid complications with timezones in the 'date-fns' package
jest.mock('date-fns', () => ({ ...jest.requireActual('date-fns'), startOfYear: () => new Date('2025-01-01') }));

const startYear = '2022',
startMonth = '01',
startDay = '15';
const startDate = new Date(`${startYear}-${startMonth}-${startDay}`);

function disableMonths(...blockedDates: string[]) {
return (date: Date) => blockedDates.every(blocked => new Date(blocked).getMonth() !== date.getMonth());
}

describe('moveMonth', () => {
const baseDate = new Date('2024-01-01');
4;

test('moves forward to the next active month', () => {
const isDateFocusable = (date: Date) => date.getMonth() === 2; // Only March is active
const result = moveMonth(baseDate, isDateFocusable, 1);
expect(result).toEqual(new Date('2024-03-01'));
});

test('moves backward to the previous active month', () => {
const isDateFocusable = (date: Date) => date.getMonth() === 10; // Only November is active
const result = moveMonth(baseDate, isDateFocusable, -1);
expect(result).toEqual(new Date('2023-11-01')); // November 1, 2023
});

test('returns start date if no active month found within 10 years forward', () => {
const isDateFocusable = () => false; // No months are active
const result = moveMonth(baseDate, isDateFocusable, 1);
expect(result).toEqual(baseDate);
});

test('returns start date if no active month found within 10 years backward', () => {
const isDateFocusable = () => false; // No months are active
const result = moveMonth(baseDate, isDateFocusable, -1);
expect(result).toEqual(baseDate);
});

test('finds active month at the edge of 10 year limit', () => {
//set to match mock above
const tenYearsLater = addYears(new Date('2025-01-01'), 10);
const tenYearsEarlier = subYears(new Date('2025-01-01'), 10);

const isDateFocusable = (date: Date) =>
date.getTime() === tenYearsLater.getTime() || date.getTime() === tenYearsEarlier.getTime();

const forwardResult = moveMonth(baseDate, isDateFocusable, 1);
expect(forwardResult).toEqual(tenYearsLater);

const backwardResult = moveMonth(baseDate, isDateFocusable, -1);
expect(backwardResult).toEqual(tenYearsEarlier);
});
});

test('getBaseMonth', () => {
expect(getBaseMonth(startDate, () => true)).toEqual(new Date('2025-01-01')); //match mocked date
expect(getBaseMonth(startDate, disableMonths('2022-01', '2022-02'))).toEqual(new Date('2025-03')); //match first after mocked date
expect(getBaseMonth(startDate, () => false)).toEqual(new Date('2025-01-01'));
});
38 changes: 0 additions & 38 deletions src/calendar/utils/__tests__/navigation.test.ts

This file was deleted.

50 changes: 50 additions & 0 deletions src/calendar/utils/navigation-day.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { addDays, differenceInYears, isSameMonth, startOfMonth } from 'date-fns';

import { CalendarProps } from '../interfaces';

export function moveNextDay(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveDay(startDate, isDateFocusable, 1);
}

export function movePrevDay(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveDay(startDate, isDateFocusable, -1);
}

export function moveNextWeek(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveDay(startDate, isDateFocusable, 7);
}

export function movePrevWeek(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveDay(startDate, isDateFocusable, -7);
}

// Returns first enabled date of the month corresponding to the given date.
// If all month's days are disabled, the first day of the month is returned.
export function getBaseDay(date: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
const startDate = startOfMonth(date);

if (isDateFocusable(startDate)) {
return startDate;
}
const firstEnabledDate = moveDay(startDate, isDateFocusable, 1);
return isSameMonth(startDate, firstEnabledDate) ? firstEnabledDate : startDate;
}

// Iterates days forwards or backwards until the next active day is found.
// If there is no active day in a year range, the start day is returned.
export function moveDay(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction, step: number): Date {
const limitYears = 1;

let current = addDays(startDate, step);

while (!isDateFocusable(current)) {
if (Math.abs(differenceInYears(startDate, current)) > limitYears) {
return startDate;
}
current = addDays(current, step);
}

return current;
}
48 changes: 48 additions & 0 deletions src/calendar/utils/navigation-month.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { addMonths, differenceInYears, isSameYear, startOfYear } from 'date-fns';

import { CalendarProps } from '../interfaces';

export function moveNextMonth(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveMonth(startDate, isDateFocusable, 1);
}

export function movePrevMonth(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveMonth(startDate, isDateFocusable, -1);
}

export function moveMonthDown(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveMonth(startDate, isDateFocusable, 3);
}

export function moveMonthUp(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
return moveMonth(startDate, isDateFocusable, -3);
}

// Returns first enabled month of the year corresponding to the given date.
// If all year's months are disabled, the first month of the year is returned.
export function getBaseMonth(date: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction) {
const startDate = startOfYear(date);
if (isDateFocusable(startDate)) {
return startDate;
}
const firstEnabledDate = moveMonth(startDate, isDateFocusable, 1);
return isSameYear(startDate, firstEnabledDate) ? firstEnabledDate : startDate;
}

// Iterates months forwards or backwards until the next active month is found.
// If there is no active month in a 10 year range, the start month is returned.
export function moveMonth(startDate: Date, isDateFocusable: CalendarProps.IsDateEnabledFunction, step: number): Date {
const limitYears = 10;
let current = addMonths(startDate, step);

while (!isDateFocusable(current)) {
if (Math.abs(differenceInYears(startDate, current)) > limitYears) {
return startDate;
}
current = addMonths(current, step);
}

return current;
}
Loading

0 comments on commit 4874df7

Please sign in to comment.