diff --git a/CHANGELOG.md b/CHANGELOG.md index 144319e..2e22acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ # Change Log -## 0.1.0 Under development +## 0.1.1 Under development + +## 0.1.0 March 5, 2024 + +- Initial release diff --git a/README.md b/README.md index 6de01d8..e4554e2 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,36 @@

- + -

Template.

+

UI Awesome HTML Helpers Code Generator for PHP.


- - PHPUnit + + PHPUnit - - Codecov + + Codecov - - Infection + + Infection - - Psalm + + Psalm - - Psalm Coverage + + Psalm Coverage + + + Style ci - - Style ci -

+HTML Helper is a PHP library that simplifies the creation of HTML elements. It provides a set of classes to generate +HTML attributes, encode content, sanitize HTML content, and more. + + ## Installation The preferred way to install this extension is through [composer](https://getcomposer.org/download/). @@ -34,20 +38,328 @@ The preferred way to install this extension is through [composer](https://getcom Either run ```shell -composer require --prefer-dist package +composer require --prefer-dist ui-awesome/html-helper:^0.1 ``` or add ```json -"package": "version" +"ui-awesome/html-helper": "^0.1" ``` -to the require-dev section of your `composer.json` file. +to the require section of your `composer.json` file. ## Usage -[Check the documentation docs](docs/README.md) to learn about usage. +### Add CSS classes + +The `CssClasses::class` helper can be used to add CSS classes to an HTML element. + +The method accepts three parameters: + +- `attributes:` (array): The HTML attributes of the element. +- `classes:` (array|string): The CSS classes to add. +- `overwrite:` (bool): Whether to overwrite the `class` attribute or not. For default, it is `false`. + +```php +attributes, ['btn', 'btn-primary', 'btn-lg']); +``` + +Overwriting the `class` attribute: + +```php + 'btn']; + +$classes = CssClasses::add($this->attributes, ['btn-primary', 'btn-lg'], true); +``` + +### Convert regular expression to pattern + +The `Utils::class` helper can be used to normalize a regular expression. + +The method accepts one parameter: + +- `regexp:` (string): The pattern to normalize. +- `delimiter:` (string): The delimiter to use. For default, it is `null`. + +```php +alert("Hello, World!")'); +``` + +### Encode value + +The `Encode::class` helper can be used to encode HTML value. + +The method accepts tree parameters: + +- `value:` (string): The value to encode. +- `doubleEncode:` (bool): Whether to double encode the value or not. For default, it is `true`. +- `charset:` (string): The charset to use. For default, it is `UTF-8`. + +```php +alert("Hello, World!")'); +``` + +### Get short class name + +The `Utils::class` helper can be used to get the short class name. + +The method accepts one parameter: + +- `class:` (string): The class name to get the short name. +- `suffix:` (string): Whether to append the `::class` suffix to the class name. + For default, it is `true`. If it is `false`, the method will return the short name without the `::class` suffix. +- `lowercase:` (bool): Whether to convert the class name to lowercase or not. + For default, it is `false`. + +```php +alert("Hello, World!")'); +``` + +### Render HTML attributes + +The `Attributes::class` helper can be used to `render` HTML attributes in a programmatic way. + +```php + 'btn btn-primary', + 'id' => 'submit-button', + 'disabled' => true, + ] +); +``` + +### Validate value in list + +The `Validator::class` helper can be used to validate a value in a list. + +The method accepts tree parameters: + +- `value:` (mixed): The value to validate. +- `exceptionMessage:` (string): The exception message to throw if the value is not in the list. +- `list:` (...string): The list to validate the value. + +```php +withPhpCsFixerSets(perCS20: true) ->withPreparedSets( - arrays: true, cleanCode: true, comments:true, docblocks: true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8ff515d..e77699d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,7 +11,7 @@ stopOnFailure="false" > - + tests diff --git a/src/Attributes.php b/src/Attributes.php new file mode 100644 index 0000000..c8ac770 --- /dev/null +++ b/src/Attributes.php @@ -0,0 +1,207 @@ + 'xyz', 'age' => 13]`, two attributes will be + * generated instead of one: `data-name="xyz" data-age="13"`. + */ + private const DATA = ['aria', 'data', 'data-ng', 'ng']; + + /** + * @var int the JSON encoding options used in {@see renderAttribute()} when rendering array values. + */ + private const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | + JSON_THROW_ON_ERROR; + + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes that are + * rendered by {@see render()}. + * + * @psalm-var string[] + */ + private const ORDER = [ + 'class', + 'id', + 'name', + 'type', + 'http-equiv', + 'value', + 'href', + 'src', + 'for', + 'title', + 'alt', + 'role', + 'tabindex', + 'srcset', + 'form', + 'action', + 'method', + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + 'alt', + 'title', + 'rel', + 'media', + ]; + + /** + * Renders the HTML tag attributes. + * + * Attributes whose values are of boolean type will be treated as + * [boolean attributes](http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes). + * + * Attributes whose values are null will not be rendered. + * + * The values of attributes will be HTML-encoded using {@see encode()}. + * + * @param array $attributes Attributes to be rendered. The attribute values will be HTML-encoded using + * {@see Encode}. + * + * `aria` and `data` attributes get special handling when they are set to an array value. In these cases, the array + * will be "expanded" and a list of ARIA/data attributes will be rendered. For example, + * `'aria' => ['role' => 'checkbox', 'value' => 'true']` would be rendered as + * `aria-role="checkbox" aria-value="true"`. + * + * If a nested `data` value is set to an array, it will be JSON-encoded. For example, + * `'data' => ['params' => ['id' => 1, 'name' => 'yii']]` would be rendered as + * `data-params='{"id":1,"name":"yii"}'`. + * + * @return string The rendering result. If the attributes are not empty, they will be rendered into a string with a + * leading space (so that it can be directly appended to the tag name in a tag). If there is no attribute, an + * empty string will be returned. + * + * {@see addCssClass()} + */ + public static function render(array $attributes): string + { + $html = ''; + $sorted = []; + + foreach (self::ORDER as $name) { + if (isset($attributes[$name])) { + /** @psalm-var string[] $sorted */ + $sorted[$name] = $attributes[$name]; + } + } + + $attributes = array_merge($sorted, $attributes); + + /** + * @var string $name + * @var mixed $values + */ + foreach ($attributes as $name => $values) { + if ($name !== '' && $values !== '' && $values !== null) { + $html .= self::renderAttributes($name, $values); + } + } + + return $html; + } + + private static function renderAttribute(string $name, string $encodedValue = '', string $quote = '"'): string + { + if ($encodedValue === '') { + return ' ' . $name; + } + + return ' ' . $name . '=' . $quote . $encodedValue . $quote; + } + + private static function renderAttributes(string $name, mixed $values): string + { + return match (gettype($values)) { + 'array' => self::renderArrayAttributes($name, $values), + 'boolean' => self::renderBooleanAttributes($name, $values), + default => self::renderAttribute($name, Encode::value($values)), + }; + } + + private static function renderArrayAttributes(string $name, array $values): string + { + $attributes = self::renderAttribute($name, json_encode($values, self::JSON_FLAGS), '\''); + + if (in_array($name, self::DATA, true)) { + $attributes = self::renderDataAttributes($name, $values); + } + + if ($name === 'class') { + $attributes = self::renderClassAttributes($name, $values); + } + + if ($name === 'style') { + $attributes = self::renderStyleAttributes($name, $values); + } + + return $attributes; + } + + private static function renderBooleanAttributes(string $name, bool $value): string + { + return $value === true ? self::renderAttribute($name) : ''; + } + + private static function renderClassAttributes(string $name, array $values): string + { + /** @psalm-var string[] $values */ + return match ($values) { + [] => '', + default => " $name=\"" . Encode::content(implode(' ', $values)) . '"', + }; + } + + private static function renderDataAttributes(string $name, array $values): string + { + $result = ''; + + /** @psalm-var array $values */ + foreach ($values as $n => $v) { + $result .= match (is_array($v)) { + true => self::renderAttribute($name . '-' . $n, json_encode($v, self::JSON_FLAGS), '\''), + false => self::renderAttribute($name . '-' . $n, Encode::value($v)), + }; + } + + return $result; + } + + private static function renderStyleAttributes(string $name, array $values): string + { + $result = ''; + + /** @psalm-var string[] $values */ + foreach ($values as $n => $v) { + $result .= "$n: $v; "; + } + + return $result === '' ? '' : " $name=\"" . Encode::content(rtrim($result)) . '"'; + } +} diff --git a/src/CssClass.php b/src/CssClass.php new file mode 100644 index 0000000..56aef3b --- /dev/null +++ b/src/CssClass.php @@ -0,0 +1,97 @@ + $class) { + if (is_int($key) && !in_array($class, $existingClasses, true)) { + $existingClasses[] = $class; + } elseif (!isset($existingClasses[$key])) { + $existingClasses[$key] = $class; + } + } + + return $existingClasses; + } +} diff --git a/src/Encode.php b/src/Encode.php new file mode 100644 index 0000000..e223648 --- /dev/null +++ b/src/Encode.php @@ -0,0 +1,55 @@ +tag content`. + * + * Characters encoded are: &, <, >. + * + * @param string $content The content to be encoded. + * @param bool $doubleEncode If already encoded, entities should be encoded. + * @param string $charset The encoding to use, defaults to "UTF-8". + * + * @return string Encoded content. + * + * @link https://html.spec.whatwg.org/#data-state + */ + public static function content(string $content, bool $doubleEncode = true, string $charset = 'UTF-8'): string + { + return htmlspecialchars($content, self::HTMLSPECIALCHARS_FLAGS, $charset, $doubleEncode); + } + + /** + * Encodes special characters into HTML entities for use as HTML tag quoted attribute value + * i.e. ``. + * Characters encoded are: &, <, >, ", ', U+0000 (null). + * + * @param mixed $value The attribute value to be encoded. + * @param bool $doubleEncode If already encoded, entities should be encoded. + * @param string $charset The encoding to use, defaults to "UTF-8". + * + * @return string Encoded attribute value. + * + * @link https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state + * @link https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state + */ + public static function value(mixed $value, bool $doubleEncode = true, string $charset = 'UTF-8'): string + { + $value = htmlspecialchars((string) $value, self::HTMLSPECIALCHARS_FLAGS, $charset, $doubleEncode); + + return strtr($value, ['\u{0000}' => '�']); + } +} diff --git a/src/Example.php b/src/Example.php deleted file mode 100644 index 4dfa14e..0000000 --- a/src/Example.php +++ /dev/null @@ -1,13 +0,0 @@ - + */ + private static array $removeEvilAttributes = [ + 'form', + 'formaction', + 'style', + ]; + /** + * @var array + */ + private static array $removeEvilHtmlTags = [ + 'button', + 'form', + 'input', + 'select', + 'svg', + 'textarea', + ]; + + /** + * Initialize the class with custom configuration. + * + * @psalm-param array $removeEvilAttributes + * @psalm-param array $removeEvilHtmlTags + */ + public static function initialize(array $removeEvilAttributes = [], array $removeEvilHtmlTags = []): void + { + self::$removeEvilAttributes = $removeEvilAttributes; + self::$removeEvilHtmlTags = $removeEvilHtmlTags; + } + + /** + * Sanitizes HTML content to prevent XSS attacks. + * + * @param RenderInterface|string ...$values The HTML content to sanitize. + * + * @return string The sanitized HTML content. + */ + public static function html(string|RenderInterface ...$values): string + { + $cleanHtml = ''; + + foreach ($values as $value) { + if ($value instanceof RenderInterface) { + $value = $value->render(); + } + + /** @psalm-var string|string[] $cleanValue */ + $cleanValue = self::cleanXSS($value); + $cleanValue = is_array($cleanValue) ? implode('', $cleanValue) : $cleanValue; + + $cleanHtml .= $cleanValue; + } + + return $cleanHtml; + } + + private static function cleanXSS(string $content): string|array + { + $antiXss = new AntiXSS(); + + $antiXss->removeEvilAttributes(self::$removeEvilAttributes); + $antiXss->removeEvilHtmlTags(self::$removeEvilHtmlTags); + + return $antiXss->xss_clean($content); + } +} diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..3bdcf37 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,209 @@ + 'content', 'prefix' => '', 'suffix' => '[0]']` + * + * An property expression is an property name prefixed and/or suffixed with array indexes. It is mainly used in + * tabular data input and/or input of an array type. Below are some examples: + * + * - `[0]content` is used in tabular data input to represent the "content" property for the first model in tabular + * input; + * - `dates[0]` represents the first array element of the "dates" property; + * - `[0]dates[0]` represents the first array element of the "dates" property for the first model in tabular + * input. + * + * @param string $property The property name or expression + * + * @throws InvalidArgumentException If the property name contains non-word characters. + * + * @psalm-return string[] + */ + private static function parseProperty(string $property): array + { + if (!preg_match('/(^|.*\])([\w\.\+\-_]+)(\[.*|$)/u', $property, $matches)) { + throw new InvalidArgumentException('Property name must contain word characters only.'); + } + + return [ + 'name' => $matches[2], + 'prefix' => $matches[1], + 'suffix' => $matches[3], + ]; + } +} diff --git a/src/Validator.php b/src/Validator.php new file mode 100644 index 0000000..51ad5e8 --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,101 @@ +assertSame($expected, Attributes::render($attributes)); + } +} diff --git a/tests/CssClassTest.php b/tests/CssClassTest.php new file mode 100644 index 0000000..5002ec5 --- /dev/null +++ b/tests/CssClassTest.php @@ -0,0 +1,108 @@ +assertSame([], $attributes); + + CssClass::add($attributes, ['test-class']); + $this->assertSame(['class' => 'test-class'], $attributes); + + CssClass::add($attributes, ['test-class']); + $this->assertSame(['class' => 'test-class'], $attributes); + + CssClass::add($attributes, ['test-class-1']); + $this->assertSame(['class' => 'test-class test-class-1'], $attributes); + + CssClass::add($attributes, ['test-class', 'test-class-1']); + $this->assertSame(['class' => 'test-class test-class-1'], $attributes); + + CssClass::add($attributes, ['test-class-2']); + $this->assertSame(['class' => 'test-class test-class-1 test-class-2'], $attributes); + + CssClass::add($attributes, ['test-override-class'], true); + $this->assertSame(['class' => 'test-override-class'], $attributes); + } + + public function testAddMethodWithArrayClasses() + { + $attributes = ['class' => ['existing-class-1', 'existing-class-2']]; + + CssClass::add($attributes, 'new-class'); + $this->assertSame('existing-class-1 existing-class-2 new-class', $attributes['class']); + + $attributes = ['class' => 'existing-class-1 existing-class-2']; + + CssClass::add($attributes, 'new-class'); + $this->assertEquals('existing-class-1 existing-class-2 new-class', $attributes['class']); + } + + public function testAddWithDefaultValueAttributesIsArray(): void + { + $attributes = []; + + CssClass::add($attributes, 'test-class'); + $this->assertSame('test-class', $attributes['class']); + + $attributes = []; + + CssClass::add($attributes, ['test-class-1', 'test-class-2']); + $this->assertSame('test-class-1 test-class-2', $attributes['class']); + } + + public function testAddDefaultValueAttributeExistClass(): void + { + $attributes = ['class' => 'existing-class']; + + CssClass::add($attributes, 'new-class'); + $this->assertEquals('existing-class new-class', $attributes['class']); + } + + public function testAddWithString(): void + { + $attributes = []; + + CssClass::add($attributes, ''); + $this->assertEmpty($attributes); + + CssClass::add($attributes, 'test-class'); + $this->assertSame(['class' => 'test-class'], $attributes); + + CssClass::add($attributes, 'test-class'); + $this->assertSame(['class' => 'test-class'], $attributes); + + CssClass::add($attributes, 'test-class-1'); + $this->assertSame(['class' => 'test-class test-class-1'], $attributes); + + CssClass::add($attributes, 'test-class test-class-1'); + $this->assertSame(['class' => 'test-class test-class-1'], $attributes); + + CssClass::add($attributes, 'test-class-2'); + $this->assertSame(['class' => 'test-class test-class-1 test-class-2'], $attributes); + + CssClass::add($attributes, 'test-override-class', true); + $this->assertSame(['class' => 'test-override-class'], $attributes); + } + + public function testMergeMethodAssignToKey() + { + $existingClasses = ['existing-class-1', 'existing-class-2']; + $additionalClasses = ['keyed-class' => 'new-class']; + + $merged = Assert::invokeMethod(new CssClass(), 'merge', [$existingClasses, $additionalClasses]); + + $this->assertArrayHasKey('keyed-class', $merged); + $this->assertEquals('new-class', $merged['keyed-class']); + } +} diff --git a/tests/EncodeTest.php b/tests/EncodeTest.php new file mode 100644 index 0000000..5298857 --- /dev/null +++ b/tests/EncodeTest.php @@ -0,0 +1,36 @@ +assertSame($expected, Encode::content($value)); + $this->assertSame($expected, Encode::content($value, $doubleEncode)); + } + + /** + * @dataProvider UIAwesome\Html\Helper\Tests\Provider\EncodeProvider::encodeValue + * + * @param string $value Value to encode. + * @param string $expected Expected result. + * @param bool $doubleEncode Whether to encode HTML entities in `$value`. + */ + public function testValue(string $value, string $expected, bool $doubleEncode): void + { + $this->assertSame($expected, Encode::value($value)); + $this->assertSame($expected, Encode::value($value, $doubleEncode)); + } +} diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 4e7a602..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue($example->getExample()); - } -} diff --git a/tests/Provider/AttributesProvider.php b/tests/Provider/AttributesProvider.php new file mode 100644 index 0000000..5565925 --- /dev/null +++ b/tests/Provider/AttributesProvider.php @@ -0,0 +1,136 @@ + true, + 'disabled' => true, + 'hidden' => false, + 'required' => 'yes', + ], + ], + [ + ' class="first second"', [ + 'class' => ['first', 'second'], + ]], + [ + '', [ + 'class' => [], + ]], + [ + ' style="width: 100px; height: 200px;"', [ + 'style' => [ + 'width' => '100px', + 'height' => '200px', + ], + ]], + [ + ' name="position" value="42"', [ + 'value' => 42, + 'name' => 'position', + ]], + [ + ' class="a b" id="x" data-a="1" data-b="2" style="width: 100px;" any=\'[1,2]\'', + [ + 'id' => 'x', + 'class' => ['a', 'b'], + 'data' => [ + 'a' => 1, + 'b' => 2, + ], + 'style' => [ + 'width' => '100px', + ], + 'any' => [1, 2], + ], + ], + [ + ' data-a="0" data-b=\'[1,2]\' any="42"', + [ + 'class' => [], + 'style' => [], + 'data' => [ + 'a' => 0, + 'b' => [1, 2], + ], + 'any' => 42, + ], + ], + [ + ' data-foo=\'[]\'', + [ + 'data' => [ + 'foo' => [], + ], + ], + ], + [ + ' src="xyz" data-a="1" data-b="c"', + [ + 'src' => 'xyz', + 'data' => [ + 'a' => 1, + 'b' => 'c', + ], + ], + ], + [ + ' src="xyz" ng-a="1" ng-b="c"', + [ + 'src' => 'xyz', + 'ng' => [ + 'a' => 1, + 'b' => 'c', + ], + ], + ], + [ + ' src="xyz" data-ng-a="1" data-ng-b="c"', + [ + 'src' => 'xyz', + 'data-ng' => [ + 'a' => 1, + 'b' => 'c', + ], + ], + ], + [ + ' src="xyz" aria-a="1" aria-b="c"', + [ + 'src' => 'xyz', + 'aria' => [ + 'a' => 1, + 'b' => 'c', + ], + ], + ], + [ + '', + [ + 'disabled' => null, + ], + ], + [ + '', + [ + 'value' => '', + ], + ], + [ + '', + [ + '' => 'test-class', + ], + ], + ]; + } +} diff --git a/tests/Provider/EncodeProvider.php b/tests/Provider/EncodeProvider.php new file mode 100644 index 0000000..42523c4 --- /dev/null +++ b/tests/Provider/EncodeProvider.php @@ -0,0 +1,32 @@ +&\"'\x80", 'a<>&"'�', false], + ["a<>&\"'\x80", 'a<>&amp;"'�', true], + ['Sam & Dark', 'Sam & Dark', false], + ['Sam & Dark', 'Sam &amp; Dark', true], + ['\u{0000}', '\u{0000}', false], + ['\u{0000}', '\u{0000}', true], + ]; + } + + public static function encodeValue(): array + { + return [ + ["a<>&\"'\x80", 'a<>&"'�', false], + ["a<>&\"'\x80", 'a<>&amp;"'�', true], + ['Sam & Dark', 'Sam & Dark', false], + ['Sam & Dark', 'Sam &amp; Dark', true], + ['\u{0000}', '�', false], + ['\u{0000}', '�', true], + ]; + } +} diff --git a/tests/Provider/UtilsProvider.php b/tests/Provider/UtilsProvider.php new file mode 100644 index 0000000..4c7a02d --- /dev/null +++ b/tests/Provider/UtilsProvider.php @@ -0,0 +1,54 @@ +assertSame( + ['form', 'style'], + Assert::inaccessibleProperty(new Sanitize(), 'removeEvilAttributes') + ); + $this->assertSame( + ['button', 'form', 'input', 'select', 'svg', 'textarea'], + Assert::inaccessibleProperty(new Sanitize(), 'removeEvilHtmlTags') + ); + } + + public function testHtml(): void + { + $this->assertSame( + 'click', + Sanitize::html('click') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '
', + Sanitize::html('
') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '', + Sanitize::html('') + ); + $this->assertSame( + '
', + Sanitize::html( + '
', + '', + '
' + ) + ); + } + + public function testHtmlWithRenderInterface(): void + { + $this->assertSame( + '', + Sanitize::html(new InputWidget()) + ); + } +} diff --git a/tests/Support/InputWidget.php b/tests/Support/InputWidget.php new file mode 100644 index 0000000..31c9ed3 --- /dev/null +++ b/tests/Support/InputWidget.php @@ -0,0 +1,20 @@ +render(); + } + + public function render(): string + { + return ''; + } +} diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php new file mode 100644 index 0000000..968fe1c --- /dev/null +++ b/tests/UtilsTest.php @@ -0,0 +1,119 @@ +assertSame($expected, Utils::convertToPattern($regexp, $delimiter)); + } + + /** + * @dataProvider UIAwesome\Html\Helper\Tests\Provider\UtilsProvider::convertToPatternInvalid + * + * @param string $regexp The regexp pattern to normalize. + * @param string $message The expected exception message. + * @param string|null $delimiter The delimiter to use. + */ + public function testConvertToPatterInvalid(string $regexp, string $message, ?string $delimiter = null): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + Utils::convertToPattern($regexp, $delimiter); + } + + public function testGenerateArrayableName(): void + { + $this->assertSame('test.name[]', Utils::generateArrayableName('test.name')); + } + + public function testGenerateId(): void + { + $this->assertMatchesRegularExpression('/^id-[0-9a-f]{13}$/', Utils::generateId()); + } + + public function testGenerateIdWithPrefix(): void + { + $this->assertMatchesRegularExpression('/^prefix-[0-9a-f]{13}$/', Utils::generateId('prefix-')); + } + + public function testGenerateInputId(): void + { + $this->assertSame('utilstest-string', Utils::generateInputId('UtilsTest', 'string')); + } + + public function testGenerateInputName(): void + { + $this->assertSame('TestForm[content][body]', Utils::generateInputName('TestForm', 'content[body]')); + } + + /** + * @dataProvider UIAwesome\Html\Helper\Tests\Provider\UtilsProvider::dataGetInputName + */ + public function testGetInputNameDataProvider(string $formName, string $attribute, bool $arrayable, string $expected): void + { + $this->assertSame($expected, Utils::generateInputName($formName, $attribute, $arrayable)); + } + + public function testGetInputNameWithArrayableTrue(): void + { + $this->assertSame('TestForm[content][body][]', Utils::generateInputName('TestForm', 'content[body]', true)); + } + + public function testGetInputNamewithOnlyCharacters(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Property name must contain word characters only.'); + + Utils::generateInputName('TestForm', 'content body'); + } + + public function testGetInputNameExceptionWithTabular(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The form model name cannot be empty for tabular inputs.'); + + Utils::generateInputName('', '[0]dates[0]'); + } + + public function testGetShortNameClass(): void + { + $this->assertSame('UtilsTest::class', Utils::getShortNameClass(self::class)); + } + + public function testGetShortNameClassWithLowercase(): void + { + $this->assertSame('utilstest', Utils::getShortNameClass(self::class, false, true)); + } + + public function testGetShortNameClassWithoutSuffix(): void + { + $this->assertSame('UtilsTest', Utils::getShortNameClass(self::class, false)); + } + + public function testMultibyteGenerateArrayableName(): void + { + $this->assertSame('登录[]', Utils::generateArrayableName('登录')); + $this->assertSame('登录[]', Utils::generateArrayableName('登录[]')); + $this->assertSame('登录[0][]', Utils::generateArrayableName('登录[0]')); + $this->assertSame('[0]登录[0][]', Utils::generateArrayableName('[0]登录[0]')); + } + + public function testMultibyteGenerateInputId(): void + { + $this->assertSame('testform-mąka', Utils::generateInputId('TestForm', 'mĄkA')); + } +} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php new file mode 100644 index 0000000..6881d6a --- /dev/null +++ b/tests/ValidatorTest.php @@ -0,0 +1,93 @@ +expectNotToPerformAssertions(); + + Validator::inList('a', '', 'a', 'b', 'c'); + } + + public function testInListException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value is not in the list: "1".'); + + Validator::inList('1', 'The value is not in the list: "%s".', 'a', 'b', 'c'); + } + + public function testInListExceptionWithEmptyValue(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value must not be empty. The valid values are: "a", "b", "c".'); + + Validator::inList('', '', 'a', 'b', 'c'); + } + + public function testIterable(): void + { + $this->expectNotToPerformAssertions(); + + Validator::isIterable([]); + } + + public function testIsIterableException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value must be an iterable or null value. The value is: string.'); + + Validator::isIterable('value'); + } + + public function testIsNumeric(): void + { + $this->expectNotToPerformAssertions(); + + Validator::isNumeric(1); + } + + public function testIsNumericException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value must be a numeric or null value. The value is: string.'); + + Validator::isNumeric('value'); + } + + public function testIsScalar(): void + { + $this->expectNotToPerformAssertions(); + + Validator::isScalar(1); + } + + public function testIsScalarException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value must be a scalar or null value. The value is: array.'); + + Validator::isScalar([]); + } + + public function testIsString(): void + { + $this->expectNotToPerformAssertions(); + + Validator::isString('value'); + } + + public function testIsStringException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value must be a string or null value. The value is: array.'); + + Validator::isString([]); + } +}