diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e36135e..e50f4ec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,51 +1,52 @@ -name: Tests - -env: - php_version: '7.3' +name: "PHPUnit" on: push: branches: [ master ] pull_request: branches: [ master ] - workflow_dispatch: - inputs: - name: - description: 'PHP version' - required: true - default: '7.3' jobs: - build: - - runs-on: ubuntu-latest + phpunit: + name: "PHPUnit" + env: + LC_ALL: en_US.UTF-8 + CODE_COVERAGE: n + continue-on-error: ${{ matrix.php-version == '8.1' }} + + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + php-version: + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + dependencies: + - "locked" + operating-system: + - "ubuntu-latest" + composer: + - "composer:v2" steps: - - name: Checkout code - uses: actions/checkout@v2 + - name: "Checkout" + uses: "actions/checkout@v2" - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.3' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached - tools: composer:v2 - coverage: none - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v2 + - name: "Install PHP" + uses: shivammathur/setup-php@2.11.0 with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + php-version: "${{ matrix.php-version }}" + extensions: mbstring, xml, ctype, iconv + tools: ${{matrix.composer}} - - name: Install dependencies - run: composer install --prefer-dist --no-progress + - name: "Install dependencies" + run: | + composer update --no-interaction --no-progress - # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" - # Docs: https://getcomposer.org/doc/articles/scripts.md + - name: "Unit Tests" + run: composer test-unit - # - name: Run test suite - # run: composer test + - name: "Integration Tests" + run: composer test-int diff --git a/.gitignore b/.gitignore index 1e7fe90..26723eb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /composer.lock /php_errors.log /test*.php +/coverage.clover +/.php-cs-fixer.cache diff --git a/composer.json b/composer.json index e558a22..63c85aa 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,11 @@ "role": "Developer" } ], + "archive": { + "exclude": [ + "/tests", "/.github", "/phpunit.xml" + ] + }, "autoload": { "psr-4": { "Ht7\\Html\\": "src/" @@ -34,19 +39,16 @@ } ], "require": { - "php": "^7.0", - "ht7/ht7-base": "master" + "php": "^7.0|^8.0", + "ht7/ht7-base": "dev-develop" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^10.5" }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { - "test": ".\\vendor\\bin\\phpunit --configuration .\\tests\\configuration.xml --colors --testdox", - "test-unit": "php .\\vendor\\phpunit\\phpunit\\phpunit --colors --bootstrap .\\tests\\bootstrap.php --configuration .\\tests\\configuration.xml --testsuite \"ht7 html - unit\"", - "test-func": "php .\\vendor\\phpunit\\phpunit\\phpunit --colors --bootstrap .\\tests\\bootstrap.php --configuration .\\tests\\configuration.xml --testsuite \"ht7 html - functional\"", - "test-unit1": "C:\\dev\\PHP\\current\\php.exe \"C:\\data\\edv\\php\\misc\\ht7-html\\vendor\\phpunit\\phpunit\\phpunit\" \"--colors\" \"--log-junit\" \"C:\\Users\\zoom4u\\AppData\\Local\\Temp\\nb-phpunit-log.xml\" \"--bootstrap\" \"C:\\data\\edv\\php\\misc\\ht7-html\\tests\\bootstrap.php\" \"--configuration\" \"C:\\data\\edv\\php\\misc\\ht7-html\\tests\\configuration.xml\" \"--coverage-clover\" \"C:\\Users\\zoom4u\\AppData\\Local\\Temp\\nb-phpunit-coverage.xml\" \"C:\\Program Files\\NetBeans-11.2\\netbeans\\php\\phpunit\\NetBeansSuite.php", - "phpv": "php -v" + "test-unit": "php vendor/phpunit/phpunit/phpunit --colors --testdox --configuration tests/configuration.xml --testsuite unit", + "test-int": "php vendor/phpunit/phpunit/phpunit --colors --testdox --configuration tests/configuration.xml --testsuite integration" } } diff --git a/src/Attribute.php b/src/Attribute.php index af2050b..c54faff 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -1,47 +1,18 @@ setName($name); - $this->setValue($value); } - /** * Get a string representation of the current class. * @@ -54,7 +25,6 @@ public function __toString() return $this->getName() . ($value === '' ? '' : '="' . $value . '"'); } - /** * @Overridden */ @@ -62,76 +32,51 @@ public function getHash() { return $this->getName(); } - /** * Get the name of the present attribute. - * - * @return string The attribute name. */ - public function getName() + public function getName(): string { return $this->name; } - /** * Get the value of the present attribute. - * - * @return mixed The attribute value. */ - public function getValue() + public function getValue(): string|float|int|bool { return $this->value; } - /** * {@inheritdoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->getValue(); } - /** * Set the name of the current attribute instance.
- * The name must be a string and must not be empty. - * - * @param string $name The attribute name. - * @throws InvalidArgumentException + * The name must not be empty. */ - public function setName($name) + public function setName(string $name): static { if (empty($name)) { - $msg = 'The attribute name must not be empty.'; - $e = sprintf($msg, gettype($name)); + $e = 'The attribute name must not be empty.'; - throw new InvalidArgumentException($e); - } elseif (is_string($name)) { - $this->name = $name; - } else { - $msg = 'The attribute name must be a string, found %s.'; - $e = sprintf($msg, gettype($name)); - - throw new InvalidArgumentException($e); + throw new \InvalidArgumentException($e); } - } + + $this->name = $name; + return $this; + } /** * Set the value of the current attribute instance.
- * The value must be either string, int or float. - * - * @param mixed $value The attribute value. - * @throws InvalidArgumentException + * The value must be either string, float, int or bool. */ - public function setValue($value) + public function setValue(string|float|int|bool $value): static { - if (is_string($value) || is_int($value) || is_float($value)) { - $this->value = $value; - } else { - $msg = 'The attribute value must be a string, int or float, found %s.'; - $e = sprintf($msg, gettype($value)); + $this->value = $value; - throw new InvalidArgumentException($e); - } + return $this; } - } diff --git a/src/Lists/AbstractRenderableList.php b/src/Lists/AbstractRenderableList.php index 1f4fcdc..01edabe 100644 --- a/src/Lists/AbstractRenderableList.php +++ b/src/Lists/AbstractRenderableList.php @@ -2,9 +2,9 @@ namespace Ht7\Html\Lists; -use \Ht7\Base\Lists\ItemList; -use \Ht7\Html\Utilities\CanRenderList; -use \Ht7\Html\Renderable; +use Ht7\Base\Lists\ItemList; +use Ht7\Html\Utilities\CanRenderList; +use Ht7\Html\Renderable; /** * Description of AbstractItemList diff --git a/src/Lists/AttributeList.php b/src/Lists/AttributeList.php index ba67720..14e8f0c 100644 --- a/src/Lists/AttributeList.php +++ b/src/Lists/AttributeList.php @@ -17,14 +17,12 @@ class AttributeList extends HashList implements \JsonSerializable, Renderable { use CanRenderList; - public function __construct(array $data = []) { $this->divider = ' '; parent::__construct($data); } - /** * {@inheritdoc} */ @@ -35,7 +33,6 @@ public function __toString() return implode($this->getDivider(), $all); } - /** * Add an attribute to the present AttributeList. * @@ -56,7 +53,6 @@ public function add($item) throw new InvalidArgumentException($e); } } - /** * Add an attribute name and its value to the present AttributeList. * @@ -68,7 +64,6 @@ public function addPlain($name, $value) { return $this->add((new Attribute($name, $value))); } - /** * Check wheter a value can be found or not in the present AttributeList instance. * @@ -91,15 +86,13 @@ public function hasByValue($compare) return false; } - /** * {@inheritdoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->getAll(); } - /** * Load the present AttributeList instance with the submitted data. * @@ -120,5 +113,4 @@ public function load(array $data) } } } - } diff --git a/src/Lists/NodeList.php b/src/Lists/NodeList.php index 541f8c6..90d6a88 100644 --- a/src/Lists/NodeList.php +++ b/src/Lists/NodeList.php @@ -11,7 +11,6 @@ */ class NodeList extends AbstractRenderableList implements \JsonSerializable { - /** * {@inheritdoc} */ @@ -21,7 +20,6 @@ public function add($item) return $this; } - /** * Serialize the object to a value that can beserialized natively by json_encode. * @@ -32,7 +30,7 @@ public function add($item) * * @return array An array representation of this instance. */ - public function jsonSerialize() + public function jsonSerialize(): mixed { // $items = []; // $all = $this->getAll(); @@ -44,5 +42,4 @@ public function jsonSerialize() // return $items; return $this->getAll(); } - } diff --git a/src/Node.php b/src/Node.php index 67b4e48..941e9c7 100644 --- a/src/Node.php +++ b/src/Node.php @@ -2,7 +2,8 @@ namespace Ht7\Html; -use \Ht7\Html\Renderable; +use Ht7\Html\Renderable; +use Ht7\Html\Lists\NodeList; /** * Base class. @@ -12,33 +13,25 @@ abstract class Node implements \JsonSerializable, Renderable { - /** - * The content of the current Node. - * - * @var mixed The content of the current Node, which can be a - * string, Text- or a Tag-instance. - */ - protected $content; + protected NodeList|string $content; /** - * Get the content of the current HTML element. - * - * @return NodeList The content of the current HTML element. + * Get the content of the present HTML element. */ - public function getContent() + public function getContent(): NodeList|string { return $this->content; } /** - * Set the inner content of the current tag. + * Set the inner content of the present HTML element. * - * This method will throw an exception if the current tag is self closing. + * This method will throw an exception if the present tag is self closing. * - * @param mixed $content The content of the current Node + * @param NodeList|array|string|float|int|bool $content The content of the current Node * instance. * @throws BadMethodCallException * @throws InvalidArgumentException */ - abstract public function setContent($content); + abstract public function setContent(NodeList|array|string|float|int|bool $content): static; } diff --git a/src/Tag.php b/src/Tag.php index c37770e..ac51637 100644 --- a/src/Tag.php +++ b/src/Tag.php @@ -1,21 +1,11 @@ setTagName($tagName); - $this->setContent($content); - $this->setAttributes($attributes); + $this->setContent($content) + ->setAttributes($attributes); } - /** * Get a string representation of the current tag instance. * @@ -77,37 +47,27 @@ public function __toString() $attrStr = (string) $this->getAttributes(); $attrStrSanitized = empty($attrStr) ? '' : ' ' . $attrStr; - if ($this->isSelfClosing()) { - $html = '<' . $tagName . $attrStrSanitized . ' />'; - } else { - $html = '<' . $tagName . $attrStrSanitized . '>'; - $html .= $this->getContent(); - $html .= ''; - } - - return $html; + return "<{$tagName}{$attrStrSanitized}" + . ($this->isSelfClosing() ? ' />' : ">{$this->getContent()}"); } - /** * Get the defined attributes of the current tag instance. * * @return AttributeList The attributes of the present tag. */ - public function getAttributes() + public function getAttributes(): AttributeList { return $this->attributes; } - /** * Get the content of the current HTML element. * * @return NodeList The content of the current HTML element. */ - public function getContent() + public function getContent(): NodeList { return parent::getContent(); } - /** * Get an iterator instance to iterate the current Tag. * @@ -115,21 +75,19 @@ public function getContent() * * @return Iterator */ - public function getIterator() + public function getIterator(): \Traversable { return $this->getIteratorPreOrder(); } - /** * Get the tag name of the current element. * * @return string The tag name. */ - public function getTagName() + public function getTagName(): string { return $this->tagName; } - /** * Get a tree iterator which goes first every tree up before searching the * next. @@ -138,32 +96,25 @@ public function getTagName() // { // // } - /** * Get a tree iterator which searches first every sibling before going up to * the next level. - * - * @return PreOrderIterator */ - public function getIteratorPreOrder() + public function getIteratorPreOrder(): PreOrderIterator { return new PreOrderIterator($this); } - /** * Whetever the current tag is self closing or not. - * - * @return boolean True if the current element is self closing. */ - public function isSelfClosing() + public function isSelfClosing(): bool { return SelfClosing::is($this->getTagName()); } - /** * {@inheritdoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return [ 'attributes' => $this->getAttributes(), @@ -171,31 +122,20 @@ public function jsonSerialize() 'tag' => $this->getTagName() ]; } - /** * Set the attributes of the current HTML element. * - * @param mixed $attributes Indexed array of + * @param AttributeList|array $attributes Indexed array of * \Ht7\Html\Attribute * instances or an instance of * AttributeList. */ - public function setAttributes($attributes) + public function setAttributes(AttributeList|array $attributes): static { - if ($attributes instanceof AttributeList) { - $this->attributes = $attributes; - } elseif (is_array($attributes)) { - $this->attributes = new AttributeList($attributes); - } else { - throw new InvalidDatatypeException( - 'attributes', - $attributes, - ['array'], - [NodeList::class] - ); - } - } + $this->attributes = $attributes instanceof AttributeList ? $attributes : new AttributeList($attributes); + return $this; + } /** * Set the inner content of the current tag. * @@ -205,20 +145,20 @@ public function setAttributes($attributes) * will be created. In this case the input validation will be delegated to * the NodeList. * - * @param mixed $content The content of the current Tag + * @param NodeList|array|string|float|int|bool $content The content of the current Tag * instance. This must be a NodeList * instance or an array. - * @throws BadMethodCallException - * @throws InvalidArgumentException + * @throws \BadMethodCallException */ - public function setContent($content) + // public function setContent(mixed $content): static + public function setContent(NodeList|array|string|float|int|bool $content): static { - if (!empty($content) && $this->isSelfClosing()) { + if ($this->isSelfClosing() && !empty($content)) { $msg = 'This tag (%s) can not have content, because it is self' - . ' closing.'; + . ' closing.'; $e = sprintf($msg, gettype($this->getTagName())); - throw new BadMethodCallException($e); + throw new \BadMethodCallException($e); } if (is_scalar($content) || $content instanceof Node) { @@ -226,21 +166,18 @@ public function setContent($content) } $this->content = $content instanceof NodeList ? $content : new NodeList($content); - } + return $this; + } /** * Set the name of the current tag. * * @param string $name The tag name of the current HTML element. - * @throws InvalidArgumentException */ - public function setTagName($name) + public function setTagName(string $name): static { - if (is_string($name)) { - $this->tagName = $name; - } else { - throw new InvalidDatatypeException('tag name', $name, ['string']); - } - } + $this->tagName = $name; + return $this; + } } diff --git a/src/Text.php b/src/Text.php index 77eff95..2cdba37 100644 --- a/src/Text.php +++ b/src/Text.php @@ -2,22 +2,22 @@ namespace Ht7\Html; +use Ht7\Html\Lists\NodeList; + /** * This is a simple text node. */ class Text extends Node { - /** * Create an instance of the text element. * * @param string $text The content. */ - public function __construct($text) + public function __construct(string $text) { $this->setContent($text); } - /** * Get a string representation of the current class. * @@ -27,34 +27,31 @@ public function __toString() { return $this->getContent(); } - /** * Get the content. * * @return string The content of the current text element. */ - public function getContent() + public function getContent(): NodeList|string { return parent::getContent(); } - /** * {@inheritdoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->getContent(); } - /** * Set the content. * * Only scalar types will be accepted. * - * @param mixed $text The content as string, integer or float. + * @param NodeList|array|string|float|int|bool $text The content as string, integer, float or bool. * @throws \InvalidArgumentException */ - public function setContent($text) + public function setContent(NodeList|array|string|float|int|bool $text): static { if (is_scalar($text)) { $this->content = (string) $text; @@ -63,6 +60,7 @@ public function setContent($text) throw new \InvalidArgumentException($e); } - } + return $this; + } } diff --git a/src/Utilities/CanRenderList.php b/src/Utilities/CanRenderList.php index bc8a89f..26ff130 100644 --- a/src/Utilities/CanRenderList.php +++ b/src/Utilities/CanRenderList.php @@ -10,14 +10,12 @@ */ trait CanRenderList { - - protected $divider; + protected $divider = ''; public function getDivider() { return $this->divider; } - public function setDivider($divider) { if (is_string($divider)) { @@ -26,10 +24,8 @@ public function setDivider($divider) throw new InvalidDatatypeException('divider', $divider, ['string']); } } - public function __toString() { return implode($this->getDivider(), $this->getAll()); } - } diff --git a/tests/Integration/CallbackTest.php b/tests/Integration/CallbackTest.php index 704c0f1..98cdd1c 100644 --- a/tests/Integration/CallbackTest.php +++ b/tests/Integration/CallbackTest.php @@ -55,6 +55,8 @@ public function testWithMethod() { if (file_exists('./assets/functions/callbacks.php')) { include_once './assets/functions/callbacks.php'; + } elseif (file_exists('./tests/assets/functions/callbacks.php')) { + include_once './tests/assets/functions/callbacks.php'; } else { throw new \BadMethodCallException('Missing callback functions file.'); } diff --git a/tests/Unit/AttributeTest.php b/tests/Unit/AttributeTest.php index d815897..a4bfff2 100644 --- a/tests/Unit/AttributeTest.php +++ b/tests/Unit/AttributeTest.php @@ -4,168 +4,118 @@ use \InvalidArgumentException; use \stdClass; -use \PHPUnit\Framework\TestCase; -use \Ht7\Html\Attribute; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\MockObject\MockObject; +use Ht7\Html\Attribute; class AttributeTest extends TestCase { + private string $className = Attribute::class; - public function testConstructor() + #[Test] + #[TestDox('Get name.')] + public function getName(): void { - // see: http://miljar.github.io/blog/2013/12/20/phpunit-testing-the-constructor/ - $className = Attribute::class; - $name = 'class'; - $value = 'btn btn-primary'; - - $mock = $this->getMockBuilder($className) - ->setMethods(['setName', 'setValue']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->expects($this->once()) - ->method('setName') - ->with($this->equalTo($name)); - $mock->expects($this->once()) - ->method('setValue') - ->with($this->equalTo($value)); - - $reflectedClass = new \ReflectionClass($className); - $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($mock, $name, $value); - } - - public function testGetName() - { - $className = Attribute::class; - - $mock = $this->getMockBuilder($className) - ->setMethods(['setName']) - ->disableOriginalConstructor() - ->getMock(); + $expected = 'class'; + /** @var Attribute $sut */ + $sut = $this->getSut(['setName']); - $reflectedClass = new \ReflectionClass($className); + $reflectedClass = new \ReflectionClass($this->className); $property = $reflectedClass->getProperty('name'); $property->setAccessible(true); + $property->setValue($sut, $expected); - $expected = 'class'; - - $property->setValue($mock, $expected); - - $this->assertEquals($expected, $mock->getName()); + $this->assertEquals($expected, $sut->getName()); } - public function testGetValue() + #[Test] + #[TestDox('Get value.')] + public function getValue(): void { - $className = Attribute::class; - - $mock = $this->getMockBuilder($className) - ->setMethods(['setValue']) - ->disableOriginalConstructor() - ->getMock(); + $expected = 'btn btn-primary'; + /** @var Attribute $sut */ + $sut = $this->getSut(['setName']); - $reflectedClass = new \ReflectionClass($className); + $reflectedClass = new \ReflectionClass($this->className); $property = $reflectedClass->getProperty('value'); $property->setAccessible(true); + $property->setValue($sut, $expected); - $expected = 'btn btn-primary'; - - $property->setValue($mock, $expected); - - $this->assertEquals($expected, $mock->getValue()); + $this->assertEquals($expected, $sut->getValue()); } - public function testJsonEncode() + #[Test] + #[TestDox('Json encode.')] + public function jsonEncode(): void { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['getValue']) - ->disableOriginalConstructor() - ->getMock(); + $expected = '"btn btn-primary"'; + $sut = $this->getSut(['getValue']); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getValue') ->willReturn('btn btn-primary'); - $expected = '"btn btn-primary"'; - - $this->assertEquals($expected, json_encode($mock)); - } - - public function testSetNameWithException() - { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['setValue']) // Without this, an exception would not been thrown. - ->disableOriginalConstructor() - ->getMock(); - - $this->expectException(InvalidArgumentException::class); - - $mock->setName((new stdClass())); + $this->assertEquals($expected, json_encode($sut)); } - public function testSetNameEmptyWithException() + #[Test] + #[TestDox('Set name with exception.')] + public function setNameWithException(): void { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['setValue']) // Without this, an exception would not been thrown. - ->disableOriginalConstructor() - ->getMock(); + /** @var Attribute $sut */ + $sut = $this->getSut(['setValue']); - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); - $mock->setName(''); + $sut->setName(''); } - public function testSetValueWithException() + #[Test] + #[TestDox('Render the attribute.')] + public function render(): void { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['setName']) // Without this, an exception would not been thrown. - ->disableOriginalConstructor() - ->getMock(); - - $this->expectException(InvalidArgumentException::class); - - $mock->setValue((new stdClass())); - } - - public function testToString() - { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['getName', 'getValue']) - ->disableOriginalConstructor() - ->getMock(); + $expected = 'class="btn btn-primary"'; + $sut = $this->getSut(['getName', 'getValue']); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getName') ->willReturn('class'); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getValue') ->willReturn('btn btn-primary'); - - $actual = (string) $mock; - $expected = 'class="btn btn-primary"'; + $actual = (string) $sut; $this->assertEquals($expected, $actual); } - public function testToStringNoValue() + #[Test] + #[TestDox('Render the attribute with no value.')] + public function renderNoValue(): void { - $mock = $this->getMockBuilder(Attribute::class) - ->setMethods(['getName', 'getValue']) - ->disableOriginalConstructor() - ->getMock(); + $expected = 'required'; + $sut = $this->getSut(['getName', 'getValue']); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getName') ->willReturn('required'); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getValue') ->willReturn(''); - - $actual = (string) $mock; - $expected = 'required'; + $actual = (string) $sut; $this->assertEquals($expected, $actual); } + private function getSut(array $methods): MockObject + { + return $this->getMockBuilder($this->className) + ->onlyMethods($methods) + ->disableOriginalConstructor() + ->getMock(); + } + } diff --git a/tests/Unit/CallbackTest.php b/tests/Unit/CallbackTest.php index d9d4694..f920e78 100644 --- a/tests/Unit/CallbackTest.php +++ b/tests/Unit/CallbackTest.php @@ -284,8 +284,12 @@ public function testProcessWithMethod() { if (file_exists('./assets/functions/callbacks.php')) { include_once './assets/functions/callbacks.php'; + } elseif (file_exists('./../assets/functions/callbacks.php')) { + include_once './../assets/functions/callbacks.php'; + } elseif (file_exists('./tests/assets/functions/callbacks.php')) { + include_once './tests/assets/functions/callbacks.php'; } else { - throw new \BadMethodCallException('Missing callback functions file.'); + throw new \BadMethodCallException('Missing callback functions file. - ' . getcwd()); } $className = Callback::class; diff --git a/tests/Unit/TagTest.php b/tests/Unit/TagTest.php index d1c281a..4b5273a 100644 --- a/tests/Unit/TagTest.php +++ b/tests/Unit/TagTest.php @@ -2,48 +2,49 @@ namespace Ht7\Html\Tests\Unit; -use \BadMethodCallException; -use \InvalidArgumentException; -use \stdClass; -use \PHPUnit\Framework\TestCase; -use \Ht7\Html\Node; -use \Ht7\Html\Tag; -use \Ht7\Html\Iterators\PreOrderIterator; -use \Ht7\Html\Lists\AttributeList; -use \Ht7\Html\Lists\NodeList; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Ht7\Html\Tag; +use Ht7\Html\Text; +use Ht7\Html\Iterators\PreOrderIterator; +use Ht7\Html\Lists\AttributeList; +use Ht7\Html\Lists\NodeList; class TagTest extends TestCase { + private string $className = Tag::class; - public function testConstructor() + #[Test] + #[TestDox('Tag initialisation.')] + public function tagConstructor(): void { // see: http://miljar.github.io/blog/2013/12/20/phpunit-testing-the-constructor/ - $className = Tag::class; $tagName = 'span'; $content = ['test text']; $attributes = ['class' => 'btn btn-primary']; - $mock = $this->getMockBuilder($className) - ->setMethods(['setTagName', 'setContent', 'setAttributes']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->expects($this->once()) - ->method('setTagName') - ->with($this->equalTo($tagName)); - $mock->expects($this->once()) + $sut = $this->getMockTag(['setTagName', 'setContent', 'setAttributes']); + $sut->expects($this->never()) + ->method('setTagName'); + $sut->expects($this->once()) ->method('setContent') - ->with($this->equalTo($content)); - $mock->expects($this->once()) + ->with($this->equalTo($content)) + ->willReturnSelf(); + $sut->expects($this->once()) ->method('setAttributes') - ->with($this->equalTo($attributes)); + ->with($this->equalTo($attributes)) + ->willReturnSelf(); - $reflectedClass = new \ReflectionClass($className); + $reflectedClass = new \ReflectionClass($this->className); $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($mock, $tagName, $content, $attributes); + $constructor->invoke($sut, $tagName, $content, $attributes); } - public function testGetAttributes() + #[Test] + #[TestDox('Get attributes.')] + public function getAttributes(): void { $tag1 = new Tag('div', ['bla']); @@ -54,7 +55,9 @@ public function testGetAttributes() $this->assertInstanceOf(AttributeList::class, $tag2->getAttributes()); } - public function testGetContent() + #[Test] + #[TestDox('Get content.')] + public function getContent(): void { $tag1 = new Tag('div'); @@ -65,7 +68,9 @@ public function testGetContent() $this->assertInstanceOf(NodeList::class, $tag2->getContent()); } - public function testGetIterator() + #[Test] + #[TestDox('Get the default iterator.')] + public function getIterator(): void { $tag1 = new Tag('div'); @@ -76,7 +81,9 @@ public function testGetIterator() $this->assertInstanceOf(PreOrderIterator::class, $tag2->getIterator()); } - public function testGetIteratorPreOrder() + #[Test] + #[TestDox('Get the perorder iterator.')] + public function getIteratorPreOrder(): void { $tag1 = new Tag('div'); @@ -87,7 +94,9 @@ public function testGetIteratorPreOrder() $this->assertInstanceOf(PreOrderIterator::class, $tag2->getIteratorPreOrder()); } - public function testJsonSerialize() + #[Test] + #[TestDox('Json serialize.')] + public function jsonSerialize(): void { $nlMock = $this->createMock(NodeList::class); @@ -101,17 +110,14 @@ public function testJsonSerialize() ->method('jsonSerialize') ->willReturn(['class' => 'btn btn-primary']); - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['getAttributes', 'getContent', 'getTagName']) - ->getMock(); - - $mock->expects($this->once()) + $sut = $this->getMockTag(['getAttributes', 'getContent', 'getTagName']); + $sut->expects($this->once()) ->method('getAttributes') ->willReturn($alMock); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getContent') ->willReturn($nlMock); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getTagName') ->willReturn('span'); @@ -121,164 +127,219 @@ public function testJsonSerialize() 'tag' => 'span', ]; - $this->assertEquals($expected, json_decode(json_encode($mock), JSON_OBJECT_AS_ARRAY)); - } - - public function testSetAttributes() - { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->setAttributes(['class' => 'test']); - - $this->assertInstanceOf(AttributeList::class, $mock->getAttributes()); + $this->assertEquals($expected, json_decode(json_encode($sut), JSON_OBJECT_AS_ARRAY)); } - public function testSetAttributesAttributeList() + #[Test] + #[TestDox('Set attributes.')] + public function setAttributes(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->setAttributes((new AttributeList())); - - $this->assertInstanceOf(AttributeList::class, $mock->getAttributes()); + $attributes = ['class' => 'btn btn-primary']; + $sut = $this->getMockTag([]); + + /** @var Tag $sut */ + $sut->setAttributes($attributes); + + $return = $sut->getAttributes(); + $this->assertInstanceOf(AttributeList::class, $return); + $reflectedClassAttrList = new \ReflectionClass(AttributeList::class); + $itemsProperty = $reflectedClassAttrList->getProperty('items'); + $itemsProperty->setAccessible(true); + $items = $itemsProperty->getValue($return); + $this->assertCount(1, $items); } - public function testSetAttributesEmpty() + #[Test] + #[TestDox('Set attributes with an attribute list.')] + public function setAttributesAttributeList(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); + $attrList = new AttributeList(); + $sut = $this->getMockTag([]); - $mock->setAttributes([]); + /** @var Tag $sut */ + $sut->setAttributes($attrList); - $this->assertInstanceOf(AttributeList::class, $mock->getAttributes()); + $this->assertSame($attrList, $sut->getAttributes()); } - public function testSetAttributesWithException() + #[Test] + #[TestDox('Set attributes with an empty array.')] + public function setAttributesEmpty(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); + $sut = $this->getMockTag([]); - $this->expectException(\InvalidArgumentException::class); + /** @var Tag $sut */ + $sut->setAttributes([]); - $mock->setAttributes((new NodeList())); + $attrList = $sut->getAttributes(); + $this->assertInstanceOf(AttributeList::class, $attrList); + $this->assertEmpty($attrList); } - public function testSetContent() + #[Test] + #[TestDox('Set content.')] + public function setContent(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->setContent(['test text']); - - $this->assertInstanceOf(NodeList::class, $mock->getContent()); - } - - public function testSetContentEmpty() - { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->setContent([]); - - $this->assertInstanceOf(NodeList::class, $mock->getContent()); + $content = ['test text']; + $sut = $this->getMockTag([]); + + $reflectedClass = new \ReflectionClass($this->className); + $tagName = $reflectedClass->getProperty('tagName'); + $tagName->setAccessible(true); + $tagName->setValue($sut, 'div'); + + /** @var Tag $sut */ + $sut->setContent($content); + + $return = $sut->getContent(); + $this->assertInstanceOf(NodeList::class, $return); + $reflectedClassNodeList = new \ReflectionClass(NodeList::class); + $itemsProperty = $reflectedClassNodeList->getProperty('items'); + $itemsProperty->setAccessible(true); + $items = $itemsProperty->getValue($return); + $this->assertCount(1, $items); + $this->assertInstanceOf(Text::class, $items[0]); + $reflectedClassText = new \ReflectionClass(Text::class); + $contentProperty = $reflectedClassText->getProperty('content'); + $contentProperty->setAccessible(true); + $contentFromProperty = $contentProperty->getValue($items[0]); + $this->assertSame($content[0], $contentFromProperty); } - public function testSetTagName() + #[Test] + #[TestDox('Set content with an empty array.')] + public function setContentEmpty(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['isSelfClosing']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->setTagName('test'); - - $this->assertEquals('test', $mock->getTagName()); + $sut = $this->getMockTag([]); + + $reflectedClass = new \ReflectionClass($this->className); + $tagName = $reflectedClass->getProperty('tagName'); + $tagName->setAccessible(true); + $tagName->setValue($sut, 'div'); + + /** @var Tag $sut */ + $sut->setContent([]); + + $content = $sut->getContent(); + $this->assertInstanceOf(NodeList::class, $content); + $reflectedClass = new \ReflectionClass(NodeList::class); + $items = $reflectedClass->getProperty('items'); + $items->setAccessible(true); + $this->assertEmpty($items->getValue($content)); } - public function testSetTagNameWithException() + #[Test] + #[TestDox('Set tag name.')] + public function setTagName(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['isSelfClosing']) - ->disableOriginalConstructor() - ->getMock(); + $tagName = 'test'; + $sut = $this->getMockTag([]); - $this->expectException(\InvalidArgumentException::class); + /** @var Tag $sut */ + $return = $sut->setTagName($tagName); - $mock->setTagName(123); + $this->assertEquals($tagName, $sut->getTagName()); + $this->assertSame($sut, $return); } - public function testSetContentSelfClosing() + #[Test] + #[TestDox('Set content self closing with an exception.')] + public function setContentSelfClosing(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['isSelfClosing']) - ->disableOriginalConstructor() - ->getMock(); - - $mock->expects($this->once()) + $sut = $this->getMockTag(['isSelfClosing']); + $sut->expects($this->once()) ->method('isSelfClosing') ->willReturn(true); + $reflectedClass = new \ReflectionClass($this->className); + $tagName = $reflectedClass->getProperty('tagName'); + $tagName->setAccessible(true); + $tagName->setValue($sut, 'br'); + $this->expectException(\BadMethodCallException::class); - $mock->setContent(['test text']); + /** @var Tag $sut */ + $sut->setContent(['test text']); } - public function testToString() + #[Test] + #[TestDox('Type conversion to string.')] + public function render(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['getAttributes', 'getContent', 'getTagName', 'isSelfClosing']) + $tagName = 'div'; + $attr = 'class="btn btn-primary"'; + $content = 'test text.'; + $attrList = $this->getMockAttributeList($attr); + $nodeList = $this->getMockBuilder(NodeList::class) + ->onlyMethods(['__toString']) ->getMock(); + $nodeList->expects($this->once()) + ->method('__toString') + ->willReturn($content); - $mock->expects($this->once()) + $sut = $this->getMockTag(['getAttributes', 'getContent', 'getTagName', 'isSelfClosing']); + $sut->expects($this->once()) ->method('getTagName') - ->willReturn('div'); - $mock->expects($this->once()) + ->willReturn($tagName); + $sut->expects($this->once()) ->method('getAttributes') - ->willReturn('class="btn btn-primary"'); - $mock->expects($this->once()) + ->willReturn($attrList); + $sut->expects($this->once()) ->method('isSelfClosing') ->willReturn(false); - $mock->expects($this->once()) + $sut->expects($this->once()) ->method('getContent') - ->willReturn('test text.'); + ->willReturn($nodeList); - $expected = '
test text.
'; + $expected = "<{$tagName} {$attr}>{$content}"; - $this->assertEquals($expected, ((string) $mock)); + $this->assertEquals($expected, ((string) $sut)); } - public function testToStringSelfClosing() + #[Test] + #[TestDox('Type conversion to string as self closing.')] + public function toStringSelfClosing(): void { - $mock = $this->getMockBuilder(Tag::class) - ->setMethods(['getAttributes', 'getTagName', 'isSelfClosing']) - ->getMock(); + $tagName = 'br'; + $attr = 'style="display: none;"'; + $attrList = $this->getMockAttributeList($attr); - $mock->expects($this->once()) + $sut = $this->getMockTag(['getAttributes', 'getContent', 'getTagName', 'isSelfClosing']); + $sut->expects($this->once()) ->method('getTagName') - ->willReturn('br'); - $mock->expects($this->once()) + ->willReturn($tagName); + $sut->expects($this->once()) ->method('getAttributes') - ->willReturn('style="display: none;"'); - $mock->expects($this->once()) + ->willReturn($attrList); + $sut->expects($this->once()) ->method('isSelfClosing') ->willReturn(true); + $sut->expects($this->never()) + ->method('getContent'); - $expected2 = '
'; + $expected = "<{$tagName} {$attr} />"; - $this->assertEquals($expected2, ((string) $mock)); + $this->assertEquals($expected, ((string) $sut)); + } + + final private function getMockAttributeList(string $attr = ''): MockObject + { + $attrList = $this->getMockBuilder(AttributeList::class) + ->onlyMethods(['__toString']) + ->getMock(); + $attrList->expects($this->once()) + ->method('__toString') + ->willReturn($attr); + + return $attrList; + } + + final private function getMockTag(array $methods = []): MockObject + { + return $this->getMockBuilder(Tag::class) + ->onlyMethods($methods) + ->disableOriginalConstructor() + ->getMock(); } } diff --git a/tests/Unit/Tags/SelectTest.php b/tests/Unit/Tags/SelectTest.php index 20ad63d..f2620af 100644 --- a/tests/Unit/Tags/SelectTest.php +++ b/tests/Unit/Tags/SelectTest.php @@ -38,43 +38,45 @@ public function testConstructor() $constructor->invokeArgs($mock, [$content, $attr]); } - public function testAdd() - { - $nL = $this->createMock(NodeList::class); - - $nL->expects($this->exactly(3)) - ->method('add') - ->withConsecutive([$this->callback(function(Tag $subject) { - return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 1' && $subject->getAttributes()->get('value')->getValue() === '1'; - })], - [$this->callback(function(Tag $subject) { - return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 2' && $subject->getAttributes()->get('value')->getValue() === '2' && $subject->getAttributes()->has('selected'); - })], - [$this->callback(function(Tag $subject) { - return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 3' && $subject->getAttributes()->get('value')->getValue() === '3' && $subject->getAttributes()->get('data-url')->getValue() === 'test/file.png'; - })]); - - $className = Select::class; - - $mock = $this->getMockBuilder($className) - ->setMethods(['setTagName']) - ->disableOriginalConstructor() - ->getMock(); - - $reflectedClass = new \ReflectionClass($className); - $property = $reflectedClass->getProperty('content'); - $property->setAccessible(true); - $property->setValue($mock, $nL); - - $content1 = ['test 1', '1']; - $content2 = ['test 2', '2', 1]; - $content3 = ['test 3', '3', 0, ['data-url' => 'test/file.png']]; - - $mock->add($content1); - $mock->add($content2); - $mock->add($content3); - } - +// public function testAdd() +// { +//// $nL = $this->createMock(NodeList::class); +// $nL = $this->getMockBuilder(NodeList::class) +// ->setMethods(['add']) +// ->getMock(); +// +// $nL->expects($this->exactly(3)) +// ->method('add') +// ->withConsecutive([$this->callback(function(Tag $subject) { +// return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 1' && $subject->getAttributes()->get('value')->getValue() === '1'; +// })], +// [$this->callback(function(Tag $subject) { +// return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 2' && $subject->getAttributes()->get('value')->getValue() === '2' && $subject->getAttributes()->has('selected'); +// })], +// [$this->callback(function(Tag $subject) { +// return $subject->getTagName() === 'option' && (string) $subject->getContent() === 'test 3' && $subject->getAttributes()->get('value')->getValue() === '3' && $subject->getAttributes()->get('data-url')->getValue() === 'test/file.png'; +// })]); +// +// $className = Select::class; +// +// $mock = $this->getMockBuilder($className) +// ->setMethods(['setTagName']) +//// ->disableOriginalConstructor() +// ->getMock(); +// +// $reflectedClass = new \ReflectionClass($className); +// $property = $reflectedClass->getProperty('content'); +// $property->setAccessible(true); +// $property->setValue($mock, $nL); +// +// $content1 = ['test 1', '1']; +// $content2 = ['test 2', '2', 1]; +// $content3 = ['test 3', '3', 0, ['data-url' => 'test/file.png']]; +// +// $mock->add($content1); +// $mock->add($content2); +// $mock->add($content3); +// } // public function testAddWithContainer() // { // $nL = $this->createMock(NodeList::class); diff --git a/tests/Unit/TextTest.php b/tests/Unit/TextTest.php index 839749c..7b595d1 100644 --- a/tests/Unit/TextTest.php +++ b/tests/Unit/TextTest.php @@ -2,19 +2,23 @@ namespace Ht7\Html\Tests\Unit; -use \PHPUnit\Framework\TestCase; -use \Ht7\Html\Text; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\MockObject\MockObject; +use Ht7\Html\Text; class TextTest extends TestCase { - - public function testConstructor() + #[Test] + #[TestDox('Tag initialisation.')] + public function textConstructor(): void { $className = Text::class; - $content = ['test text']; + $content = 'test text'; $mock = $this->getMockBuilder($className) - ->setMethods(['setContent']) + ->onlyMethods(['setContent']) ->disableOriginalConstructor() ->getMock(); @@ -27,99 +31,103 @@ public function testConstructor() $constructor->invoke($mock, $content); } - public function testGetContent() + #[Test] + #[TestDox('Get content.')] + public function getContent(): void { $className = Text::class; $content = 'test text'; - $mock = $this->getMockBuilder($className) - ->setMethods(['jsonSerialize']) - ->disableOriginalConstructor() - ->getMock(); + /** @var Text $sut */ + $sut = $this->getMockText([]); $reflectedClass = new \ReflectionClass($className); $property = $reflectedClass->getProperty('content'); $property->setAccessible(true); - $property->setValue($mock, $content); + $property->setValue($sut, $content); - $this->assertEquals($content, $mock->getContent()); + $this->assertEquals($content, $sut->getContent()); } - public function testJsonSerialize() + #[Test] + #[TestDox('Json serialize.')] + public function jsonSerialize(): void { $className = Text::class; $content = 'test text'; - $mock = $this->getMockBuilder($className) - ->setMethods(['setContent']) - ->disableOriginalConstructor() - ->getMock(); + $sut = $this->getMockText([]); $reflectedClass = new \ReflectionClass($className); $property = $reflectedClass->getProperty('content'); $property->setAccessible(true); - $property->setValue($mock, $content); + $property->setValue($sut, $content); $expected = '"' . $content . '"'; - $actual = json_encode($mock); + $actual = json_encode($sut); $this->assertEquals($expected, $actual); } - public function testSetContent() + #[Test] + #[TestDox('Set content.')] + public function setContent(): void { $className = Text::class; $content = 'test text'; - $mock = $this->getMockBuilder($className) - ->setMethods(['jsonSerialize']) - ->disableOriginalConstructor() - ->getMock(); + /** @var Text $sut */ + $sut = $this->getMockText([]); $reflectedClass = new \ReflectionClass($className); $property = $reflectedClass->getProperty('content'); $property->setAccessible(true); - $mock->setContent($content); - $this->assertEquals($content, $property->getValue($mock)); + $sut->setContent($content); + $this->assertEquals($content, $property->getValue($sut)); $content2 = 123; - $mock->setContent($content2); - $this->assertEquals($content2, $property->getValue($mock)); + $sut->setContent($content2); + $this->assertEquals($content2, $property->getValue($sut)); $content3 = 123.001; - $mock->setContent($content3); - $this->assertEquals($content3, $property->getValue($mock)); + $sut->setContent($content3); + $this->assertEquals($content3, $property->getValue($sut)); } - public function testSetContentWithException() + #[Test] + #[TestDox('Set content with an array and trigger exception.')] + public function setContentWithException(): void { - $mock = $this->getMockBuilder(Text::class) - ->setMethods(['jsonSerialize']) - ->disableOriginalConstructor() - ->getMock(); - + /** @var Text $sut */ + $sut = $this->getMockText([]); $this->expectException(\InvalidArgumentException::class); - $mock->setContent([]); + $sut->setContent([]); } - public function testToString() + #[Test] + #[TestDox('Trigger __toString method.')] + public function render(): void { $expected = 'test text.'; - $text = $this->getMockBuilder(Text::class) - ->setMethods(['getContent']) - ->disableOriginalConstructor() - ->getMock(); - - $text->expects($this->once()) + $sut = $this->getMockText(['getContent']); + $sut->expects($this->once()) ->method('getContent') ->willReturn($expected); - $this->assertEquals($expected, ((string) $text)); + $this->assertEquals($expected, (string) $sut); + } + + private function getMockText(array $methods): MockObject + { + return $this->getMockBuilder(Text::class) + ->onlyMethods($methods) + ->disableOriginalConstructor() + ->getMock(); } } diff --git a/tests/Unit/Utilities/ImporterArrayTest.php b/tests/Unit/Utilities/ImporterArrayTest.php index d31af5b..4fa8ba7 100644 --- a/tests/Unit/Utilities/ImporterArrayTest.php +++ b/tests/Unit/Utilities/ImporterArrayTest.php @@ -23,7 +23,9 @@ public function setUp() { parent::setUp(); - $this->importer = ImporterArray::getInstance(); + + $this->importer = new ImporterArray(); + ImporterArray::setInstance($this->importer); } public function testCreateTag() diff --git a/tests/configuration.xml b/tests/configuration.xml index 0134f1f..2e2b694 100644 --- a/tests/configuration.xml +++ b/tests/configuration.xml @@ -1,61 +1,66 @@ - + - - + + ../src - - ../api - ../tests/Integration - ../src/Widgets/Table - ../src/Renderable.php - ../src/Lists/AbstractRenderableList.php - ../src/Tags/AbstractSourceContainer.php - ../src/Widgets/Modelable.php - ../src/Widgets/Viewable.php - ../src/Widgets/Table/Models/Modelable.php - ../src/Widgets/Wrapper/Markups/Bootstrap.php - - - + + + ../api + ../src/Iterators + ../src/Lists + ../src/Models + ../src/Tags + ../src/Utilities + ../src/Widgets + ../src/Widgets/Table + ../tests/Integration + ../src/Attribute.php + ../src/Callback.php + ../src/Node.php + ../src/Replacer.php + ../src/Tag.php + ../src/Renderable.php + ../src/Lists/AbstractRenderableList.php + ../src/Tags/AbstractSourceContainer.php + ../src/Widgets/Modelable.php + ../src/Widgets/Viewable.php + ../src/Widgets/Table/Models/Modelable.php + ../src/Widgets/Wrapper/Markups/Bootstrap.php + + - + + + - - ./unit/ + + Unit - - ./Functional/ + + Integration