fix(client): dc calendar showing future challenges (#69256)

This commit is contained in:
Tom
2026-08-04 06:23:30 -05:00
committed by GitHub
parent be3d9ffcda
commit 604ae9fd7c
2 changed files with 113 additions and 5 deletions
@@ -0,0 +1,105 @@
import type { ReactElement } from 'react';
import { describe, expect, it, vi } from 'vitest';
import {
getDaysInMonth,
getMonthInfo,
type DailyChallengeMap
} from './calendar';
vi.mock('../../utils/get-words', () => ({
randomQuote: () => ({ quote: 'Test quote', author: 'Test author' })
}));
function buildMap(
entries: Record<string, Partial<DailyChallengeMap>>
): Map<string, DailyChallengeMap> {
const map = new Map<string, DailyChallengeMap>();
for (const [monthDay, entry] of Object.entries(entries)) {
map.set(monthDay, {
id: monthDay,
date: entry.date ?? '',
challengeNumber: entry.challengeNumber ?? 1,
title: entry.title ?? '',
completedLanguages: entry.completedLanguages ?? []
});
}
return map;
}
function isAvailable(days: JSX.Element[], day: number): boolean {
const element = days[day - 1] as ReactElement<{ isAvailable: boolean }>;
return element.props.isAvailable;
}
describe('getDaysInMonth', () => {
it('caps February at 28 days in a real leap year', () => {
expect(getDaysInMonth(2028, 1)).toBe(28);
});
it('is still 28 days in a real non-leap year', () => {
expect(getDaysInMonth(2026, 1)).toBe(28);
});
it('is unaffected for other months', () => {
expect(getDaysInMonth(2026, 6)).toBe(31);
expect(getDaysInMonth(2026, 3)).toBe(30);
});
});
describe('getMonthInfo', () => {
it('never generates a 29th day for February', () => {
const { days } = getMonthInfo(2028, 1, buildMap({}));
expect(days).toHaveLength(28);
});
it('marks a day unavailable when no challenge exists for it', () => {
const { days } = getMonthInfo(2026, 6, buildMap({}));
expect(isAvailable(days, 15)).toBe(false);
});
it('marks a day available when a challenge exists and no gating applies', () => {
const map = buildMap({ '07-15': { date: '2026-07-15' } });
const { days } = getMonthInfo(2026, 6, map);
expect(isAvailable(days, 15)).toBe(true);
});
it('hideDaysAfter hides days past the given day, regardless of data', () => {
const map = buildMap({
'07-05': { date: '2026-07-05' },
'07-10': { date: '2026-07-10' }
});
const { days } = getMonthInfo(2026, 6, map, 5);
expect(isAvailable(days, 5)).toBe(true);
expect(isAvailable(days, 10)).toBe(false);
});
it('hideDaysThrough hides days up to and including the given day', () => {
const map = buildMap({
'07-05': { date: '2026-07-05' },
'07-10': { date: '2026-07-10' }
});
const { days } = getMonthInfo(2026, 6, map, undefined, 5);
expect(isAvailable(days, 5)).toBe(false);
expect(isAvailable(days, 10)).toBe(true);
});
describe('requireExactYear', () => {
const map = buildMap({ '08-15': { date: '2025-08-15' } });
it('hides a day whose stored year does not match the displayed year', () => {
const { days } = getMonthInfo(2026, 7, map, undefined, undefined, true);
expect(isAvailable(days, 15)).toBe(false);
});
it('shows the day when the displayed year matches the stored year', () => {
const { days } = getMonthInfo(2025, 7, map, undefined, undefined, true);
expect(isAvailable(days, 15)).toBe(true);
});
it('is ignored when not set, matching evergreen year-agnostic lookup', () => {
const { days } = getMonthInfo(2026, 7, map);
expect(isAvailable(days, 15)).toBe(true);
});
});
});
@@ -45,7 +45,7 @@ interface AllDailyChallengeFromDb {
title: string;
}
interface DailyChallengeMap {
export interface DailyChallengeMap {
id: string;
date: string;
challengeNumber: number;
@@ -63,17 +63,18 @@ interface MonthInfo {
}
// Cap Feb to 28 days regardless of which "year" is displayed
const getDaysInMonth = (year: number, monthIndex: number): number => {
export const getDaysInMonth = (year: number, monthIndex: number): number => {
const realDays = new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
return monthIndex === 1 ? 28 : realDays;
};
const getMonthInfo = (
export const getMonthInfo = (
year: number,
monthIndex: number,
dailyChallengesMap: DailyChallengesMap,
hideDaysAfter?: number,
hideDaysThrough?: number
hideDaysThrough?: number,
requireExactYear?: boolean
) => {
// Create date for first of the month (handles rollover automatically)
const firstOfMonth = new Date(Date.UTC(year, monthIndex, 1));
@@ -95,6 +96,7 @@ const getMonthInfo = (
const title = challengeData?.title || '';
const isAvailable =
challengeData !== undefined &&
(!requireExactYear || challengeData.date === formattedDate) &&
(hideDaysAfter === undefined || day <= hideDaysAfter) &&
(hideDaysThrough === undefined || day > hideDaysThrough);
const challengeNumber = challengeData?.challengeNumber;
@@ -223,7 +225,8 @@ function DailyCodingChallengeCalendar({
todayMonth - 1 + monthOffset,
dailyChallengesMap,
lastDailyChallengeReleased && monthOffset === 0 ? todayDay : undefined,
lastDailyChallengeReleased && isBoundaryMonth ? todayDay : undefined
lastDailyChallengeReleased && isBoundaryMonth ? todayDay : undefined,
!lastDailyChallengeReleased
);
const showPrevButton = lastDailyChallengeReleased