render($this->createWorkMonth()); $this->assertStringStartsWith('%PDF', $pdf); $this->assertGreaterThan(2000, strlen($pdf), 'A month of rows should not fit in a stub.'); } /** * A month whose days the SaaS never reported still has to produce a full sheet, so the * teamer can write on the empty lines. */ public function testItPlacesTheLogoInTheTopRightCorner(): void { $pdf = $this->render($this->createWorkMonth()); // A4 is 595.28 x 841.89pt and the page margins are 15mm at the side, 18mm at the top, // so the content corner sits at 552.76 / 790.87. Asserting the placement rather than // the markup is the only way to know the logo actually landed there. $this->assertSame(1, preg_match( '#([\d.]+) 0 0 ([\d.]+) ([\d.]+) ([\d.]+) cm\s*/\w+ Do#', $this->contentStreams($pdf), $placement ), 'The logo should be drawn exactly once.'); [, $width, $height, $x, $y] = array_map('floatval', $placement); $this->assertEqualsWithDelta(552.76, $x + $width, 1.0, 'Flush with the right margin.'); $this->assertEqualsWithDelta(790.87, $y + $height, 1.0, 'Flush with the top margin.'); $this->assertEqualsWithDelta(230 / 70, $width / $height, 0.05, 'The logo must not be stretched.'); } /** * A working-time record is worth printing without its letterhead. */ public function testItRendersWithoutALogo(): void { $renderer = new WorkMonthRenderer( static::getContainer()->get(Environment::class), ['project_dir' => sys_get_temp_dir()], ); $this->assertStringStartsWith('%PDF', $renderer->render($this->createWorkMonth())); } private function contentStreams(string $pdf): string { preg_match_all('#stream\r?\n(.*?)endstream#s', $pdf, $matches); $decoded = ''; foreach ($matches[1] as $stream) { $inflated = @gzuncompress(ltrim($stream, "\r\n")); if (false !== $inflated) { $decoded .= $inflated; } } return $decoded; } public function testItRendersAMonthWithoutAnyBookings(): void { $workMonth = (new TeamerWorkMonth()) ->setTeamer($this->createTeamer()) ->setYear(2026) ->setMonth(2) ->setDays(WorkMonthDays::fromArray([])) ; $this->assertStringStartsWith('%PDF', $this->render($workMonth)); } public function testTheFilenameCarriesTheTeamerAndTheMonth(): void { $this->assertSame( 'Arbeitszeitnachweis_Test_2026-03.pdf', $this->createRenderer()->filename($this->createWorkMonth()), ); } public function testTheFilenameSurvivesAnAwkwardName(): void { $renderer = $this->createRenderer(); $workMonth = $this->createWorkMonth(); $workMonth->getTeamer()->setLastName('Müller-Lüdenscheidt'); $this->assertSame('Arbeitszeitnachweis_M_ller_L_denscheidt_2026-03.pdf', $renderer->filename($workMonth)); } private function render(TeamerWorkMonth $workMonth): string { return $this->createRenderer()->render($workMonth); } private function createRenderer(): WorkMonthRenderer { $projectDir = static::getContainer()->getParameter('kernel.project_dir'); /** @var Environment $twig */ $twig = static::getContainer()->get(Environment::class); return new WorkMonthRenderer($twig, ['project_dir' => $projectDir]); } private function createWorkMonth(): TeamerWorkMonth { return (new TeamerWorkMonth()) ->setTeamer($this->createTeamer()) ->setYear(2026) ->setMonth(3) ->setDays(WorkMonthDays::fromArray([ '2026-03-01' => ['minutes' => 285], '2026-03-05' => ['minutes' => 0, 'absence' => 'Ruhetag 24/25'], '2026-03-12' => ['minutes' => 510], ])) ->setSourceIstMinutes(795) ; } private function createTeamer(): Teamer { return (new Teamer())->setFirstName('Theo')->setLastName('Test'); } }