diff --git a/lib/block-supports/spacing.php b/lib/block-supports/spacing.php index 86bb96598af44..d2530293d2453 100644 --- a/lib/block-supports/spacing.php +++ b/lib/block-supports/spacing.php @@ -52,7 +52,7 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { return $attributes; } - $style_engine = WP_Style_Engine_Gutenberg::get_instance(); + $style_engine = gutenberg_get_style_engine(); $skip_padding = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'padding' ); $skip_margin = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'margin' ); $spacing_block_styles = array(); diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 9661277c1c7b9..68ed7e6413079 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -92,117 +92,139 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { $has_text_decoration_support = _wp_array_get( $typography_supports, array( '__experimentalTextDecoration' ), false ); $has_text_transform_support = _wp_array_get( $typography_supports, array( '__experimentalTextTransform' ), false ); - if ( $has_font_size_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontSize' ) ) { + // Whether to skip individual block support features. + $should_skip_font_size = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontSize' ); + $should_skip_font_family = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontFamily' ); + $should_skip_font_style = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontStyle' ); + $should_skip_font_weight = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontWeight' ); + $should_skip_line_height = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'lineHeight' ); + $should_skip_text_decoration = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textDecoration' ); + $should_skip_text_transform = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textTransform' ); + $should_skip_letter_spacing = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'letterSpacing' ); + + if ( $has_font_size_support && ! $should_skip_font_size ) { $has_named_font_size = array_key_exists( 'fontSize', $block_attributes ); $has_custom_font_size = isset( $block_attributes['style']['typography']['fontSize'] ); if ( $has_named_font_size ) { - $classes[] = sprintf( 'has-%s-font-size', _wp_to_kebab_case( $block_attributes['fontSize'] ) ); + $classes['fontSize'] = $block_attributes['fontSize']; } elseif ( $has_custom_font_size ) { - $styles[] = sprintf( 'font-size: %s;', $block_attributes['style']['typography']['fontSize'] ); + $styles['fontSize'] = $block_attributes['style']['typography']['fontSize']; } } - if ( $has_font_family_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontFamily' ) ) { + if ( $has_font_family_support && ! $should_skip_font_family ) { $has_named_font_family = array_key_exists( 'fontFamily', $block_attributes ); $has_custom_font_family = isset( $block_attributes['style']['typography']['fontFamily'] ); if ( $has_named_font_family ) { - $classes[] = sprintf( 'has-%s-font-family', _wp_to_kebab_case( $block_attributes['fontFamily'] ) ); + $classes['fontFamily'] = $block_attributes['fontFamily']; } elseif ( $has_custom_font_family ) { - // Before using classes, the value was serialized as a CSS Custom Property. - // We don't need this code path when it lands in core. $font_family_custom = $block_attributes['style']['typography']['fontFamily']; - if ( strpos( $font_family_custom, 'var:preset|font-family' ) !== false ) { - $index_to_splice = strrpos( $font_family_custom, '|' ) + 1; - $font_family_slug = _wp_to_kebab_case( substr( $font_family_custom, $index_to_splice ) ); - $font_family_custom = sprintf( 'var(--wp--preset--font-family--%s)', $font_family_slug ); - } - $styles[] = sprintf( 'font-family: %s;', $font_family_custom ); + // Before using classes, the value was serialized as a CSS Custom Property. + // We don't need to check for a preset when it lands in core. + $font_family_value = gutenberg_typography_get_preset_inline_style_value( $font_family_custom, 'font-family' ); + $styles['fontFamily'] = $font_family_value; } } - if ( $has_font_style_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontStyle' ) ) { - $font_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontStyle', 'font-style' ); - if ( $font_style ) { - $styles[] = $font_style; + if ( $has_font_style_support && ! $should_skip_font_style ) { + $font_style = _wp_array_get( $block_attributes, array( 'style', 'typography', 'fontStyle' ), null ); + $font_style_value = gutenberg_typography_get_preset_inline_style_value( $font_style, 'font-style' ); + if ( $font_style_value ) { + $styles['fontStyle'] = $font_style_value; } } - if ( $has_font_weight_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontWeight' ) ) { - $font_weight = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontWeight', 'font-weight' ); - if ( $font_weight ) { - $styles[] = $font_weight; + if ( $has_font_weight_support && ! $should_skip_font_weight ) { + $font_weight = _wp_array_get( $block_attributes, array( 'style', 'typography', 'fontWeight' ), null ); + $font_weight_value = gutenberg_typography_get_preset_inline_style_value( $font_weight, 'font-weight' ); + if ( $font_weight_value ) { + $styles['fontWeight'] = $font_weight_value; } } - if ( $has_line_height_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'lineHeight' ) ) { + if ( $has_line_height_support && ! $should_skip_line_height ) { $has_line_height = isset( $block_attributes['style']['typography']['lineHeight'] ); if ( $has_line_height ) { - $styles[] = sprintf( 'line-height: %s;', $block_attributes['style']['typography']['lineHeight'] ); + $styles['lineHeight'] = $block_attributes['style']['typography']['lineHeight']; } } - if ( $has_text_decoration_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textDecoration' ) ) { - $text_decoration_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textDecoration', 'text-decoration' ); - if ( $text_decoration_style ) { - $styles[] = $text_decoration_style; + if ( $has_text_decoration_support && ! $should_skip_text_decoration ) { + $text_decoration = _wp_array_get( $block_attributes, array( 'style', 'typography', 'textDecoration' ), null ); + $text_decoration_value = gutenberg_typography_get_preset_inline_style_value( $text_decoration, 'text-decoration' ); + if ( $text_decoration_value ) { + $styles['textDecoration'] = $text_decoration_value; } } - if ( $has_text_transform_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textTransform' ) ) { - $text_transform_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textTransform', 'text-transform' ); - if ( $text_transform_style ) { - $styles[] = $text_transform_style; + if ( $has_text_transform_support && ! $should_skip_text_transform ) { + $text_transform = _wp_array_get( $block_attributes, array( 'style', 'typography', 'textTransform' ), null ); + $text_transform_value = gutenberg_typography_get_preset_inline_style_value( $text_transform, 'text-transform' ); + if ( $text_transform_value ) { + $styles['textTransform'] = $text_transform_value; } } - if ( $has_letter_spacing_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'letterSpacing' ) ) { - $letter_spacing_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'letterSpacing', 'letter-spacing' ); - if ( $letter_spacing_style ) { - $styles[] = $letter_spacing_style; + if ( $has_letter_spacing_support && ! $should_skip_letter_spacing ) { + $letter_spacing = _wp_array_get( $block_attributes, array( 'style', 'typography', 'letterSpacing' ), null ); + $letter_spacing_value = gutenberg_typography_get_preset_inline_style_value( $letter_spacing, 'letter-spacing' ); + if ( $letter_spacing_value ) { + $styles['letterSpacing'] = $letter_spacing_value; } } - if ( ! empty( $classes ) ) { - $attributes['class'] = implode( ' ', $classes ); + $style_engine = gutenberg_get_style_engine(); + $inline_styles = $style_engine->generate( + array( 'typography' => $styles ), + array( + 'inline' => true, + ) + ); + + $classnames = $style_engine->get_classnames( + array( 'typography' => $classes ) + ); + + if ( ! empty( $classnames ) ) { + $attributes['class'] = $classnames; } - if ( ! empty( $styles ) ) { - $attributes['style'] = implode( ' ', $styles ); + + if ( ! empty( $inline_styles ) ) { + $attributes['style'] = $inline_styles; } return $attributes; } /** - * Generates an inline style for a typography feature e.g. text decoration, + * Generates an inline style value for a typography feature e.g. text decoration, * text transform, and font style. * - * @param array $attributes Block's attributes. - * @param string $feature Key for the feature within the typography styles. - * @param string $css_property Slug for the CSS property the inline style sets. + * @param string $style_value A raw style value for a single typography feature from a block's style attribute. + * @param string $css_property Slug for the CSS property the inline style sets. * - * @return string CSS inline style. + * @return string? A CSS inline style value. */ -function gutenberg_typography_get_css_variable_inline_style( $attributes, $feature, $css_property ) { - // Retrieve current attribute value or skip if not found. - $style_value = _wp_array_get( $attributes, array( 'style', 'typography', $feature ), false ); - if ( ! $style_value ) { - return; - } - - // If we don't have a preset CSS variable, we'll assume it's a regular CSS value. - if ( strpos( $style_value, "var:preset|{$css_property}|" ) === false ) { - return sprintf( '%s:%s;', $css_property, $style_value ); +function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_property ) { + // If the style value is not a preset CSS variable go no further. + if ( empty( $style_value ) || strpos( $style_value, "var:preset|{$css_property}|" ) === false ) { + return $style_value; } + // For backwards compatibility. + // Presets were removed in https://github.com/WordPress/gutenberg/pull/27555. // We have a preset CSS variable as the style. // Get the style value from the string and return CSS style. $index_to_splice = strrpos( $style_value, '|' ) + 1; - $slug = substr( $style_value, $index_to_splice ); + // @TODO + // Font family requires the slugs to be converted to kebab case. Should this be optional in this method? + // Let's test with some older blocks. + $slug = _wp_to_kebab_case( substr( $style_value, $index_to_splice ) ); - // Return the actual CSS inline style e.g. `text-decoration:var(--wp--preset--text-decoration--underline);`. - return sprintf( '%s:var(--wp--preset--%s--%s);', $css_property, $css_property, $slug ); + // Return the actual CSS inline style value e.g. `var(--wp--preset--text-decoration--underline);`. + return sprintf( 'var(--wp--preset--%s--%s);', $css_property, $slug ); } // Register the block support. @@ -213,5 +235,3 @@ function gutenberg_typography_get_css_variable_inline_style( $attributes, $featu 'apply' => 'gutenberg_apply_typography_support', ) ); - - diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index c07075184c41b..148b208c51c25 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -36,16 +36,50 @@ class WP_Style_Engine { * For example, `'padding' => 'array( 'top' => '1em' )` will return `array( 'padding-top' => '1em' )` */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( - 'spacing' => array( + 'spacing' => array( 'padding' => array( 'property_key' => 'padding', 'path' => array( 'spacing', 'padding' ), - 'value_func' => 'static::get_css_box_rules', ), 'margin' => array( 'property_key' => 'margin', 'path' => array( 'spacing', 'margin' ), - 'value_func' => 'static::get_css_box_rules', + ), + ), + 'typography' => array( + 'fontSize' => array( + 'property_key' => 'font-size', + 'path' => array( 'typography', 'fontSize' ), + 'classname_schema' => 'has-%s-font-size', + ), + 'fontFamily' => array( + 'property_key' => 'font-family', + 'path' => array( 'typography', 'fontFamily' ), + 'classname_schema' => 'has-%s-font-family', + ), + 'fontStyle' => array( + 'property_key' => 'font-style', + 'path' => array( 'typography', 'fontStyle' ), + ), + 'fontWeight' => array( + 'property_key' => 'font-weight', + 'path' => array( 'typography', 'fontWeight' ), + ), + 'lineHeight' => array( + 'property_key' => 'line-height', + 'path' => array( 'typography', 'lineHeight' ), + ), + 'textDecoration' => array( + 'property_key' => 'text-decoration', + 'path' => array( 'typography', 'textDecoration' ), + ), + 'textTransform' => array( + 'property_key' => 'text-transform', + 'path' => array( 'typography', 'textTransform' ), + ), + 'letterSpacing' => array( + 'property_key' => 'letter-spacing', + 'path' => array( 'typography', 'letterSpacing' ), ), ), ); @@ -77,17 +111,57 @@ protected function get_block_style_css_rules( $style_value, $path ) { $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $path, null ); if ( ! empty( $style_definition ) ) { + // Style definitions can define a function to generate custom CSS rules. if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { return call_user_func( $style_definition['value_func'], $style_value, $style_definition['property_key'] ); + } else { + return static::get_css_rules( $style_value, $style_definition['property_key'] ); } } return array(); } + /** + * Returns a classname built using a provided schema. + * + * @param array $block_styles An array of styles from a block's attributes. + * Some values may contain slugs that need to be parsed using a schema. + * + * @return string A CSS classname. + */ + public function get_classnames( $block_styles ) { + $output = ''; + + if ( empty( $block_styles ) ) { + return $output; + } + + $classnames = array(); + foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) { + foreach ( $definition_group as $style_definition ) { + $classname_value = _wp_array_get( $block_styles, $style_definition['path'], null ); + + if ( empty( $classname_value ) ) { + continue; + } + + $classname_value = _wp_to_kebab_case( $classname_value ); + $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition['path'], null ); + // Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA. + // One day, if there are no stored schemata, we could allow custom patterns or + // generate classnames based on other properties + // such as a path or a value or a prefix passed in options. + $classnames[] = isset( $style_definition['classname_schema'] ) ? sprintf( $style_definition['classname_schema'], $classname_value ) : $classname_value; + } + } + + return implode( ' ', $classnames ); + } + /** * Returns an CSS ruleset. * Styles are bundled based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. @@ -109,23 +183,13 @@ public function generate( $block_styles, $options = array() ) { $rules = array(); - // If a path to a specific block style is defined, only return rules for that style. - if ( isset( $options['path'] ) && is_array( $options['path'] ) ) { - $style_value = _wp_array_get( $block_styles, $options['path'], null ); - if ( empty( $style_value ) ) { - return $output; - } - $rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $options['path'] ) ); - } else { - // Otherwise build them all. - foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) { - foreach ( $definition_group as $style_definition ) { - $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); - if ( empty( $style_value ) ) { - continue; - } - $rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $style_definition['path'] ) ); + foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) { + foreach ( $definition_group as $style_definition ) { + $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); + if ( empty( $style_value ) ) { + continue; } + $rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $style_definition['path'] ) ); } } @@ -133,32 +197,35 @@ public function generate( $block_styles, $options = array() ) { // Generate inline style rules. if ( isset( $options['inline'] ) && true === $options['inline'] ) { foreach ( $rules as $rule => $value ) { - $filtered_css = esc_html( safecss_filter_attr( "{$rule}:{$value}" ) ); + $filtered_css = esc_html( safecss_filter_attr( "{$rule}: {$value}" ) ); if ( ! empty( $filtered_css ) ) { - $output .= $filtered_css . ';'; + $output .= $filtered_css . '; '; } } } } - return $output; + return trim( $output ); } /** - * Returns a CSS ruleset for box model styles such as margins, padding, and borders. + * Default style value parser that returns a CSS ruleset. + * If the input contains an array, it will treated like a box model + * for styles such as margins, padding, and borders * * @param string|array $style_value A single raw Gutenberg style attributes value for a CSS property. * @param string $style_property The CSS property for which we're creating a rule. * * @return array The class name for the added style. */ - public static function get_css_box_rules( $style_value, $style_property ) { + public static function get_css_rules( $style_value, $style_property ) { $rules = array(); if ( ! $style_value ) { return $rules; } + // We assume box model-like properties. if ( is_array( $style_value ) ) { foreach ( $style_value as $key => $value ) { $rules[ "$style_property-$key" ] = $value; diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/class-wp-style-engine-test.php index 2e0e9afe17bfc..be3c7630013d6 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -13,12 +13,12 @@ */ class WP_Style_Engine_Test extends WP_UnitTestCase { /** - * Tests various manifestations of the $block_styles argument. + * Tests generating styles based on various manifestations of the $block_styles argument. * - * @dataProvider data_block_styles_fixtures + * @dataProvider data_generate_css_fixtures */ function test_generate_css( $block_styles, $options, $expected_output ) { - $style_engine = WP_Style_Engine::get_instance(); + $style_engine = wp_get_style_engine(); $generated_styles = $style_engine->generate( $block_styles, $options @@ -31,7 +31,7 @@ function test_generate_css( $block_styles, $options, $expected_output ) { * * @return array */ - public function data_block_styles_fixtures() { + public function data_generate_css_fixtures() { return array( 'default_return_value' => array( 'block_styles' => array(), @@ -42,7 +42,6 @@ public function data_block_styles_fixtures() { 'inline_invalid_block_styles_empty' => array( 'block_styles' => array(), 'options' => array( - 'path' => array( 'spacing', 'padding' ), 'inline' => true, ), 'expected_output' => '', @@ -63,7 +62,6 @@ public function data_block_styles_fixtures() { 'pageBreakAfter' => 'verso', ), 'options' => array( - 'path' => array( 'pageBreakAfter', 'verso' ), 'inline' => true, ), 'expected_output' => '', @@ -76,38 +74,24 @@ public function data_block_styles_fixtures() { ), ), 'options' => array( - 'path' => array( 'spacing', 'padding' ), 'inline' => true, ), 'expected_output' => '', ), - 'inline_invalid_multiple_style_unknown_property' => array( - 'block_styles' => array( - 'spacing' => array( - 'gavin' => '1000vw', - ), - ), - 'options' => array( - 'inline' => true, - ), - 'expected_output' => '', - ), - - 'inline_valid_single_style_string' => array( + 'inline_valid_style_string' => array( 'block_styles' => array( 'spacing' => array( 'margin' => '111px', ), ), 'options' => array( - 'path' => array( 'spacing', 'margin' ), 'inline' => true, ), - 'expected_output' => 'margin:111px;', + 'expected_output' => 'margin: 111px;', ), - 'inline_valid_single_style' => array( + 'inline_valid_box_model_style' => array( 'block_styles' => array( 'spacing' => array( 'padding' => array( @@ -125,33 +109,69 @@ public function data_block_styles_fixtures() { ), ), 'options' => array( - 'path' => array( 'spacing', 'padding' ), 'inline' => true, ), - 'expected_output' => 'padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;', + 'expected_output' => 'padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; margin-top: 12rem; margin-left: 2vh; margin-bottom: 2px; margin-right: 10em;', ), - 'inline_valid_multiple_style' => array( + 'inline_valid_typography_style' => array( 'block_styles' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '42px', - 'left' => '2%', - 'bottom' => '44px', - 'right' => '5rem', - ), - 'margin' => array( - 'top' => '12rem', - 'left' => '2vh', - 'bottom' => '2px', - 'right' => '10em', - ), + 'typography' => array( + 'fontSize' => 'clamp(2em, 2vw, 4em)', + 'fontFamily' => 'Roboto,Oxygen-Sans,Ubuntu,sans-serif', + 'fontStyle' => 'italic', + 'fontWeight' => '800', + 'lineHeight' => '1.3', + 'textDecoration' => 'underline', + 'textTransform' => 'uppercase', + 'letterSpacing' => '2', ), ), 'options' => array( 'inline' => true, ), - 'expected_output' => 'padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;margin-top:12rem;margin-left:2vh;margin-bottom:2px;margin-right:10em;', + 'expected_output' => 'font-family: Roboto,Oxygen-Sans,Ubuntu,sans-serif; font-style: italic; font-weight: 800; line-height: 1.3; text-decoration: underline; text-transform: uppercase; letter-spacing: 2;', + ), + ); + } + + /** + * Tests generating classnames based on various manifestations of the $block_styles argument. + * + * @dataProvider data_get_classnames_fixtures + */ + function test_get_classnames( $block_styles, $options, $expected_output ) { + $style_engine = wp_get_style_engine(); + $generated_styles = $style_engine->get_classnames( + $block_styles, + $options + ); + $this->assertSame( $expected_output, $generated_styles ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_classnames_fixtures() { + return array( + 'default_return_value' => array( + 'block_styles' => array(), + 'options' => null, + 'expected_output' => '', + ), + 'valid_classnames_use_schema' => array( + 'block_styles' => array( + 'typography' => array( + 'fontSize' => 'fantastic', + 'fontFamily' => 'totally-awesome', + ), + ), + 'options' => array( + 'use_schema' => true, + ), + 'expected_output' => 'has-fantastic-font-size has-totally-awesome-font-family', ), ); } diff --git a/phpunit/block-supports/spacing-test.php b/phpunit/block-supports/spacing-test.php index 618c809b84f6a..613f415557cfd 100644 --- a/phpunit/block-supports/spacing-test.php +++ b/phpunit/block-supports/spacing-test.php @@ -62,7 +62,7 @@ function test_spacing_style_is_applied() { $actual = gutenberg_apply_spacing_support( $block_type, $block_atts ); $expected = array( - 'style' => 'padding:111px;margin-top:1px;margin-right:2px;margin-bottom:3px;margin-left:4px;', + 'style' => 'padding: 111px; margin-top: 1px; margin-right: 2px; margin-bottom: 3px; margin-left: 4px;', ); $this->assertSame( $expected, $actual ); @@ -152,7 +152,7 @@ function test_margin_with_individual_skipped_serialization_block_supports() { $actual = gutenberg_apply_spacing_support( $block_type, $block_atts ); $expected = array( - 'style' => 'padding-top:1px;padding-right:2px;padding-bottom:3px;padding-left:4px;', + 'style' => 'padding-top: 1px; padding-right: 2px; padding-bottom: 3px; padding-left: 4px;', ); $this->assertSame( $expected, $actual );