: 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(); $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 testNoShapeCarriesATooltip(): void { $desk = $this->desk('desk-01'); $this->book($desk, $this->user('bea@ep-reisen.de', 'Bea Kranz'), 540, 780); $crawler = $this->board(); self::assertCount(0, $crawler->filter('svg title')); self::assertCount(0, $crawler->filter('svg g title')); } public function testABookingIsListedInsideTheShape(): void { $desk = $this->desk('desk-01'); $this->book($desk, $this->user('bea@ep-reisen.de', 'Bea Kranz'), 540, 780); self::assertSame( ['Schreibtisch 01', 'Bea K. 09:00–13:00', 'frei 13:00–24:00'], $this->linesOf($this->board()), ); } /** * Full names do not fit a 140px box at this size, so they are shortened. */ public function testALongNameIsShortenedToAFirstNameAndInitial(): void { $desk = $this->desk('desk-01'); $this->book($desk, $this->user('x@ep-reisen.de', 'Maximilian Wolkenstein'), 0, 1440); self::assertContains('Maximilian W.', $this->linesOf($this->board())); } public function testASingleWordNameIsLeftAlone(): void { $desk = $this->desk('desk-01'); $this->book($desk, $this->user('x@ep-reisen.de', 'Cher'), 0, 1440); self::assertContains('Cher', $this->linesOf($this->board())); } public function testYourOwnBookingReadsAsDu(): void { $desk = $this->desk('desk-01'); $me = $this->user('anna@ep-reisen.de', '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('desk-01'); $this->book($desk, $this->user('bea@ep-reisen.de', 'Bea Kranz'), 0, 1440); self::assertSame(['Schreibtisch 01', 'Bea K.'], $this->linesOf($this->board())); } /** * A box fits 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('desk-01'); $this->book($desk, $this->user('a@ep-reisen.de', 'Ann Alpha'), 0, 360); $this->book($desk, $this->user('b@ep-reisen.de', 'Bea Beta'), 360, 720); $this->book($desk, $this->user('c@ep-reisen.de', 'Cem Gamma'), 720, 1080); $this->book($desk, $this->user('d@ep-reisen.de', 'Dia Delta'), 1080, 1440); $lines = $this->linesOf($this->board()); self::assertSame( ['Schreibtisch 01', 'Ann A. 00:00–06:00', 'Bea B. 06:00–12:00', '+2 weitere'], $lines, ); // Label plus MAX_LINES, never more. self::assertLessThanOrEqual(4, \count($lines)); } public function testAFreeShapeShowsOnlyItsLabel(): void { $this->desk('desk-01'); self::assertSame(['Schreibtisch 01'], $this->linesOf($this->board())); } /** * Screen readers get the same information the box shows, because role="img" on the * would otherwise hide every one of those elements. */ public function testTheShapeCarriesAnAccessibleLabel(): void { $desk = $this->desk('desk-01'); $this->book($desk, $this->user('bea@ep-reisen.de', 'Bea Kranz'), 540, 780); $label = $this->board()->filter('svg g[aria-label]')->attr('aria-label'); self::assertSame('Schreibtisch 01 — Bea K. 09:00–13:00, frei 13:00–24:00', $label); } /** * @return list */ private function linesOf(\Symfony\Component\DomCrawler\Crawler $crawler): array { return $crawler->filter('svg g text')->each( static fn (\Symfony\Component\DomCrawler\Crawler $node): string => trim($node->text()), ); } private function board(?User $as = null): \Symfony\Component\DomCrawler\Crawler { $this->client->loginUser($as ?? $this->user('anna@ep-reisen.de', 'Anna Berger')); return $this->client->request('GET', '/book/desk?date=2026-10-01'); } private function desk(string $svgElementId): Resource { $resource = new Resource(ResourceType::DESK, 'Schreibtisch 01', $svgElementId); $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; } }