* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD * @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE) * @link https://github.com/tecnickcom/tc-lib-pdf * * This file is part of tc-lib-pdf software library. */ namespace Test; use PHPUnit\Framework\Attributes\DataProvider; class HTMLRealPageCorpusTest extends TestUtil { private const CORPUS_FILE = __DIR__ . '/fixtures/html/real_pages/corpus.json'; /** * @param array $value * @return array */ private static function toStringKeyMap(array $value): array { $typed = []; foreach (\array_keys($value) as $key) { if (!\is_string($key)) { continue; } $typed[$key] = $value[$key] ?? null; } return $typed; } private static function scalarToString(mixed $value): string { return \is_scalar($value) ? (string) $value : ''; } /** @return list */ private static function toStringList(mixed $value): array { if (!\is_array($value)) { return []; } $list = []; $items = \array_filter($value, static fn(mixed $item): bool => \is_scalar($item)); return \array_values(\array_map(static fn(string|int|float|bool $item): string => (string) $item, $items)); } /** @return array> */ private static function toPageList(mixed $value): array { if (!\is_array($value)) { return []; } /** @var array> $pages */ $pages = []; /** @var array> $pageRows */ $pageRows = \array_values(\array_filter($value, static fn(mixed $page): bool => \is_array($page))); foreach ($pageRows as $page) { $pages[] = self::toStringKeyMap($page); } return $pages; } public static function setUpBeforeClass(): void { self::setUpFontsPath(); } /** * @return array{ * version: int, * failure_tags: array, * severity_levels: array, * pages: array> * } * @throws \Throwable */ private static function loadCorpus(): array { $raw = \file_get_contents(self::CORPUS_FILE); if ($raw === false) { throw new \RuntimeException('Unable to read corpus manifest: ' . self::CORPUS_FILE); } /** @var array|null $data */ $data = \json_decode($raw, true); if (!\is_array($data)) { throw new \RuntimeException('Invalid JSON in corpus manifest: ' . self::CORPUS_FILE); } /** @var array{ * version: int, * failure_tags: array, * severity_levels: array, * pages: array> * } $typed */ return [ 'version' => \is_int($data['version'] ?? null) ? $data['version'] : 0, 'failure_tags' => self::toStringList($data['failure_tags'] ?? []), 'severity_levels' => self::toStringList($data['severity_levels'] ?? []), 'pages' => self::toPageList($data['pages'] ?? []), ]; } /** @throws \Throwable */ public function testRealPageCorpusManifestHasRequiredArchetypesAndSeverityTaggedFailures(): void { $corpus = self::loadCorpus(); $this->assertSame(1, $corpus['version']); $expectedArchetypes = [ 'long-form article', 'invoice and statement', 'product/documentation page', 'admin/report dashboard with tables', 'form-heavy page', ]; $this->assertNotEmpty($corpus['failure_tags']); $this->assertNotEmpty($corpus['severity_levels']); $this->assertNotEmpty($corpus['pages']); $archetypes = []; $totalFailures = 0; foreach ($corpus['pages'] as $page) { $pageId = self::scalarToString($page['id'] ?? ''); $fixture = self::scalarToString($page['fixture'] ?? ''); $archetype = self::scalarToString($page['archetype'] ?? ''); $this->assertNotSame('', $pageId); $this->assertNotSame('', $fixture); $this->assertNotSame('', $archetype); $fixturePath = __DIR__ . '/fixtures/html/real_pages/' . $fixture; $this->assertFileExists($fixturePath, 'Missing corpus fixture for page id: ' . $pageId); $archetypes[$archetype] = true; $failures = []; if (isset($page['failures']) && \is_array($page['failures'])) { $failures = $page['failures']; } /** @var array> $failureRows */ $failureRows = \array_values(\array_filter($failures, static fn(mixed $failure): bool => \is_array( $failure, ))); foreach ($failureRows as $failure) { $typedFailure = self::toStringKeyMap($failure); $tag = self::scalarToString($typedFailure['tag'] ?? ''); $severity = self::scalarToString($typedFailure['severity'] ?? ''); $this->assertContains($tag, $corpus['failure_tags']); $this->assertContains($severity, $corpus['severity_levels']); $totalFailures++; } } \sort($expectedArchetypes); $actualArchetypes = \array_keys($archetypes); \sort($actualArchetypes); $this->assertSame($expectedArchetypes, $actualArchetypes); $this->assertGreaterThanOrEqual(0, $totalFailures, 'Corpus should track severity-tagged failures.'); } /** * @return array * @throws \Throwable */ public static function corpusPageProvider(): array { $corpus = self::loadCorpus(); $dataset = []; foreach ($corpus['pages'] as $page) { $pageId = self::scalarToString($page['id'] ?? 'unknown'); $fixture = self::scalarToString($page['fixture'] ?? ''); $dataset[$pageId] = [$pageId, $fixture]; } return $dataset; } /** @throws \Throwable */ #[DataProvider('corpusPageProvider')] public function testRealPageCorpusFixturesRenderWithoutFatal(string $pageId, string $fixture): void { $obj = new \Com\Tecnick\Pdf\Tcpdf(); $this->initFontAndPage($obj); $fixturePath = __DIR__ . '/fixtures/html/real_pages/' . $fixture; $html = \file_get_contents($fixturePath); $this->assertNotFalse($html, 'Unable to read fixture for page id: ' . $pageId); $obj->addHTMLCell($html, 10, 10, 190, 0); $pdf = $obj->getOutPDFString(); $this->assertNotSame('', $pdf, 'Expected rendered PDF output for page id: ' . $pageId); } }