Files
ephub/tests/Model/TimeRangeTest.php
T

170 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Tests\Model;
use App\Model\TimeRange;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
class TimeRangeTest extends TestCase
{
public function testAWholeDayRunsFromMidnightToMidnight(): void
{
$range = TimeRange::wholeDay();
self::assertTrue($range->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<string, array{?string, ?string, ?string}>
*/
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];
}
public function testRemainingIsTheWholeSpanWhenNothingIsTaken(): void
{
$free = TimeRange::remaining(TimeRange::wholeDay(), []);
self::assertSame(['00:00–24:00'], self::render($free));
}
public function testRemainingSplitsAroundABookingInTheMiddle(): void
{
$free = TimeRange::remaining(TimeRange::wholeDay(), [TimeRange::fromMinutes(540, 780)]);
self::assertSame(['00:00–09:00', '13:00–24:00'], self::render($free));
}
public function testRemainingIsEmptyWhenTheSpanIsFullyCovered(): void
{
self::assertSame([], TimeRange::remaining(TimeRange::wholeDay(), [TimeRange::wholeDay()]));
}
public function testRemainingHandlesOverlappingAndUnsortedInput(): void
{
// Bookings on one resource cannot overlap, but the span may be covered by several that
// touch, and nothing guarantees the order they arrive in.
$free = TimeRange::remaining(TimeRange::wholeDay(), [
TimeRange::fromMinutes(780, 1020),
TimeRange::fromMinutes(540, 900),
]);
self::assertSame(['00:00–09:00', '17:00–24:00'], self::render($free));
}
public function testRemainingIgnoresWhatFallsOutsideTheSpan(): void
{
$free = TimeRange::remaining(TimeRange::fromMinutes(780, 1020), [TimeRange::fromMinutes(0, 540)]);
self::assertSame(['13:00–17:00'], self::render($free));
}
public function testRemainingClipsABookingThatOverhangsTheSpan(): void
{
$free = TimeRange::remaining(TimeRange::fromMinutes(480, 1080), [TimeRange::fromMinutes(0, 600)]);
self::assertSame(['10:00–18:00'], self::render($free));
}
public function testDurationIsInMinutes(): void
{
self::assertSame(240, TimeRange::fromMinutes(540, 780)->durationInMinutes());
self::assertSame(1440, TimeRange::wholeDay()->durationInMinutes());
}
/**
* 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()]));
}
public function testAOneSidedRangeReadsAsAbOrBis(): void
{
self::assertSame('bis 13:00', (string) TimeRange::fromMinutes(TimeRange::DAY_START, 13 * 60));
self::assertSame('ab 13:00', (string) TimeRange::fromMinutes(13 * 60, TimeRange::DAY_END));
self::assertSame('09:00–13:00', (string) TimeRange::fromMinutes(9 * 60, 13 * 60));
self::assertSame('ganztägig', (string) TimeRange::wholeDay());
}
/**
* @param list<TimeRange> $ranges
*
* @return list<string>
*/
private static function render(array $ranges): array
{
return array_map(static fn (TimeRange $range): string => $range->start().'–'.$range->end(), $ranges);
}
}