1a9e9f334SAndreas Gohr<?php 2a9e9f334SAndreas Gohr 3a9e9f334SAndreas Gohrnamespace dokuwiki\plugin\dw2pdf\test; 4a9e9f334SAndreas Gohr 5bee95f00SAndreas Gohruse dokuwiki\plugin\dw2pdf\src\BookCreatorLiveSelectionCollector; 6a9e9f334SAndreas Gohruse dokuwiki\plugin\dw2pdf\src\Cache; 7a9e9f334SAndreas Gohruse dokuwiki\plugin\dw2pdf\src\Config; 8a9e9f334SAndreas Gohruse dokuwiki\plugin\dw2pdf\src\PageCollector; 9a9e9f334SAndreas Gohruse dokuwiki\plugin\dw2pdf\src\PdfExportService; 10a9e9f334SAndreas Gohruse DOMWrap\Document; 11*c124fc53SAndreas Gohruse DOMWrap\Element; 12a9e9f334SAndreas Gohr 13a9e9f334SAndreas Gohr/** 14a9e9f334SAndreas Gohr * End-to-end tests for the dw2pdf plugin 15a9e9f334SAndreas Gohr * 16a9e9f334SAndreas Gohr * @group plugin_dw2pdf 17a9e9f334SAndreas Gohr * @group plugins 18a9e9f334SAndreas Gohr */ 19a9e9f334SAndreas Gohrclass EndToEndTest extends \DokuWikiTest 20a9e9f334SAndreas Gohr{ 21a9e9f334SAndreas Gohr protected $pluginsEnabled = ['dw2pdf']; 22a9e9f334SAndreas Gohr 23a9e9f334SAndreas Gohr public function setUp(): void 24a9e9f334SAndreas Gohr { 25a9e9f334SAndreas Gohr parent::setUp(); 26a9e9f334SAndreas Gohr $_REQUEST = []; 27a9e9f334SAndreas Gohr } 28a9e9f334SAndreas Gohr 29a9e9f334SAndreas Gohr 30a9e9f334SAndreas Gohr /** 31bee95f00SAndreas Gohr * Create the pages, render them through the PdfExportService in debug mode and return the resulting HTML. 32a9e9f334SAndreas Gohr * 33bee95f00SAndreas Gohr * @param string|string[] $pages One or more pages to be included in the export 34bee95f00SAndreas Gohr * @return string Rendered HTML output 35a9e9f334SAndreas Gohr */ 36bee95f00SAndreas Gohr protected function getDebugHTML($pages, $conf = []): string 37a9e9f334SAndreas Gohr { 38bee95f00SAndreas Gohr $pages = (array)$pages; 39bee95f00SAndreas Gohr 40bee95f00SAndreas Gohr foreach ($pages as $page) { 41*c124fc53SAndreas Gohr $this->prepareFixturePage($page); 42bee95f00SAndreas Gohr } 43a9e9f334SAndreas Gohr 44a9e9f334SAndreas Gohr $config = new Config(array_merge( 45a9e9f334SAndreas Gohr $conf, 46bee95f00SAndreas Gohr [ 47bee95f00SAndreas Gohr 'debug' => 1, 48bee95f00SAndreas Gohr 'liveselection' => json_encode($pages) 49bee95f00SAndreas Gohr ] 50a9e9f334SAndreas Gohr )); 51bee95f00SAndreas Gohr $collector = new BookCreatorLiveSelectionCollector($config); 52a9e9f334SAndreas Gohr $cache = new Cache($config, $collector); 53a9e9f334SAndreas Gohr $service = new PdfExportService($config, $collector, $cache, 'Contents', 'tester'); 54a9e9f334SAndreas Gohr return $service->getDebugHtml(); 55a9e9f334SAndreas Gohr } 56a9e9f334SAndreas Gohr 57bee95f00SAndreas Gohr /** 58bee95f00SAndreas Gohr * Test that numbered headers are rendered correctly 59bee95f00SAndreas Gohr */ 60a9e9f334SAndreas Gohr public function testNumberedHeaders(): void 61a9e9f334SAndreas Gohr { 62a9e9f334SAndreas Gohr $html = $this->getDebugHTML('headers', ['headernumber' => 1]); 63a9e9f334SAndreas Gohr 64a9e9f334SAndreas Gohr $dom = (new Document())->html($html); 65a9e9f334SAndreas Gohr 66a9e9f334SAndreas Gohr $dom->find('h1')->each(function ($h) { 67a9e9f334SAndreas Gohr $this->assertMatchesRegularExpression('/^1\. Header/', $h->text()); 68a9e9f334SAndreas Gohr }); 69a9e9f334SAndreas Gohr 70a9e9f334SAndreas Gohr $dom->find('h2')->each(function ($h) { 71a9e9f334SAndreas Gohr $this->assertMatchesRegularExpression('/^\d\.\d\. Header/', $h->text()); 72a9e9f334SAndreas Gohr }); 73a9e9f334SAndreas Gohr 74a9e9f334SAndreas Gohr $dom->find('h3')->each(function ($h) { 75a9e9f334SAndreas Gohr $this->assertMatchesRegularExpression('/^\d\.\d\.\d\. Header/', $h->text()); 76a9e9f334SAndreas Gohr }); 77a9e9f334SAndreas Gohr } 78bee95f00SAndreas Gohr 79bee95f00SAndreas Gohr /** 80bee95f00SAndreas Gohr * Test that numbered headers are rendered correctly across multiple pages 81bee95f00SAndreas Gohr * 82bee95f00SAndreas Gohr * Each new page should increase the top-level header number 83bee95f00SAndreas Gohr */ 84bee95f00SAndreas Gohr public function testNumberedHeadersMultipage(): void 85bee95f00SAndreas Gohr { 86bee95f00SAndreas Gohr $html = $this->getDebugHTML(['headers', 'simple'], ['headernumber' => 1]); 87bee95f00SAndreas Gohr 88bee95f00SAndreas Gohr $dom = (new Document())->html($html); 89bee95f00SAndreas Gohr 90bee95f00SAndreas Gohr $count = 1; 91bee95f00SAndreas Gohr $dom->find('h1')->each(function ($h) use (&$count) { 92bee95f00SAndreas Gohr $this->assertMatchesRegularExpression('/^' . ($count++) . '\. /', $h->text()); 93bee95f00SAndreas Gohr }); 94bee95f00SAndreas Gohr } 95*c124fc53SAndreas Gohr 96*c124fc53SAndreas Gohr /** 97*c124fc53SAndreas Gohr * Ensure each rendered page begins with an anchor that namespaces intra-page links. 98*c124fc53SAndreas Gohr */ 99*c124fc53SAndreas Gohr public function testDocumentStartCreatesPageAnchors(): void 100*c124fc53SAndreas Gohr { 101*c124fc53SAndreas Gohr $html = $this->getDebugHTML(['renderer_features', 'target']); 102*c124fc53SAndreas Gohr 103*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 104*c124fc53SAndreas Gohr 105*c124fc53SAndreas Gohr foreach (['renderer_features', 'target'] as $pageId) { 106*c124fc53SAndreas Gohr $anchors = $dom->find('a[name="' . $pageId . '__"]'); 107*c124fc53SAndreas Gohr $this->assertSame( 108*c124fc53SAndreas Gohr 1, 109*c124fc53SAndreas Gohr count($anchors), 110*c124fc53SAndreas Gohr 'Missing document_start anchor for ' . $pageId 111*c124fc53SAndreas Gohr ); 112*c124fc53SAndreas Gohr } 113*c124fc53SAndreas Gohr } 114*c124fc53SAndreas Gohr 115*c124fc53SAndreas Gohr /** 116*c124fc53SAndreas Gohr * Bookmarks should only be produced up to the configured level and include numbering. 117*c124fc53SAndreas Gohr */ 118*c124fc53SAndreas Gohr public function testBookmarksRespectConfiguredLevels(): void 119*c124fc53SAndreas Gohr { 120*c124fc53SAndreas Gohr $html = $this->getDebugHTML('headers', ['headernumber' => 1, 'maxbookmarks' => 2]); 121*c124fc53SAndreas Gohr 122*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 123*c124fc53SAndreas Gohr $bookmarks = $dom->find('bookmark'); 124*c124fc53SAndreas Gohr 125*c124fc53SAndreas Gohr $this->assertGreaterThan(0, count($bookmarks)); 126*c124fc53SAndreas Gohr 127*c124fc53SAndreas Gohr foreach ($bookmarks as $bookmark) { 128*c124fc53SAndreas Gohr $this->assertLessThanOrEqual( 129*c124fc53SAndreas Gohr 1, 130*c124fc53SAndreas Gohr (int)$bookmark->attr('level'), 131*c124fc53SAndreas Gohr 'Bookmark level exceeded configured maximum' 132*c124fc53SAndreas Gohr ); 133*c124fc53SAndreas Gohr 134*c124fc53SAndreas Gohr $content = trim((string)$bookmark->attr('content')); 135*c124fc53SAndreas Gohr $this->assertMatchesRegularExpression('/^\d+(?:\.\d+)*\.\s+Header/', $content); 136*c124fc53SAndreas Gohr } 137*c124fc53SAndreas Gohr 138*c124fc53SAndreas Gohr $this->assertSame(count($dom->find('h1, h2')), count($bookmarks)); 139*c124fc53SAndreas Gohr } 140*c124fc53SAndreas Gohr 141*c124fc53SAndreas Gohr /** 142*c124fc53SAndreas Gohr * Local section links should include the page-specific prefix. 143*c124fc53SAndreas Gohr */ 144*c124fc53SAndreas Gohr public function testLocallinksArePrefixedWithPageId(): void 145*c124fc53SAndreas Gohr { 146*c124fc53SAndreas Gohr $html = $this->getDebugHTML(['renderer_features', 'target']); 147*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 148*c124fc53SAndreas Gohr 149*c124fc53SAndreas Gohr $link = $this->findLinkByText($dom, 'Jump to Remote Section'); 150*c124fc53SAndreas Gohr $this->assertNotNull($link, 'Local section link missing'); 151*c124fc53SAndreas Gohr 152*c124fc53SAndreas Gohr $this->assertSame('#renderer_features__remote_section', $link->attr('href')); 153*c124fc53SAndreas Gohr } 154*c124fc53SAndreas Gohr 155*c124fc53SAndreas Gohr /** 156*c124fc53SAndreas Gohr * Internal links must expose dw2pdf data attributes so the writer can retarget them. 157*c124fc53SAndreas Gohr */ 158*c124fc53SAndreas Gohr public function testInternalLinksExposeDw2pdfMetadata(): void 159*c124fc53SAndreas Gohr { 160*c124fc53SAndreas Gohr $html = $this->getRawRendererHtml('renderer_features', [], ['target']); 161*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 162*c124fc53SAndreas Gohr 163*c124fc53SAndreas Gohr $pageLink = $this->findLinkByText($dom, 'Target page link'); 164*c124fc53SAndreas Gohr $this->assertNotNull($pageLink, 'Page link missing'); 165*c124fc53SAndreas Gohr $this->assertSame('target', $pageLink->attr('data-dw2pdf-target')); 166*c124fc53SAndreas Gohr $this->assertSame('', $pageLink->attr('data-dw2pdf-hash')); 167*c124fc53SAndreas Gohr 168*c124fc53SAndreas Gohr $sectionLink = $this->findLinkByText($dom, 'Target section link'); 169*c124fc53SAndreas Gohr $this->assertNotNull($sectionLink, 'Section link missing'); 170*c124fc53SAndreas Gohr $this->assertSame('target', $sectionLink->attr('data-dw2pdf-target')); 171*c124fc53SAndreas Gohr $this->assertSame('sub_section', $sectionLink->attr('data-dw2pdf-hash')); 172*c124fc53SAndreas Gohr } 173*c124fc53SAndreas Gohr 174*c124fc53SAndreas Gohr /** 175*c124fc53SAndreas Gohr * Centered media needs to be wrapped so CSS centering survives inside mPDF. 176*c124fc53SAndreas Gohr */ 177*c124fc53SAndreas Gohr public function testCenteredMediaIsWrapped(): void 178*c124fc53SAndreas Gohr { 179*c124fc53SAndreas Gohr $html = $this->getDebugHTML('renderer_features'); 180*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 181*c124fc53SAndreas Gohr 182*c124fc53SAndreas Gohr $wrappers = $dom->find('div[align="center"][style*="text-align: center"]'); 183*c124fc53SAndreas Gohr $this->assertGreaterThan(0, count($wrappers), 'Centered media wrapper missing'); 184*c124fc53SAndreas Gohr $this->assertGreaterThan(0, count($wrappers->first()->find('img'))); 185*c124fc53SAndreas Gohr } 186*c124fc53SAndreas Gohr 187*c124fc53SAndreas Gohr /** 188*c124fc53SAndreas Gohr * Acronyms should render as plain text to avoid useless hover hints in PDFs. 189*c124fc53SAndreas Gohr */ 190*c124fc53SAndreas Gohr public function testAcronymOutputDropsHover(): void 191*c124fc53SAndreas Gohr { 192*c124fc53SAndreas Gohr $html = $this->getDebugHTML('renderer_features'); 193*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 194*c124fc53SAndreas Gohr 195*c124fc53SAndreas Gohr $this->assertSame(0, count($dom->find('acronym'))); 196*c124fc53SAndreas Gohr $this->assertStringContainsString('FAQ', $html); 197*c124fc53SAndreas Gohr $this->assertStringNotContainsString('Frequently Asked Questions', $html); 198*c124fc53SAndreas Gohr } 199*c124fc53SAndreas Gohr 200*c124fc53SAndreas Gohr /** 201*c124fc53SAndreas Gohr * Email addresses must not be obfuscated so that mailto links remain readable. 202*c124fc53SAndreas Gohr */ 203*c124fc53SAndreas Gohr public function testEmailLinksStayReadable(): void 204*c124fc53SAndreas Gohr { 205*c124fc53SAndreas Gohr $html = $this->getDebugHTML('renderer_features'); 206*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 207*c124fc53SAndreas Gohr 208*c124fc53SAndreas Gohr $link = $this->findLinkByText($dom, 'test@example.com'); 209*c124fc53SAndreas Gohr $this->assertNotNull($link, 'Email link missing'); 210*c124fc53SAndreas Gohr $this->assertSame('mailto:test@example.com', $link->attr('href')); 211*c124fc53SAndreas Gohr } 212*c124fc53SAndreas Gohr 213*c124fc53SAndreas Gohr /** 214*c124fc53SAndreas Gohr * Interwiki links should be prefixed with the respective icon. 215*c124fc53SAndreas Gohr */ 216*c124fc53SAndreas Gohr public function testInterwikiLinksArePrefixedWithIcon(): void 217*c124fc53SAndreas Gohr { 218*c124fc53SAndreas Gohr $html = $this->getDebugHTML('renderer_features'); 219*c124fc53SAndreas Gohr $dom = (new Document())->html($html); 220*c124fc53SAndreas Gohr 221*c124fc53SAndreas Gohr $link = $dom->find('a.interwiki')->first(); 222*c124fc53SAndreas Gohr $this->assertNotNull($link, 'Interwiki link missing'); 223*c124fc53SAndreas Gohr 224*c124fc53SAndreas Gohr $icon = $link->children()->first(); 225*c124fc53SAndreas Gohr $this->assertNotNull($icon, 'Interwiki icon missing'); 226*c124fc53SAndreas Gohr $this->assertSame('img', strtolower($icon->nodeName)); 227*c124fc53SAndreas Gohr $this->assertStringContainsString('iw_doku', $icon->attr('class')); 228*c124fc53SAndreas Gohr } 229*c124fc53SAndreas Gohr 230*c124fc53SAndreas Gohr /** 231*c124fc53SAndreas Gohr * Render a single page through the dw2pdf renderer without writer post-processing. 232*c124fc53SAndreas Gohr * 233*c124fc53SAndreas Gohr * @param string $pageId 234*c124fc53SAndreas Gohr * @param array $conf 235*c124fc53SAndreas Gohr * @param string[] $additionalPages 236*c124fc53SAndreas Gohr * @return string 237*c124fc53SAndreas Gohr */ 238*c124fc53SAndreas Gohr protected function getRawRendererHtml(string $pageId, array $conf = [], array $additionalPages = []): string 239*c124fc53SAndreas Gohr { 240*c124fc53SAndreas Gohr $this->prepareFixturePage($pageId); 241*c124fc53SAndreas Gohr foreach ($additionalPages as $related) { 242*c124fc53SAndreas Gohr $this->prepareFixturePage($related); 243*c124fc53SAndreas Gohr } 244*c124fc53SAndreas Gohr 245*c124fc53SAndreas Gohr /** @var \renderer_plugin_dw2pdf $renderer */ 246*c124fc53SAndreas Gohr $renderer = plugin_load('renderer', 'dw2pdf', true); 247*c124fc53SAndreas Gohr $renderer->setConfig(new Config($conf)); 248*c124fc53SAndreas Gohr 249*c124fc53SAndreas Gohr global $ID; 250*c124fc53SAndreas Gohr $keep = $ID; 251*c124fc53SAndreas Gohr $ID = $pageId; 252*c124fc53SAndreas Gohr 253*c124fc53SAndreas Gohr $file = wikiFN($pageId); 254*c124fc53SAndreas Gohr $instructions = p_get_instructions(io_readWikiPage($file, $pageId)); 255*c124fc53SAndreas Gohr $info = []; 256*c124fc53SAndreas Gohr $html = p_render('dw2pdf', $instructions, $info); 257*c124fc53SAndreas Gohr 258*c124fc53SAndreas Gohr $ID = $keep; 259*c124fc53SAndreas Gohr 260*c124fc53SAndreas Gohr return $html; 261*c124fc53SAndreas Gohr } 262*c124fc53SAndreas Gohr 263*c124fc53SAndreas Gohr /** 264*c124fc53SAndreas Gohr * Persist the given fixture page into the wiki so that it can be rendered. 265*c124fc53SAndreas Gohr * 266*c124fc53SAndreas Gohr * @param string $pageId 267*c124fc53SAndreas Gohr * @return void 268*c124fc53SAndreas Gohr */ 269*c124fc53SAndreas Gohr protected function prepareFixturePage(string $pageId): void 270*c124fc53SAndreas Gohr { 271*c124fc53SAndreas Gohr $data = file_get_contents(__DIR__ . '/pages/' . $pageId . '.txt'); 272*c124fc53SAndreas Gohr saveWikiText($pageId, $data, 'dw2pdf renderer test'); 273*c124fc53SAndreas Gohr } 274*c124fc53SAndreas Gohr 275*c124fc53SAndreas Gohr /** 276*c124fc53SAndreas Gohr * Locate the first hyperlink whose trimmed text matches the expected label. 277*c124fc53SAndreas Gohr * 278*c124fc53SAndreas Gohr * @param Document $dom 279*c124fc53SAndreas Gohr * @param string $text 280*c124fc53SAndreas Gohr * @return Element|null 281*c124fc53SAndreas Gohr */ 282*c124fc53SAndreas Gohr protected function findLinkByText(Document $dom, string $text): ?Element 283*c124fc53SAndreas Gohr { 284*c124fc53SAndreas Gohr foreach ($dom->find('a') as $anchor) { 285*c124fc53SAndreas Gohr if (trim($anchor->text()) === $text) { 286*c124fc53SAndreas Gohr return $anchor; 287*c124fc53SAndreas Gohr } 288*c124fc53SAndreas Gohr } 289*c124fc53SAndreas Gohr 290*c124fc53SAndreas Gohr return null; 291*c124fc53SAndreas Gohr } 292a9e9f334SAndreas Gohr} 293