diff --git a/inc/Data/ThirdPartyDataFormatter.php b/inc/Data/ThirdPartyDataFormatter.php index 5dfd179..a8c79c4 100644 --- a/inc/Data/ThirdPartyDataFormatter.php +++ b/inc/Data/ThirdPartyDataFormatter.php @@ -20,7 +20,7 @@ class ThirdPartyDataFormatter /** * Formats third party data for a given set of input arguments and returns the corresponding output. * - * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L94 + * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/54cd44d1bd197a7809ab2f6ede4d13a973087c3d/src/utils/index.ts#L105 * * @param ThirdPartyData $data Third party data to format. * @param array $args Input arguments to format third party data with. @@ -31,15 +31,20 @@ public static function formatData(ThirdPartyData $data, array $args): ThirdParty $htmlData = $data->getHtml(); $scriptsData = $data->getScripts(); - $allScriptParams = array_reduce( - $scriptsData, - static function ($acc, ThirdPartyScriptData $scriptData) { - foreach ($scriptData->getParams() as $param) { - $acc[] = $param; - } - return $acc; - }, - [] + $allScriptParams = array_unique( + array_reduce( + $scriptsData, + static function ($acc, ThirdPartyScriptData $scriptData) { + foreach ($scriptData->getParams() as $param) { + $acc[] = $param; + } + foreach (array_keys($scriptData->getOptionalParams()) as $param) { + $acc[] = $param; + } + return $acc; + }, + [] + ) ); $scriptUrlParamInputs = self::intersectArgs($args, $allScriptParams); @@ -84,20 +89,24 @@ static function ($acc, ThirdPartyScriptData $scriptData) { } if (isset($newData['scripts']) && $newData['scripts']) { $newData['scripts'] = array_map( - static function ($scriptData) use ($scriptUrlParamInputs) { + static function ($scriptData) use ($allScriptParams, $scriptUrlParamInputs) { if (isset($scriptData['url'])) { $scriptData['url'] = self::formatUrl( $scriptData['url'], - $scriptData['params'], - $scriptUrlParamInputs + $allScriptParams, + $scriptUrlParamInputs, + [], + $scriptData['optionalParams'] ?? [] ); } else { $scriptData['code'] = self::formatCode( $scriptData['code'], - $scriptUrlParamInputs + $scriptUrlParamInputs, + $scriptData['optionalParams'] ?? [] ); } - unset($scriptData['params']); // Params are irrelevant for formatted output. + // Params are irrelevant for formatted output. + unset($scriptData['params'], $scriptData['optionalParams']); return $scriptData; }, $newData['scripts'] @@ -110,7 +119,7 @@ static function ($scriptData) use ($scriptUrlParamInputs) { /** * Formats the given HTML arguments into an HTML string. * - * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L55 + * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/54cd44d1bd197a7809ab2f6ede4d13a973087c3d/src/utils/index.ts#L66 * * @param string $element Element tag name for the HTML element. * @param array $attributes Attributes for the HTML element. @@ -152,17 +161,24 @@ public static function formatHtml( /** * Formats the given URL arguments into a URL string. * - * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L28 + * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/54cd44d1bd197a7809ab2f6ede4d13a973087c3d/src/utils/index.ts#L28 * - * @param string $url Base URL. - * @param string[] $params Parameter names. - * @param array $args Input arguments for the src attribute query parameters. - * @param array $slugParamArg Optional. Input argument for the src attribute slug query parameter. - * Default empty array. + * @param string $url Base URL. + * @param string[] $params Parameter names. + * @param array $args Input arguments for the src attribute query parameters. + * @param array $slugParamArg Optional. Input argument for the src attribute slug query parameter. + * Default empty array. + * @param array $optionalParams Optional. Optional parameter names and their defaults. + * Default empty array. * @return string HTML string. */ - public static function formatUrl(string $url, array $params, array $args, array $slugParamArg = []): string - { + public static function formatUrl( + string $url, + array $params, + array $args, + array $slugParamArg = [], + array $optionalParams = [] + ): string { if ($slugParamArg) { $slug = array_values($slugParamArg)[0]; @@ -181,6 +197,14 @@ public static function formatUrl(string $url, array $params, array $args, array if ($params && $args) { $queryArgs = self::intersectArgs($args, $params); + if ($optionalParams) { + $optionalArgs = self::intersectArgs($optionalParams, $params); + foreach ($optionalArgs as $k => $v) { + if (!isset($queryArgs[$k])) { + $queryArgs[$k] = $v; + } + } + } if ($queryArgs) { $url = self::setUrlQueryArgs($url, $queryArgs); } @@ -192,21 +216,29 @@ public static function formatUrl(string $url, array $params, array $args, array /** * Formats the given code arguments into a code string. * - * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/0831b937a8468e0f74bd79edd5a59fa8b2e6e763/src/utils/index.ts#L48 + * @see https://github.com/GoogleChromeLabs/third-party-capital/blob/54cd44d1bd197a7809ab2f6ede4d13a973087c3d/src/utils/index.ts#L52 * - * @param string $code Code string with placeholders for URL query parameters. - * @param array $args Input arguments for the src attribute query parameters. + * @param string $code Code string with placeholders for URL query parameters. + * @param array $args Input arguments for the src attribute query parameters. + * @param array $optionalParams Optional. Optional parameter names and their defaults. + * Default empty array. * @return string HTML string. */ - public static function formatCode(string $code, array $args): string - { + public static function formatCode( + string $code, + array $args, + array $optionalParams = [] + ): string { return preg_replace_callback( '/{{([^}]+)}}/', - static function ($matches) use ($args) { + static function ($matches) use ($args, $optionalParams) { if (isset($args[ $matches[1] ])) { - return $args[ $matches[1] ]; + return json_encode($args[ $matches[1] ]); + } + if (isset($optionalParams[ $matches[1] ])) { + return json_encode($optionalParams[ $matches[1] ]); } - return ''; + return '""'; // The same as `json_encode('')`. }, $code ); diff --git a/inc/Data/ThirdPartyHtmlAttributes.php b/inc/Data/ThirdPartyHtmlAttributes.php index 4722934..051d7d9 100644 --- a/inc/Data/ThirdPartyHtmlAttributes.php +++ b/inc/Data/ThirdPartyHtmlAttributes.php @@ -9,6 +9,7 @@ namespace GoogleChromeLabs\ThirdPartyCapital\Data; +use GoogleChromeLabs\ThirdPartyCapital\Contracts\Arrayable; use GoogleChromeLabs\ThirdPartyCapital\Util\HtmlAttributes; /** @@ -22,7 +23,7 @@ class ThirdPartyHtmlAttributes extends HtmlAttributes * * @param string $name Attribute name. * @param mixed $value Attribute value. - * @return mixed Sanitized attribute value. + * @return string|bool|null|Arrayable Sanitized attribute value. */ protected function sanitizeAttr(string $name, $value) { @@ -36,8 +37,8 @@ protected function sanitizeAttr(string $name, $value) /** * Returns the attribute string for the given attribute name and value. * - * @param string $name Attribute name. - * @param mixed $value Attribute value. + * @param string $name Attribute name. + * @param string|bool|null|Arrayable $value Attribute value. * @return string HTML attribute string (starts with a space), or empty string to skip. */ protected function toAttrString(string $name, $value): string diff --git a/inc/Data/ThirdPartyHtmlData.php b/inc/Data/ThirdPartyHtmlData.php index d956f7b..182ed7b 100644 --- a/inc/Data/ThirdPartyHtmlData.php +++ b/inc/Data/ThirdPartyHtmlData.php @@ -80,9 +80,6 @@ private function validateData(array $htmlData): void if (!isset($htmlData['attributes'])) { throw new InvalidThirdPartyDataException('Missing HTML attributes.'); } - if (!isset($htmlData['attributes']['src'])) { - throw new InvalidThirdPartyDataException('Missing HTML src attribute.'); - } } /** diff --git a/inc/Data/ThirdPartyScriptData.php b/inc/Data/ThirdPartyScriptData.php index 5d6f036..1ed8484 100644 --- a/inc/Data/ThirdPartyScriptData.php +++ b/inc/Data/ThirdPartyScriptData.php @@ -76,6 +76,13 @@ class ThirdPartyScriptData implements Arrayable */ private $params; + /** + * Optional parameters for the script and their defaults, if needed. + * + * @var array + */ + private $optionalParams; + /** * Constructor. * @@ -159,6 +166,16 @@ public function getParams(): array return $this->params; } + /** + * Gets the optional parameters for the script with their defaults, if needed. + * + * @return array Optional parameters for the script, if needed. + */ + public function getOptionalParams(): array + { + return $this->optionalParams; + } + /** * Determines whether the script is an external script. * @@ -192,6 +209,9 @@ public function toArray(): array if ($this->params) { $data['params'] = $this->params; } + if ($this->optionalParams) { + $data['optionalParams'] = $this->optionalParams; + } return $data; } @@ -278,6 +298,7 @@ private function setData(array $scriptData): void $this->$field = isset($scriptData[ $field ]) ? (string) $scriptData[ $field ] : ''; } - $this->params = isset($scriptData['params']) ? array_map('strval', $scriptData['params']) : []; + $this->params = isset($scriptData['params']) ? array_map('strval', $scriptData['params']) : []; + $this->optionalParams = isset($scriptData['optionalParams']) ? (array) $scriptData['optionalParams'] : []; } } diff --git a/inc/ThirdParties/GoogleTagManager.php b/inc/ThirdParties/GoogleTagManager.php new file mode 100644 index 0000000..19d66a5 --- /dev/null +++ b/inc/ThirdParties/GoogleTagManager.php @@ -0,0 +1,29 @@ + $args Input arguments to set. */ - public function __construct(array $args) + final public function __construct(array $args) { $this->jsonFilePath = $this->getJsonFilePath(); @@ -65,7 +65,7 @@ public function __construct(array $args) * * @return string Third party identifier. */ - public function getId(): string + final public function getId(): string { $this->lazilyInitialize(); @@ -77,7 +77,7 @@ public function getId(): string * * @param array $args Input arguments to set. */ - public function setArgs(array $args): void + final public function setArgs(array $args): void { $this->args = $args; @@ -92,7 +92,7 @@ public function setArgs(array $args): void * * @return string HTML output, or empty string if not applicable. */ - public function getHtml(): string + final public function getHtml(): string { $this->lazilyInitialize(); @@ -106,7 +106,7 @@ public function getHtml(): string * * @return string[] List of stylesheet URLs, or empty array if not applicable. */ - public function getStylesheets(): array + final public function getStylesheets(): array { $this->lazilyInitialize(); @@ -120,7 +120,7 @@ public function getStylesheets(): array * * @return ThirdPartyScriptOutput[] List of script definition objects, or empty array if not applicable. */ - public function getScripts(): array + final public function getScripts(): array { $this->lazilyInitialize(); diff --git a/inc/Util/HtmlAttributes.php b/inc/Util/HtmlAttributes.php index cb6ce05..2b9d953 100644 --- a/inc/Util/HtmlAttributes.php +++ b/inc/Util/HtmlAttributes.php @@ -19,8 +19,8 @@ /** * Class representing a set of HTML Attributes. * - * @implements ArrayAccess - * @implements IteratorAggregate + * @implements ArrayAccess + * @implements IteratorAggregate */ class HtmlAttributes implements Arrayable, ArrayAccess, IteratorAggregate { @@ -28,7 +28,7 @@ class HtmlAttributes implements Arrayable, ArrayAccess, IteratorAggregate /** * Internal attributes storage. * - * @var array + * @var array */ private $attr = []; @@ -64,7 +64,7 @@ public function offsetExists($name) * @since n.e.x.t * * @param string $name Attribute name. - * @return mixed Value for the given attribute. + * @return string|bool|null|Arrayable Value for the given attribute. * * @throws NotFoundException Thrown if the attribute is not set. */ @@ -115,7 +115,7 @@ public function offsetUnset($name) * * @since n.e.x.t * - * @return ArrayIterator Attributes iterator. + * @return ArrayIterator Attributes iterator. */ public function getIterator(): Traversable { @@ -159,11 +159,11 @@ public function __toString(): string * * @param string $name Attribute name. * @param mixed $value Attribute value. - * @return mixed Sanitized attribute value. + * @return string|bool|null|Arrayable Sanitized attribute value. */ protected function sanitizeAttr(string $name, $value) { - if (is_bool($value)) { + if (is_bool($value) || is_null($value)) { return $value; } return (string) $value; @@ -172,12 +172,16 @@ protected function sanitizeAttr(string $name, $value) /** * Returns the attribute string for the given attribute name and value. * - * @param string $name Attribute name. - * @param mixed $value Attribute value. + * @param string $name Attribute name. + * @param string|bool|null|Arrayable $value Attribute value. * @return string HTML attribute string (starts with a space), or empty string to skip. */ protected function toAttrString(string $name, $value): string { + if (is_null($value)) { + return ''; + } + if (is_bool($value)) { return $value ? ' ' . $name : ''; } diff --git a/tests/phpunit/tests/Data/ThirdPartyDataFormatterTest.php b/tests/phpunit/tests/Data/ThirdPartyDataFormatterTest.php index f5a4719..749bf84 100644 --- a/tests/phpunit/tests/Data/ThirdPartyDataFormatterTest.php +++ b/tests/phpunit/tests/Data/ThirdPartyDataFormatterTest.php @@ -333,11 +333,11 @@ public function testFormatUrlWithQueryAndSlugParamAndParamsAndArgs() public function testFormatCodeWithoutArgs() { $code = ThirdPartyDataFormatter::formatCode( - 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});', + 'document.querySelector({{selector}}).addEventListener(api[{{callback}}]);', [] ); $this->assertSame( - 'document.querySelector("").addEventListener(api.);', + 'document.querySelector("").addEventListener(api[""]);', $code ); } @@ -345,14 +345,14 @@ public function testFormatCodeWithoutArgs() public function testFormatCodeWithArgs() { $code = ThirdPartyDataFormatter::formatCode( - 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});', + 'document.querySelector({{selector}}).addEventListener(api[{{callback}}]);', [ 'selector' => '.my-cta-button', 'callback' => 'addToCart', ] ); $this->assertSame( - 'document.querySelector(".my-cta-button").addEventListener(api.addToCart);', + 'document.querySelector(".my-cta-button").addEventListener(api["addToCart"]);', $code ); } @@ -360,7 +360,7 @@ public function testFormatCodeWithArgs() public function testFormatCodeWithArgsIncorrectOrderAndTooMany() { $code = ThirdPartyDataFormatter::formatCode( - 'document.querySelector("{{selector}}").addEventListener(api.{{callback}});', + 'document.querySelector({{selector}}).addEventListener(api[{{callback}}]);', [ 'callback' => 'addToCart', 'device' => 'phone', @@ -368,7 +368,7 @@ public function testFormatCodeWithArgsIncorrectOrderAndTooMany() ] ); $this->assertSame( - 'document.querySelector(".my-cta-button").addEventListener(api.addToCart);', + 'document.querySelector(".my-cta-button").addEventListener(api["addToCart"]);', $code ); } diff --git a/tests/phpunit/tests/ThirdParties/GoogleAnalyticsTest.php b/tests/phpunit/tests/ThirdParties/GoogleAnalyticsTest.php new file mode 100644 index 0000000..fb2bdc4 --- /dev/null +++ b/tests/phpunit/tests/ThirdParties/GoogleAnalyticsTest.php @@ -0,0 +1,90 @@ +assertSame('google-analytics', $ga->getId()); + $this->assertSame('', $ga->getHtml()); + $this->assertSame([], $ga->getStylesheets()); + $this->assertSame( + $expectedScripts, + array_map( + static function ($script) { + return $script->toArray(); + }, + $ga->getScripts() + ) + ); + } + + public function dataOutput(): array + { + $consentDefault = '{"ad_user_data":"denied","ad_personalization":"denied","ad_storage":"denied","analytics_storage":"denied","wait_for_update":500}'; + return [ + 'basic example' => [ + [ 'id' => 'G-12345678' ], + [ + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'url' => 'https://www.googletagmanager.com/gtag/js?id=G-12345678', + 'key' => 'gtag', + ], + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'code' => "window[\"dataLayer\"]=window[\"dataLayer\"]||[];window['gtag-'+\"dataLayer\"]=function (){window[\"dataLayer\"].push(arguments);};window['gtag-'+\"dataLayer\"]('consent', \"default\", {$consentDefault});window['gtag-'+\"dataLayer\"]('js',new Date());window['gtag-'+\"dataLayer\"]('config',\"G-12345678\")", + 'key' => 'setup', + ], + ], + ], + 'with custom data layer' => [ + [ + 'id' => 'G-13579', + 'l' => 'myDataLayer1', + ], + [ + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'url' => 'https://www.googletagmanager.com/gtag/js?id=G-13579&l=myDataLayer1', + 'key' => 'gtag', + ], + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'code' => "window[\"myDataLayer1\"]=window[\"myDataLayer1\"]||[];window['gtag-'+\"myDataLayer1\"]=function (){window[\"myDataLayer1\"].push(arguments);};window['gtag-'+\"myDataLayer1\"]('consent', \"default\", {$consentDefault});window['gtag-'+\"myDataLayer1\"]('js',new Date());window['gtag-'+\"myDataLayer1\"]('config',\"G-13579\")", + 'key' => 'setup', + ], + ], + ], + ]; + } +} diff --git a/tests/phpunit/tests/ThirdParties/GoogleMapsEmbedTest.php b/tests/phpunit/tests/ThirdParties/GoogleMapsEmbedTest.php new file mode 100644 index 0000000..ea7ea03 --- /dev/null +++ b/tests/phpunit/tests/ThirdParties/GoogleMapsEmbedTest.php @@ -0,0 +1,75 @@ +assertSame('google-maps-embed', $gme->getId()); + $this->assertSame($expectedHtml, $gme->getHtml()); + $this->assertSame([], $gme->getStylesheets()); + $this->assertSame([], $gme->getScripts()); + } + + public function dataOutput(): array + { + return [ + 'basic example' => [ + [ + 'key' => 'MY_API_KEY', + 'q' => 'Space Needle, Seattle WA', + ], + $this->getHtmlString( + 'iframe', + [ + 'loading' => 'lazy', + 'src' => 'https://www.google.com/maps/embed/v1/place?key=MY_API_KEY&q=Space+Needle%2C+Seattle+WA', + 'referrerpolicy' => 'no-referrer-when-downgrade', + 'frameborder' => '0', + 'style' => 'border:0', + 'allowfullscreen' => true, + ] + ), + ], + 'with custom mode' => [ + [ + 'mode' => 'search', + 'key' => 'MY_API_KEY', + 'q' => 'tourist attractions in Seattle', + 'maptype' => 'satellite', + ], + $this->getHtmlString( + 'iframe', + [ + 'loading' => 'lazy', + 'src' => 'https://www.google.com/maps/embed/v1/search?key=MY_API_KEY&q=tourist+attractions+in+Seattle&maptype=satellite', + 'referrerpolicy' => 'no-referrer-when-downgrade', + 'frameborder' => '0', + 'style' => 'border:0', + 'allowfullscreen' => true, + ] + ), + ], + ]; + } +} diff --git a/tests/phpunit/tests/ThirdParties/GoogleTagManagerTest.php b/tests/phpunit/tests/ThirdParties/GoogleTagManagerTest.php new file mode 100644 index 0000000..aec448d --- /dev/null +++ b/tests/phpunit/tests/ThirdParties/GoogleTagManagerTest.php @@ -0,0 +1,89 @@ +assertSame('google-tag-manager', $gtm->getId()); + $this->assertSame('', $gtm->getHtml()); + $this->assertSame([], $gtm->getStylesheets()); + $this->assertSame( + $expectedScripts, + array_map( + static function ($script) { + return $script->toArray(); + }, + $gtm->getScripts() + ) + ); + } + + public function dataOutput(): array + { + return [ + 'basic example' => [ + [ 'id' => 'GTM-12345678' ], + [ + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'url' => 'https://www.googletagmanager.com/gtm.js?id=GTM-12345678', + 'key' => 'gtm', + ], + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'code' => "window[\"dataLayer\"]=window[\"dataLayer\"]||[];window[\"dataLayer\"].push({'gtm.start':new Date().getTime(),event:'gtm.js'});", + 'key' => 'setup', + ], + ], + ], + 'with custom data layer' => [ + [ + 'id' => 'GTM-A1B2C3', + 'l' => 'myDataLayer1', + ], + [ + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'url' => 'https://www.googletagmanager.com/gtm.js?id=GTM-A1B2C3&l=myDataLayer1', + 'key' => 'gtm', + ], + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_WORKER, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'code' => "window[\"myDataLayer1\"]=window[\"myDataLayer1\"]||[];window[\"myDataLayer1\"].push({'gtm.start':new Date().getTime(),event:'gtm.js'});", + 'key' => 'setup', + ], + ], + ], + ]; + } +} diff --git a/tests/phpunit/tests/ThirdParties/YouTubeEmbedTest.php b/tests/phpunit/tests/ThirdParties/YouTubeEmbedTest.php new file mode 100644 index 0000000..67cc12e --- /dev/null +++ b/tests/phpunit/tests/ThirdParties/YouTubeEmbedTest.php @@ -0,0 +1,72 @@ +assertSame('youtube-embed', $yte->getId()); + $this->assertSame($expectedHtml, $yte->getHtml()); + $this->assertSame( + ['https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@master/src/lite-yt-embed.css'], + $yte->getStylesheets() + ); + $this->assertSame( + [ + [ + 'strategy' => ThirdPartyScriptData::STRATEGY_IDLE, + 'location' => ThirdPartyScriptData::LOCATION_HEAD, + 'action' => ThirdPartyScriptData::ACTION_APPEND, + 'url' => 'https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@master/src/lite-yt-embed.js', + 'key' => 'lite-yt-embed', + ], + ], + array_map( + static function ($script) { + return $script->toArray(); + }, + $yte->getScripts() + ) + ); + } + + public function dataOutput(): array + { + return [ + 'basic example' => [ + [ + 'videoid' => 'ogfYd705cRs', + 'playlabel' => 'Play: Keynote (Google I/O 2018)', + ], + $this->getHtmlString( + 'lite-youtube', + [ + 'videoid' => 'ogfYd705cRs', + 'playlabel' => 'Play: Keynote (Google I/O 2018)', + ] + ), + ], + ]; + } +} diff --git a/tests/phpunit/tests/Util/HtmlAttributesTest.php b/tests/phpunit/tests/Util/HtmlAttributesTest.php index 7e039f1..5338537 100644 --- a/tests/phpunit/tests/Util/HtmlAttributesTest.php +++ b/tests/phpunit/tests/Util/HtmlAttributesTest.php @@ -140,6 +140,15 @@ public function dataToString() ], ' id="unique-id" async class="demo-class"', ], + 'with null' => [ + [ + 'id' => 'some-id', + 'width' => null, + 'height' => null, + 'class' => 'demo-class', + ], + ' id="some-id" class="demo-class"', + ], ]; } } diff --git a/tests/phpunit/utils/TestCase.php b/tests/phpunit/utils/TestCase.php index 0cfebcb..285bf7b 100644 --- a/tests/phpunit/utils/TestCase.php +++ b/tests/phpunit/utils/TestCase.php @@ -16,6 +16,28 @@ abstract class TestCase extends PHPUnitTestCase { + /** + * Test helper to turn a HTML element name and attributes array into a string. + * + * @param string $element HTML element name. + * @param array $attributes Associative array of HTML attributes. + * @return string The resulting HTML string. + */ + protected function getHtmlString(string $element, array $attributes): string + { + $attr_string = ''; + foreach ($attributes as $key => $value) { + if (is_bool($value)) { + if ($value) { + $attr_string .= ' ' . $key; + } + continue; + } + $attr_string .= ' ' . $key . '="' . $value . '"'; + } + return '<' . $element . $attr_string . '>'; + } + protected function runGetterTestCase(string $className, string $getMethod, array $args, $expected) { if (is_subclass_of($expected, Exception::class)) {