weeks); foreach ($grid->weeks as $week) { self::assertCount(7, $week); } } public function testItStartsOnTheMondayOnOrBeforeTheFirst(): void { // 1 September 2026 is a Tuesday, so the grid opens on 31 August. $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21'), new \DateTimeImmutable('2026-09-21')); self::assertSame('2026-08-31', $grid->gridStart->format('Y-m-d')); self::assertSame('2026-09-01', $grid->monthStart->format('Y-m-d')); self::assertSame('1', $grid->gridStart->format('N')); } public function testAMonthBeginningOnAMondayStartsOnThatMonday(): void { // 1 June 2026 is a Monday: no leading days at all. $grid = MonthGrid::around(new \DateTimeImmutable('2026-06-10'), new \DateTimeImmutable('2026-06-10')); self::assertSame('2026-06-01', $grid->gridStart->format('Y-m-d')); } public function testTheEndIsTheLastCellOfTheGrid(): void { $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21'), new \DateTimeImmutable('2026-09-21')); self::assertSame('2026-10-11', $grid->gridEnd->format('Y-m-d')); self::assertEquals($grid->weeks[5][6]->date, $grid->gridEnd); } public function testEveryCellIsTheDayAfterTheOneBefore(): void { // Guards the modify('+1 day') walk: March and October carry the DST switches, where naive // arithmetic on timestamps would repeat or skip a day. foreach (['2026-03-15', '2026-10-15'] as $inMonth) { $grid = MonthGrid::around(new \DateTimeImmutable($inMonth), new \DateTimeImmutable($inMonth)); $expected = $grid->gridStart; foreach ($grid->weeks as $week) { foreach ($week as $day) { self::assertSame($expected->format('Y-m-d'), $day->date->format('Y-m-d')); self::assertSame('00:00:00', $day->date->format('H:i:s')); $expected = $expected->modify('+1 day'); } } } } public function testDaysOutsideTheMonthAreFlagged(): void { $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21'), new \DateTimeImmutable('2026-09-21')); self::assertFalse($grid->weeks[0][0]->inMonth, '31 August is a leading day.'); self::assertTrue($grid->weeks[0][1]->inMonth, '1 September is in the month.'); self::assertFalse($grid->weeks[5][6]->inMonth, '11 October is a trailing day.'); } public function testTodayIsNeitherPastNorAnyOtherDay(): void { $today = new \DateTimeImmutable('2026-09-21'); $grid = MonthGrid::around($today, $today); $flagged = []; foreach ($grid->weeks as $week) { foreach ($week as $day) { if ($day->isToday) { $flagged[] = $day->date->format('Y-m-d'); self::assertFalse($day->isPast, 'Today is not past.'); } } } self::assertSame(['2026-09-21'], $flagged); } public function testPastAndWeekendFlags(): void { $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21'), new \DateTimeImmutable('2026-09-21')); $days = []; foreach ($grid->weeks as $week) { foreach ($week as $day) { $days[$day->key()] = $day; } } self::assertTrue($days['2026-09-20']->isPast); self::assertFalse($days['2026-09-22']->isPast); // 19/20 September 2026 is a Saturday/Sunday. self::assertTrue($days['2026-09-19']->isWeekend); self::assertTrue($days['2026-09-20']->isWeekend); self::assertFalse($days['2026-09-21']->isWeekend); } public function testTheTimePartOfTheGivenDayIsIgnored(): void { $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21 17:43:12'), new \DateTimeImmutable('2026-09-21')); self::assertSame('2026-09-01 00:00:00', $grid->monthStart->format('Y-m-d H:i:s')); self::assertTrue($grid->weeks[3][0]->isToday, 'Today must still be recognised.'); } public function testTheKeyIsTheDayItIndexesPayloadBy(): void { $grid = MonthGrid::around(new \DateTimeImmutable('2026-09-21'), new \DateTimeImmutable('2026-09-21')); self::assertSame('2026-08-31', $grid->weeks[0][0]->key()); } }