diff --git a/tests/CloneTest.php b/tests/CloneTest.php index 3c630c32..529dacba 100644 --- a/tests/CloneTest.php +++ b/tests/CloneTest.php @@ -2,17 +2,154 @@ namespace ipl\Tests\Html; +use Closure; use InvalidArgumentException; use ipl\Html\Attribute; use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; use ipl\Html\Form; -use ipl\Html\FormElement\InputElement; use ipl\Html\FormElement\SelectElement; use ipl\Html\FormElement\SubmitElement; +use ReflectionException; +use ReflectionFunction; use ReflectionProperty; class CloneTest extends TestCase { + public const IGNORED_FILES = [ + "BaseFormElement.php", + "FormElements.php", + "RadioOption.php" + ]; + + public const ELEMENT_PARAMS = [ + "_defaultName" => 'default-name', + "ButtonElement" => [], + "SelectElement" => [ + 'label' => 'test-label', + 'options' => [ + 1 => 'option 1', + 'option 2' => 2 + ] + ], + "SelectOption" => 'test-value' + ]; + + public static function noopCallback() + { + } + + /** + * @throws ReflectionException + */ + public function testFormElementsCloning(): void + { + $elementsDir = realpath(__DIR__ . '/../src/FormElement'); + + if ($handle = opendir($elementsDir)) { + while (false !== ($file = readdir($handle))) { + if ($file === '.' || $file === '..' || in_array($file, self::IGNORED_FILES)) { + continue; + } + + include_once $elementsDir . '/' . $file; + + $className = explode(".", $file)[0]; + $prefix = uniqid(); + $classPath = "ipl\\Html\\FormElement\\" . $className; + + /** @var BaseHtmlElement $element */ + $element = new $classPath( + self::ELEMENT_PARAMS['_defaultName'], + self::ELEMENT_PARAMS[$className] ?? [] + ); + + $this->registerCallbackFor($element, 'test-global-scope', 'strtolower'); + $this->registerCallbackFor($element, 'test-instance-scope-noop-inline', function () { + }); + $this->registerCallbackFor($element, 'test-instance-noop-attribute', [$this, 'noopCallback']); + + $this->registerCallbackFor( + $element, + 'test-closure-static-scope-noop', + Closure::fromCallable('self::noopCallback') + ); + + $this->registerCallbackFor( + $element, + 'test-closure-instance-scope-noop', + Closure::fromCallable([$this, 'noopCallback']) + ); + + $this->registerCallbackFor( + $element, + 'test-closure-instance-element-get-label', + Closure::fromCallable([$element, 'getLabel']) + ); + + $clone = clone $element; + + $this->assertCallbacksFor($element, $this); + $this->assertCallbacksFor($clone, $this); + } + } + } + + /** + * @param BaseHtmlElement $element + * @param string $name + * @param callable|null $callback + * @param callable|null $setterCallback + * + * @return void + */ + public function registerCallbackFor( + BaseHtmlElement $element, + string $name, + ?callable $callback, + ?callable $setterCallback = null + ): void + { + $element->getAttributes()->registerAttributeCallback($name, $callback, $setterCallback); + } + + public function assertCallbacksFor(object $element, object $thisRef) + { + $this->assertGlobalOrStaticCallback($element->getAttributes(), 'test-global-scope'); + $this->assertCallbackBelongsTo($element->getAttributes(), 'test-instance-scope-noop-inline', $thisRef); + $this->assertCallbackBelongsTo( + $element->getAttributes(), + 'test-instance-noop-attribute', + $thisRef + ); + $this->assertGlobalOrStaticCallback( + $element->getAttributes(), + 'test-closure-static-scope-noop' + ); + $this->assertGlobalOrStaticCallback( + $element->getAttributes(), + 'test-closure-instance-scope-noop' + ); + $this->assertCallbackBelongsTo( + $element->getAttributes(), + 'test-closure-instance-element-get-label', + $element + ); + } + + public function assertGlobalOrStaticCallback(Attributes $attributes, string $callbackName) + { + $callback = $this->getAttributeCallback($attributes, $callbackName); + $this->assertTrue($this->isCallbackGlobalOrStatic($callback)); + } + + public function assertCallbackBelongsTo(Attributes $attributes, string $callbackName, object $owner) + { + $callback = $this->getAttributeCallback($attributes, $callbackName); + $callbackThis = $this->getCallbackThis($callback); + $this->assertTrue(spl_object_id($callbackThis) === spl_object_id($owner)); + } + public function testCloningSubmitElement(): void { $originalButton = new SubmitElement('exampleButton', ['class' => 'class01']); @@ -69,33 +206,67 @@ public function testCloningAttributes(): void $this->assertNotSame($original, $clone); } - public function testCallbacks() + /** + * @param callable $callback + * + * @return bool + * @throws ReflectionException + */ + protected function isCallbackGlobalOrStatic(callable $callback): bool { - $input = (new InputElement('text', 'text')) - ->setValue('original'); - $clone = (clone $input) - ->setValue('clone'); + if (! $callback instanceof Closure) { + if (is_array($callback) && ! is_string($callback[0])) { + return false; + } + } else { + $closureThis = (new ReflectionFunction($callback)) + ->getClosureThis(); + + if ($closureThis) { + return false; + } + } - $this->assertEquals('original', $this->getAttributeCallbackValue($input->getAttributes(), 'value')); - $this->assertEquals('clone', $this->getAttributeCallbackValue($clone->getAttributes(), 'value')); + return true; } - protected function getAttributeCallbackValue(Attributes $attributes, string $name) + /** + * @param Attributes $attributes + * @param string $name + * + * @return mixed + * @throws ReflectionException + */ + protected function getAttributeCallback(Attributes $attributes, string $name) { $callbacksProperty = new ReflectionProperty(get_class($attributes), 'callbacks'); $callbacksProperty->setAccessible(true); $callbacks = $callbacksProperty->getValue($attributes); if (isset($callbacks[$name])) { - $attribute = $callbacks[$name](); + return $callbacks[$name]; + } - if ($attribute instanceof Attribute) { - return $attribute->getValue(); - } + throw new InvalidArgumentException(); + } - return $attribute; + /** + * @param callable $callback + * + * @return mixed|object|null + * @throws ReflectionException + */ + protected function getCallbackThis(callable $callback) + { + if (! $callback instanceof Closure) { + if (is_array($callback) && ! is_string($callback[0])) { + return $callback[0]; + } else { + return null; + } } - throw new InvalidArgumentException(); + return (new ReflectionFunction($callback)) + ->getClosureThis(); } }