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