Files

290 lines
9.7 KiB
PHP
Raw Permalink 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\Controller;
use App\Entity\Booking;
use App\Entity\Resource;
use App\Entity\ResourceGroup;
use App\Entity\User;
use App\Enum\ResourceType;
use App\Model\TimeRange;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* What a card on the floor plan says.
*
* Who holds what is listed inside the card rather than in a title tooltip: a tooltip only shows
* on hover, which is useless on touch and invisible when scanning the plan.
*/
class FloorPlanTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $em;
protected function setUp(): void
{
$this->client = self::createClient(server: ['HTTPS' => true]);
$this->client->disableReboot();
$this->em = self::getContainer()->get(EntityManagerInterface::class);
$this->em->getConnection()->beginTransaction();
}
protected function tearDown(): void
{
$this->em->getConnection()->rollBack();
parent::tearDown();
}
public function testNoCardCarriesATooltip(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
$crawler = $this->board();
self::assertCount(1, $crawler->filter('[data-spot]'));
self::assertCount(0, $crawler->filter('[data-spot][title], [data-spot] [title]'));
}
public function testABookingIsListedInsideTheShape(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
self::assertSame(
['Schreibtisch 01', 'B. Kranz 09:00–13:00', 'frei ab 13:00'],
$this->linesOf($this->board()),
);
}
/**
* Full names are shortened so a card stays one line per booking at phone width. The surname
* survives, not the first name, so two colleagues sharing a first name stay distinguishable.
*/
public function testALongNameIsShortenedToAnInitialAndTheSurname(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Maximilian Wolkenstein'), 0, 1440);
self::assertContains('M. Wolkenstein', $this->linesOf($this->board()));
}
public function testASingleWordNameIsLeftAlone(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Cher'), 0, 1440);
self::assertContains('Cher', $this->linesOf($this->board()));
}
public function testYourOwnBookingReadsAsDu(): void
{
$desk = $this->desk();
$me = $this->user('[email protected]', 'Anna Berger');
$this->book($desk, $me, 540, 780);
self::assertContains('du 09:00–13:00', $this->linesOf($this->board($me)));
}
public function testAnAllDayBookingOmitsTheTimes(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 0, 1440);
self::assertSame(['Schreibtisch 01', 'B. Kranz'], $this->linesOf($this->board()));
}
/**
* A card lists three lines. More than that is summarised rather than allowed to spill out --
* and the counter has to account for the line the counter itself occupies.
*/
public function testTooManyBookingsAreSummarised(): void
{
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Ann Alpha'), 0, 360);
$this->book($desk, $this->user('[email protected]', 'Bea Beta'), 360, 720);
$this->book($desk, $this->user('[email protected]', 'Cem Gamma'), 720, 1080);
$this->book($desk, $this->user('[email protected]', 'Dia Delta'), 1080, 1440);
$lines = $this->linesOf($this->board());
self::assertSame(
['Schreibtisch 01', 'A. Alpha bis 06:00', 'B. Beta 06:00–12:00', '+2 weitere'],
$lines,
);
// Label plus MAX_LINES, never more.
self::assertLessThanOrEqual(4, \count($lines));
}
public function testAFreeCardShowsOnlyItsLabel(): void
{
$this->desk();
self::assertSame(['Schreibtisch 01'], $this->linesOf($this->board()));
}
/**
* Free is otherwise only a colour, which a screen reader cannot see.
*/
public function testAFreeCardSaysSoToScreenReaders(): void
{
$this->desk();
self::assertSame('frei', $this->board()->filter('[data-spot] .sr-only')->text());
}
public function testARoomIsDrawnWithItsNameAndHowManyAreFree(): void
{
$room = $this->room('Raum Nord');
$this->deskIn($room, 'Schreibtisch 01');
$this->deskIn($room, 'Schreibtisch 02');
$this->deskIn($room, 'Schreibtisch 03');
self::assertSame(['Raum Nord · 3 frei'], $this->roomsOf($this->board()));
}
/**
* The badge counts what can still be had for the whole span, so a desk that is free only in
* the afternoon does not count -- it is already amber on the plan, and counting it would
* make the number disagree with the colour beside it.
*/
public function testAPartlyBookedDeskIsNotCountedAsFree(): void
{
$room = $this->room('Raum Nord');
$partly = $this->deskIn($room, 'Schreibtisch 01');
$this->deskIn($room, 'Schreibtisch 02');
$this->book($partly, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
self::assertSame(['Raum Nord · 1 frei'], $this->roomsOf($this->board()));
}
/**
* The count means the same thing as the colours beside it, so it is relative to the board's
* span: the desk that is unavailable for the whole day is free for an afternoon board.
*/
public function testTheFreeCountFollowsTheSelectedSpan(): void
{
$room = $this->room('Raum Nord');
$desk = $this->deskIn($room, 'Schreibtisch 01');
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
self::assertSame(['Raum Nord · belegt'], $this->roomsOf($this->board()));
self::assertSame(['Raum Nord · 1 frei'], $this->roomsOf($this->board(query: '&from=14:00&to=18:00')));
}
public function testARoomWithNothingLeftSaysSoInsteadOfShowingAZero(): void
{
$room = $this->room('Raum Nord');
$desk = $this->deskIn($room, 'Schreibtisch 01');
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 0, 1440);
self::assertSame(['Raum Nord · belegt'], $this->roomsOf($this->board()));
}
public function testRoomsAreListedInTheirConfiguredOrder(): void
{
$this->deskIn($this->room('Zweiter Raum', 1), 'Schreibtisch 04');
$this->deskIn($this->room('Erster Raum', 0), 'Schreibtisch 01');
self::assertSame(['Erster Raum · 1 frei', 'Zweiter Raum · 1 frei'], $this->roomsOf($this->board()));
}
/**
* A resource with no group is still bookable and still listed -- the plan simply gives it no
* room heading. That is the path the car park runs on.
*/
public function testAnUngroupedDeskIsListedWithoutARoom(): void
{
$this->desk();
$crawler = $this->board();
self::assertSame([], $this->roomsOf($crawler));
self::assertSame(['Schreibtisch 01'], $this->linesOf($crawler));
}
/**
* The room headings.
*
* @return list<string>
*/
private function roomsOf(\Symfony\Component\DomCrawler\Crawler $crawler): array
{
return $crawler->filter('#board-floorplan section > h3')->each(
static fn (\Symfony\Component\DomCrawler\Crawler $node): string => trim(preg_replace('/\s+/', ' ', $node->text()) ?? ''),
);
}
private function room(string $label, int $position = 0): ResourceGroup
{
$group = new ResourceGroup(ResourceType::DESK, $label, $position);
$this->em->persist($group);
$this->em->flush();
return $group;
}
private function deskIn(ResourceGroup $group, string $label): Resource
{
$resource = new Resource(ResourceType::DESK, $label);
$resource->setGroup($group);
$this->em->persist($resource);
$this->em->flush();
return $resource;
}
/**
* The label, then one entry per listed line.
*
* @return list<string>
*/
private function linesOf(\Symfony\Component\DomCrawler\Crawler $crawler): array
{
return $crawler->filter('[data-spot] > span:not(.sr-only), [data-spot] li')->each(
static fn (\Symfony\Component\DomCrawler\Crawler $node): string => trim($node->text()),
);
}
private function board(?User $as = null, string $query = ''): \Symfony\Component\DomCrawler\Crawler
{
$this->client->loginUser($as ?? $this->user('[email protected]', 'Anna Berger'));
return $this->client->request('GET', '/book/desk?date=2026-10-01'.$query);
}
private function desk(): Resource
{
$resource = new Resource(ResourceType::DESK, 'Schreibtisch 01');
$this->em->persist($resource);
$this->em->flush();
return $resource;
}
private function book(Resource $resource, User $user, int $start, int $end): void
{
$this->em->persist(new Booking($user, $resource, new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes($start, $end)));
$this->em->flush();
}
private function user(string $email, string $name): User
{
$existing = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
if (null !== $existing) {
return $existing;
}
$user = new User($email, $name);
$user->setRoles(['ROLE_EMPLOYEE']);
$this->em->persist($user);
$this->em->flush();
return $user;
}
}