xref: /plugin/dw2pdf/_test/EndToEndTest.php (revision c124fc539f0c5c773f84bc64bb897525f386e8f8)
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