isAllDay()); self::assertSame(0, $range->startMinute); self::assertSame(1440, $range->endMinute); } public function testTheEndMustComeAfterTheStart(): void { $this->expectException(\InvalidArgumentException::class); TimeRange::fromMinutes(13 * 60, 9 * 60); } public function testARangeCannotLeaveTheDay(): void { $this->expectException(\InvalidArgumentException::class); TimeRange::fromMinutes(0, 1441); } public function testTouchingRangesDoNotOverlap(): void { $morning = TimeRange::fromMinutes(9 * 60, 13 * 60); $afternoon = TimeRange::fromMinutes(13 * 60, 17 * 60); // Half-open, so handing a desk over at 13:00 is not a collision. self::assertFalse($morning->overlaps($afternoon)); self::assertFalse($afternoon->overlaps($morning)); } public function testPartlyCoveringRangesOverlapBothWays(): void { $a = TimeRange::fromMinutes(9 * 60, 13 * 60); $b = TimeRange::fromMinutes(12 * 60, 17 * 60); self::assertTrue($a->overlaps($b)); self::assertTrue($b->overlaps($a)); } public function testAWholeDayOverlapsEverything(): void { self::assertTrue(TimeRange::wholeDay()->overlaps(TimeRange::fromMinutes(0, 15))); } #[DataProvider('urlPairs')] public function testParsingTheBoardsQueryParameters(?string $from, ?string $to, ?string $expected): void { $range = TimeRange::parse($from, $to); self::assertSame($expected, null === $range ? null : $range->start().'-'.$range->end()); } /** * @return iterable */ public static function urlPairs(): iterable { yield 'both given' => ['09:00', '13:00', '09:00-13:00']; yield 'only a start is open to the end of the day' => ['09:00', null, '09:00-24:00']; yield 'only an end is open from midnight' => [null, '13:00', '00:00-13:00']; yield 'neither means the whole day, i.e. no filter' => [null, null, null]; yield 'empty strings are the same as absent' => ['', '', null]; yield 'reversed is rejected' => ['13:00', '09:00', null]; yield 'equal is rejected' => ['09:00', '09:00', null]; yield 'nonsense is rejected' => ['halb zehn', '13:00', null]; yield 'out of range is rejected' => ['25:00', '26:00', null]; } /** * Regression guard for a trap that cost real debugging time: Twig resolves `range.allDay` by * looking for a method literally named `allDay` BEFORE it tries `isAllDay()`, and it happily * calls a static one. While the factory was called allDay(), every template read back a * freshly built all-day range -- always truthy -- so the whole board silently ignored the * selected span. Renaming the factory to wholeDay() is what fixes it; this keeps it fixed. */ public function testTheAllDayAccessorIsNotShadowedInTwig(): void { $twig = new Environment(new ArrayLoader(['t' => '{{ range.allDay ? "yes" : "no" }}'])); self::assertSame('no', $twig->render('t', ['range' => TimeRange::fromMinutes(9 * 60, 13 * 60)])); self::assertSame('yes', $twig->render('t', ['range' => TimeRange::wholeDay()])); } }