diff --git a/.gitignore b/.gitignore
index c6c7e22a3f38..2a4490336337 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,4 @@ local_version.txt
storage/migrations
nbproject
-.php_cs.cache
\ No newline at end of file
+.php_cs.cache
diff --git a/tests/Feature/PdfMaker/Business.php b/tests/Feature/PdfMaker/Business.php
new file mode 100644
index 000000000000..6429bb9468f1
--- /dev/null
+++ b/tests/Feature/PdfMaker/Business.php
@@ -0,0 +1,13 @@
+data = $data;
+ }
+
+ public function design(string $design)
+ {
+ $this->design = new $design();
+
+ $this->initializeDomDocument();
+
+ return $this;
+ }
+
+ public function build()
+ {
+ if (isset($this->data['template'])) {
+ $this->updateElementProperties($this->data['template']);
+ }
+
+ if (isset($this->data['variables'])) {
+ $this->updateVariables($this->data['variables']);
+ }
+
+ return $this;
+ }
+
+ public function getCompiledHTML()
+ {
+ return $this->document->saveHTML();
+ }
+}
diff --git a/tests/Feature/PdfMaker/PdfMakerTest.php b/tests/Feature/PdfMaker/PdfMakerTest.php
new file mode 100644
index 000000000000..006a37f45ccb
--- /dev/null
+++ b/tests/Feature/PdfMaker/PdfMakerTest.php
@@ -0,0 +1,311 @@
+ [],
+ 'variables' => [],
+ ];
+
+ public function testDesignLoadsCorrectly()
+ {
+ $maker = new PdfMaker($this->state);
+
+ $maker->design(Business::class);
+
+ $this->assertInstanceOf(Business::class, $maker->design);
+ }
+
+ public function testHtmlDesignLoadsCorrectly()
+ {
+ $maker = new PdfMaker($this->state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $this->assertStringContainsString('', $maker->getCompiledHTML());
+ }
+
+ public function testGetSectionUtility()
+ {
+ $maker = new PdfMaker($this->state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $this->assertEquals('table', $maker->getSectionNode('product-table')->nodeName);
+ }
+
+ public function testTableAttributesAreInjected()
+ {
+ $state = [
+ 'template' => [
+ 'product-table' => [
+ 'id' => 'product-table',
+ 'properties' => [
+ 'class' => 'my-awesome-class',
+ 'style' => 'margin-top: 10px;',
+ 'script' => 'console.log(1)',
+ ],
+ ],
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => [
+ 'class' => 'header-class',
+ ],
+ ],
+ ],
+ 'variables' => [],
+ ];
+
+ $maker = new PdfMaker($state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $this->assertStringContainsString('my-awesome-class', $maker->getSection('product-table', 'class'));
+ $this->assertStringContainsString('margin-top: 10px;', $maker->getSection('product-table', 'style'));
+ $this->assertStringContainsString('console.log(1)', $maker->getSection('product-table', 'script'));
+ }
+
+ public function testVariablesAreReplaced()
+ {
+ $state = [
+ 'template' => [
+ 'product-table' => [
+ 'id' => 'product-table',
+ 'properties' => [
+ 'class' => 'my-awesome-class',
+ 'style' => 'margin-top: 10px;',
+ 'script' => 'console.log(1)',
+ ],
+ ],
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => [
+ 'class' => 'header-class',
+ ],
+ ],
+ ],
+ 'variables' => [
+ '$title' => 'Invoice Ninja',
+ ],
+ ];
+
+ $maker = new PdfMaker($state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $this->assertStringContainsString('Invoice Ninja', $maker->getCompiledHTML());
+ $this->assertStringContainsString('Invoice Ninja', $maker->getSection('header'));
+ }
+
+ public function testElementContentIsGenerated()
+ {
+ $state = [
+ 'template' => [
+ 'product-table' => [
+ 'id' => 'product-table',
+ 'properties' => [],
+ 'elements' => [
+ ['element' => 'thead', 'content' => '', 'elements' => [
+ ['element' => 'th', 'content' => 'Company',],
+ ['element' => 'th', 'content' => 'Contact'],
+ ['element' => 'th', 'content' => 'Country', 'properties' => [
+ 'colspan' => 3,
+ ]],
+ ]],
+ ['element' => 'tr', 'content' => '', 'elements' => [
+ ['element' => 'td', 'content' => '$company'],
+ ['element' => 'td', 'content' => '$email'],
+ ['element' => 'td', 'content' => '$country', 'elements' => [
+ ['element' => 'a', 'content' => 'Click here for a link', 'properties' => [
+ 'href' => 'https://github.com/invoiceninja/invoiceninja',
+ ]],
+ ]],
+ ]],
+ ],
+ ],
+ ],
+ 'variables' => [
+ '$company' => 'Invoice Ninja',
+ '$email' => 'contact@invoiceninja.com',
+ '$country' => 'UK',
+ ],
+ ];
+
+ $maker = new PdfMaker($state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $compiled = 'contact@invoiceninja.com';
+
+ $this->assertStringContainsString($compiled, $maker->getCompiledHTML());
+ }
+
+ public function testConditionalRenderingOfElements()
+ {
+ $maker1 = new PdfMaker([
+ 'template' => [
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => [],
+ ],
+ ],
+ ]);
+
+ $maker1
+ ->design(Business::class)
+ ->build();
+
+ $output1 = $maker1->getCompiledHTML();
+
+ $this->assertStringContainsString('
', $output1);
+
+ $maker2 = new PdfMaker([
+ 'template' => [
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => ['hidden' => "true"],
+ ],
+ ],
+ ]);
+
+ $maker2
+ ->design(Business::class)
+ ->build();
+
+ $output2 = $maker2->getCompiledHTML();
+
+ $this->assertStringContainsString('', $output2);
+
+ $this->assertNotSame($output1, $output2);
+ }
+
+ public function testOrderingElements()
+ {
+ $maker = new PdfMaker([
+ 'template' => [
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => [],
+ 'elements' => [
+ ['element' => 'h1', 'content' => 'h1-element'],
+ ['element' => 'span', 'content' => 'span-element'],
+ ]
+ ],
+ ],
+ ]);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $node = $maker->getSectionNode('header');
+
+ $before = [];
+
+ foreach ($node->childNodes as $child) {
+ $before[] = $child->nodeName;
+ }
+
+ $this->assertEquals('h1', $before[1]);
+
+ $maker = new PdfMaker([
+ 'template' => [
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => [],
+ 'elements' => [
+ ['element' => 'h1', 'content' => 'h1-element', 'order' => 1],
+ ['element' => 'span', 'content' => 'span-element', 'order' => 0],
+ ]
+ ],
+ ],
+ ]);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ $node = $maker->getSectionNode('header');
+
+ $after = [];
+
+ foreach ($node->childNodes as $child) {
+ $after[] = $child->nodeName;
+ }
+
+ $this->assertEquals('span', $after[1]);
+ }
+
+ public function testGeneratingPdf()
+ {
+ $state = [
+ 'template' => [
+ 'header' => [
+ 'id' => 'header',
+ 'properties' => ['class' => 'text-white bg-blue-600 p-2'],
+ ],
+ 'product-table' => [
+ 'id' => 'product-table',
+ 'properties' => ['class' => 'table-auto'],
+ 'elements' => [
+ ['element' => 'thead', 'content' => '', 'elements' => [
+ ['element' => 'tr', 'content' => '', 'elements' => [
+ ['element' => 'th', 'content' => 'Title', 'properties' => ['class' => 'px-4 py-2']],
+ ['element' => 'th', 'content' => 'Author', 'properties' => ['class' => 'px-4 py-2']],
+ ['element' => 'th', 'content' => 'Views', 'properties' => ['class' => 'px-4 py-2']],
+ ]]
+ ]],
+ ['element' => 'tbody', 'content' => '', 'elements' => [
+ ['element' => 'tr', 'content' => '', 'elements' => [
+ ['element' => 'td', 'content' => 'An amazing guy', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => 'David Bomba', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => '1M', 'properties' => ['class' => 'border px-4 py-2']],
+ ]],
+ ['element' => 'tr', 'content' => '', 'elements' => [
+ ['element' => 'td', 'content' => 'Flutter master', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => 'Hillel Coren', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => '1M', 'properties' => ['class' => 'border px-4 py-2']],
+ ]],
+ ['element' => 'tr', 'content' => '', 'elements' => [
+ ['element' => 'td', 'content' => 'Bosssssssss', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => 'Shalom Stark', 'properties' => ['class' => 'border px-4 py-2']],
+ ['element' => 'td', 'content' => '1M', 'properties' => ['class' => 'border px-4 py-2']],
+ ]],
+ ['element' => 'tr', 'content' => '', 'order' => 4, 'elements' => [
+ ['element' => 'td', 'content' => 'Three amazing guys', 'properties' => ['class' => 'border px-4 py-2', 'colspan' => '100%']],
+ ]],
+ ]],
+ ],
+ ]
+ ],
+ 'variables' =>[
+ '$title' => 'Invoice Ninja',
+ ]
+ ];
+
+ $maker = new PdfMaker($state);
+
+ $maker
+ ->design(Business::class)
+ ->build();
+
+ info($maker->getCompiledHTML());
+
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Feature/PdfMaker/PdfMakerUtilities.php b/tests/Feature/PdfMaker/PdfMakerUtilities.php
new file mode 100644
index 000000000000..6672ffa7b584
--- /dev/null
+++ b/tests/Feature/PdfMaker/PdfMakerUtilities.php
@@ -0,0 +1,132 @@
+validateOnParse = true;
+ @$document->loadHTML($this->design->html());
+
+ $this->document = $document;
+ $this->xpath = new DOMXPath($document);
+ }
+
+ public function getSection(string $selector, string $section = null)
+ {
+ $element = $this->document->getElementById($selector);
+
+ if ($section) {
+ return $element->getAttribute($section);
+ }
+
+ return $element->nodeValue;
+ }
+
+ public function getSectionNode(string $selector)
+ {
+ return $this->document->getElementById($selector);
+ }
+
+ public function updateElementProperties(array $elements)
+ {
+ foreach ($elements as $element) {
+ $node = $this->document->getElementById($element['id']);
+
+ if (isset($element['properties'])) {
+ foreach ($element['properties'] as $property => $value) {
+ $this->updateElementProperty($node, $property, $value);
+ }
+ }
+
+ if (isset($element['elements'])) {
+ $sorted = $this->processChildrenOrder($element['elements']);
+
+ $this->createElementContent($node, $sorted);
+ }
+ }
+ }
+
+ public function processChildrenOrder(array $children)
+ {
+ $processed = [];
+
+ foreach($children as $child) {
+ if (!isset($child['order'])) {
+ $child['order'] = 0;
+ }
+
+ $processed[] = $child;
+ }
+
+ usort($processed, function ($a, $b) {
+ return $a['order'] <=> $b['order'];
+ });
+
+ return $processed;
+ }
+
+ public function updateElementProperty($element, string $attribute, string $value)
+ {
+ $element->setAttribute($attribute, $value);
+
+ if ($element->getAttribute($attribute) === $value) {
+ return $element;
+ }
+
+ return $element;
+ }
+
+ public function createElementContent($element, $children)
+ {
+ foreach ($children as $child) {
+
+ $_child = $this->document->createElement($child['element'], $child['content']);
+ $element->appendChild($_child);
+
+ if (isset($child['properties'])) {
+ foreach ($child['properties'] as $property => $value) {
+ $this->updateElementProperty($_child, $property, $value);
+ }
+ }
+
+ if (isset($child['elements'])) {
+ $sorted = $this->processChildrenOrder($child['elements']);
+
+ $this->createElementContent($_child, $sorted);
+ }
+ }
+ }
+
+ public function updateVariables(array $variables)
+ {
+ $html = strtr($this->getCompiledHTML(), $variables);
+
+ $this->document->loadHTML($html);
+
+ $this->document->saveHTML();
+ }
+
+ public function updateVariable(string $element, string $variable, string $value)
+ {
+ $element = $this->document->getElementById($element);
+
+ $original = $element->nodeValue;
+
+ $element->nodeValue = '';
+
+ $replaced = strtr($original, [$variable => $value]);
+
+ $element->appendChild(
+ $this->document->createTextNode($replaced)
+ );
+
+ return $element;
+ }
+}
diff --git a/tests/Feature/PdfMaker/business.html b/tests/Feature/PdfMaker/business.html
new file mode 100644
index 000000000000..b2c0c81a2b06
--- /dev/null
+++ b/tests/Feature/PdfMaker/business.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file