feat: render the floor plan in html instead of svg

This commit is contained in:
fromme committed 2026-09-17 16:10:50 +02:00
1 parent 5a01054b0e
commit ad5d5acb00
20 files changed
+256 -356

No files matched your search

+50 -52
View File
@@ -15,10 +15,10 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* What a shape on the floor plan says.
* What a card on the floor plan says.
*
* Who holds what is listed inside the box rather than in an SVG <title>: a tooltip only shows on
* hover, which is useless on touch and invisible when scanning the plan.
* 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
{
@@ -39,20 +39,20 @@ class FloorPlanTest extends WebTestCase
parent::tearDown();
}
public function testNoShapeCarriesATooltip(): void
public function testNoCardCarriesATooltip(): void
{
$desk = $this->desk('desk-01');
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
$crawler = $this->board();
self::assertCount(0, $crawler->filter('svg title'));
self::assertCount(0, $crawler->filter('svg g title'));
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('desk-01');
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
self::assertSame(
@@ -62,11 +62,11 @@ class FloorPlanTest extends WebTestCase
}
/**
* Full names do not fit a 140px box at this size, so they are shortened.
* Full names are shortened so a card stays one line per booking at phone width.
*/
public function testALongNameIsShortenedToAFirstNameAndInitial(): void
{
$desk = $this->desk('desk-01');
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Maximilian Wolkenstein'), 0, 1440);
self::assertContains('Maximilian W.', $this->linesOf($this->board()));
@@ -74,7 +74,7 @@ class FloorPlanTest extends WebTestCase
public function testASingleWordNameIsLeftAlone(): void
{
$desk = $this->desk('desk-01');
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', 'Cher'), 0, 1440);
self::assertContains('Cher', $this->linesOf($this->board()));
@@ -82,7 +82,7 @@ class FloorPlanTest extends WebTestCase
public function testYourOwnBookingReadsAsDu(): void
{
$desk = $this->desk('desk-01');
$desk = $this->desk();
$me = $this->user('[email protected]', 'Anna Berger');
$this->book($desk, $me, 540, 780);
@@ -91,19 +91,19 @@ class FloorPlanTest extends WebTestCase
public function testAnAllDayBookingOmitsTheTimes(): void
{
$desk = $this->desk('desk-01');
$desk = $this->desk();
$this->book($desk, $this->user('[email protected]', '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 --
* 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('desk-01');
$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);
@@ -119,33 +119,29 @@ class FloorPlanTest extends WebTestCase
self::assertLessThanOrEqual(4, \count($lines));
}
public function testAFreeShapeShowsOnlyItsLabel(): void
public function testAFreeCardShowsOnlyItsLabel(): void
{
$this->desk('desk-01');
$this->desk();
self::assertSame(['Schreibtisch 01'], $this->linesOf($this->board()));
}
/**
* Screen readers get the same information the box shows, because role="img" on the <svg>
* would otherwise hide every one of those <text> elements.
* Free is otherwise only a colour, which a screen reader cannot see.
*/
public function testTheShapeCarriesAnAccessibleLabel(): void
public function testAFreeCardSaysSoToScreenReaders(): void
{
$desk = $this->desk('desk-01');
$this->book($desk, $this->user('[email protected]', 'Bea Kranz'), 540, 780);
$this->desk();
$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);
self::assertSame('frei', $this->board()->filter('[data-spot] .sr-only')->text());
}
public function testARoomIsDrawnWithItsNameAndHowManyAreFree(): void
{
$room = $this->room('Raum Nord', 'room-01');
$this->deskIn($room, 'desk-01', 'Schreibtisch 01');
$this->deskIn($room, 'desk-02', 'Schreibtisch 02');
$this->deskIn($room, 'desk-03', 'Schreibtisch 03');
$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()));
}
@@ -157,9 +153,9 @@ class FloorPlanTest extends WebTestCase
*/
public function testAPartlyBookedDeskIsNotCountedAsFree(): void
{
$room = $this->room('Raum Nord', 'room-01');
$partly = $this->deskIn($room, 'desk-01', 'Schreibtisch 01');
$this->deskIn($room, 'desk-02', 'Schreibtisch 02');
$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()));
@@ -171,8 +167,8 @@ class FloorPlanTest extends WebTestCase
*/
public function testTheFreeCountFollowsTheSelectedSpan(): void
{
$room = $this->room('Raum Nord', 'room-01');
$desk = $this->deskIn($room, 'desk-01', 'Schreibtisch 01');
$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()));
@@ -181,28 +177,28 @@ class FloorPlanTest extends WebTestCase
public function testARoomWithNothingLeftSaysSoInsteadOfShowingAZero(): void
{
$room = $this->room('Raum Nord', 'room-01');
$desk = $this->deskIn($room, 'desk-01', 'Schreibtisch 01');
$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 testRoomsAreDrawnInTheirConfiguredOrder(): void
public function testRoomsAreListedInTheirConfiguredOrder(): void
{
$this->deskIn($this->room('Zweiter Raum', 'room-02', 1), 'desk-04', 'Schreibtisch 04');
$this->deskIn($this->room('Erster Raum', 'room-01', 0), 'desk-01', 'Schreibtisch 01');
$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 drawn -- the plan simply does not
* frame it. That is the path the car park runs on.
* 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 testAnUngroupedDeskIsDrawnWithoutARoom(): void
public function testAnUngroupedDeskIsListedWithoutARoom(): void
{
$this->desk('desk-01');
$this->desk();
$crawler = $this->board();
@@ -211,29 +207,29 @@ class FloorPlanTest extends WebTestCase
}
/**
* The room captions, which sit directly under the <svg> rather than inside a shape's <g>.
* The room headings.
*
* @return list<string>
*/
private function roomsOf(\Symfony\Component\DomCrawler\Crawler $crawler): array
{
return $crawler->filter('svg > text')->each(
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, string $svgElementId, int $position = 0): ResourceGroup
private function room(string $label, int $position = 0): ResourceGroup
{
$group = new ResourceGroup(ResourceType::DESK, $label, $position, $svgElementId);
$group = new ResourceGroup(ResourceType::DESK, $label, $position);
$this->em->persist($group);
$this->em->flush();
return $group;
}
private function deskIn(ResourceGroup $group, string $svgElementId, string $label): Resource
private function deskIn(ResourceGroup $group, string $label): Resource
{
$resource = new Resource(ResourceType::DESK, $label, $svgElementId);
$resource = new Resource(ResourceType::DESK, $label);
$resource->setGroup($group);
$this->em->persist($resource);
$this->em->flush();
@@ -242,11 +238,13 @@ class FloorPlanTest extends WebTestCase
}
/**
* The label, then one entry per listed line.
*
* @return list<string>
*/
private function linesOf(\Symfony\Component\DomCrawler\Crawler $crawler): array
{
return $crawler->filter('svg g text')->each(
return $crawler->filter('[data-spot] > span:not(.sr-only), [data-spot] li')->each(
static fn (\Symfony\Component\DomCrawler\Crawler $node): string => trim($node->text()),
);
}
@@ -258,9 +256,9 @@ class FloorPlanTest extends WebTestCase
return $this->client->request('GET', '/book/desk?date=2026-10-01'.$query);
}
private function desk(string $svgElementId): Resource
private function desk(): Resource
{
$resource = new Resource(ResourceType::DESK, 'Schreibtisch 01', $svgElementId);
$resource = new Resource(ResourceType::DESK, 'Schreibtisch 01');
$this->em->persist($resource);
$this->em->flush();