room('Raum Nord'); $south = $this->room('Raum Süd'); $groups = SpotGroup::over([ $this->spot('D1', $north), $this->spot('D2', $north), $this->spot('D3', $south), ]); self::assertCount(2, $groups); self::assertSame($north, $groups[0]->group); self::assertCount(2, $groups[0]->spots); self::assertSame($south, $groups[1]->group); self::assertCount(1, $groups[1]->spots); } /** * The repository orders by group position, so the order spots arrive in is already the order * they should be drawn in -- grouping must not reshuffle it. */ public function testTheOrderSpotsArriveInIsKept(): void { $first = $this->room('Erster Raum'); $second = $this->room('Zweiter Raum'); $groups = SpotGroup::over([$this->spot('D1', $second), $this->spot('D2', $first)]); self::assertSame(['Zweiter Raum', 'Erster Raum'], array_map( static fn (SpotGroup $group): string => $group->group?->getLabel() ?? '', $groups, )); } /** * Two unpersisted groups both have a null id, so grouping cannot key on it. */ public function testTwoUnsavedRoomsDoNotCollapseIntoOne(): void { $groups = SpotGroup::over([$this->spot('D1', $this->room('A')), $this->spot('D2', $this->room('B'))]); self::assertCount(2, $groups); } public function testUngroupedSpotsCollectIntoOneTrailingGroup(): void { $room = $this->room('Raum Nord'); $groups = SpotGroup::over([$this->spot('D1', null), $this->spot('D2', $room), $this->spot('D3', null)]); self::assertCount(2, $groups); self::assertSame($room, $groups[0]->group); self::assertNull($groups[1]->group); self::assertCount(2, $groups[1]->spots); } public function testAPlanWithNoGroupsAtAllIsOneNullGroup(): void { $groups = SpotGroup::over([$this->spot('P1', null), $this->spot('P2', null)]); self::assertCount(1, $groups); self::assertNull($groups[0]->group); } public function testNoSpotsMeansNoGroups(): void { self::assertSame([], SpotGroup::over([])); } /** * FREE only, deliberately: a partly booked desk is already amber on the plan, and counting * it here would make the badge disagree with the colours beside it. */ public function testOnlyFullyFreeSpotsAreCounted(): void { $room = $this->room('Raum Nord'); $group = SpotGroup::over([ $this->spot('D1', $room), $this->spot('D2', $room, [$this->booking(540, 780)]), // PARTLY $this->spot('D3', $room, [$this->booking(0, 1440)]), // TAKEN ])[0]; self::assertCount(3, $group->spots); self::assertSame(1, $group->freeCount()); } /** * @param list $bookings */ private function spot(string $label, ?ResourceGroup $group, array $bookings = []): Spot { $resource = new Resource(ResourceType::DESK, $label); $resource->setGroup($group); return Spot::for($resource, $bookings, TimeRange::wholeDay(), $this->viewer()); } private function booking(int $start, int $end): Booking { return new Booking( new UserWithId('bea@ep-reisen.de', 'Bea', 2), new Resource(ResourceType::DESK, 'X'), new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes($start, $end), ); } private function room(string $label): ResourceGroup { return new ResourceGroup(ResourceType::DESK, $label); } private function viewer(): User { return new UserWithId('anna@ep-reisen.de', 'Anna', 1); } }