Skip to content

Gutenberg Backport - Block level custom CSS#10777

Open
glendaviesnz wants to merge 12 commits intoWordPress:trunkfrom
glendaviesnz:trac-64544
Open

Gutenberg Backport - Block level custom CSS#10777
glendaviesnz wants to merge 12 commits intoWordPress:trunkfrom
glendaviesnz:trac-64544

Conversation

@glendaviesnz
Copy link

@glendaviesnz glendaviesnz commented Jan 23, 2026

This is a backport of WordPress/gutenberg#73959 which adds block level custom CSS support.

Trac ticket: https://core.trac.wordpress.org/ticket/64544


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Jan 23, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props glendaviesnz, shailu25, scruffian, aaronrobertshaw, mamaduka, shimotomoki.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@glendaviesnz
Copy link
Author

@Mamaduka I have added the change from WordPress/gutenberg#74969

@Mamaduka
Copy link
Member

Thank you, @glendaviesnz!

@glendaviesnz
Copy link
Author

@scruffian, @aaronrobertshaw I am not sure why all the tests are failing - it seems unrelated to this PR - have tried a rebase.

@scruffian
Copy link
Contributor

A couple of reasons!

  1. Change WP_Theme_JSON_Gutenberg to WP_Theme_JSON in src/wp-includes/block-supports/custom-css.php:43
  2. Add static keyword to the method signature in src/wp-includes/class-wp-theme-json.php:1514

@aaronrobertshaw
Copy link

@scruffian is on the money regarding the test failures.

One other nit is that I believe core tests required a @ticket XXXX annotation so it is easier to see when a specific test was added.

@shimotmk
Copy link

@glendaviesnz Could you please also add these changes?🙏
WordPress/gutenberg#75052

@glendaviesnz
Copy link
Author

glendaviesnz commented Feb 16, 2026

@glendaviesnz Could you please also add these changes?🙏 WordPress/gutenberg#75052

@shimotmk Done ddfccb2

@glendaviesnz
Copy link
Author

@scruffian, @aaronrobertshaw I think it is ready for another review

Copy link

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for iterating here @glendaviesnz 👍

The tests are passing and this looks good except for a few minor issues I've noted in inline comments.

Also, I think we should keep the @covers annotations to be consistent with the other block support tests. The Gutenberg PR had these at the individual test level but they could be best done at the class level.

On that note, there's a new approach to organising the PHP unit tests for block supports that I think we might also need to follow here. You can see the newest block supports such as background and dimensions have dedicated classes for different functions.

Example diff to refactor to current test structure
diff --git a/tests/phpunit/tests/block-supports/custom-css.php b/tests/phpunit/tests/block-supports/custom-css.php
deleted file mode 100644
index b69cc03795..0000000000
--- a/tests/phpunit/tests/block-supports/custom-css.php
+++ /dev/null
@@ -1,441 +0,0 @@
-<?php
-/**
- * Test the custom CSS block support.
- *
- * @package WordPress
- */
-
-class WP_Block_Supports_Custom_CSS_Test extends WP_UnitTestCase {
-	/**
-	 * @var string|null
-	 */
-	private $test_block_name;
-
-	public function set_up() {
-		parent::set_up();
-		$this->test_block_name = null;
-	}
-
-	public function tear_down() {
-		if ( $this->test_block_name ) {
-			unregister_block_type( $this->test_block_name );
-		}
-		$this->test_block_name = null;
-		parent::tear_down();
-	}
-
-	/**
-	 * Registers a new block for testing custom CSS support.
-	 *
-	 * @param string $block_name Name for the test block.
-	 * @param array  $supports   Array defining block support configuration.
-	 *
-	 * @return WP_Block_Type The block type for the newly registered test block.
-	 */
-	private function register_custom_css_block_with_support( $block_name, $supports = array() ) {
-		$this->test_block_name = $block_name;
-		register_block_type(
-			$this->test_block_name,
-			array(
-				'api_version' => 3,
-				'attributes'  => array(
-					'style' => array(
-						'type' => 'object',
-					),
-				),
-				'supports'    => $supports,
-			)
-		);
-		$registry = WP_Block_Type_Registry::get_instance();
-
-		return $registry->get_registered( $this->test_block_name );
-	}
-
-	/**
-	 * Tests that custom CSS support adds class name when block has custom CSS.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_adds_class_name_when_css_present() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-block',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-block',
-			'attrs'     => array(
-				'style' => array(
-					'css' => 'color: red;',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayHasKey( 'className', $result['attrs'], 'Block should have className added.' );
-		$this->assertMatchesRegularExpression( '/wp-custom-css-/', $result['attrs']['className'], 'className should contain wp-custom-css- prefix.' );
-	}
-
-	/**
-	 * Tests that custom CSS support preserves existing className.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_preserves_existing_class_name() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-block-existing',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-block-existing',
-			'attrs'     => array(
-				'className' => 'my-existing-class',
-				'style'     => array(
-					'css' => 'color: blue;',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertStringContainsString( 'my-existing-class', $result['attrs']['className'], 'Existing className should be preserved.' );
-		$this->assertMatchesRegularExpression( '/wp-custom-css-/', $result['attrs']['className'], 'className should also contain wp-custom-css- prefix.' );
-	}
-
-	/**
-	 * Tests that custom CSS support returns unchanged block when support is disabled.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_returns_unchanged_when_support_disabled() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-disabled',
-			array( 'customCSS' => false )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-disabled',
-			'attrs'     => array(
-				'style' => array(
-					'css' => 'color: green;',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when support is disabled.' );
-	}
-
-	/**
-	 * Tests that custom CSS support returns unchanged block when no CSS attribute present.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_returns_unchanged_when_no_css() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-no-css',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-no-css',
-			'attrs'     => array(
-				'style' => array(
-					'color' => 'red',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when no CSS attribute.' );
-	}
-
-	/**
-	 * Tests that custom CSS support returns unchanged block when CSS is empty.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_returns_unchanged_when_css_empty() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-empty',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-empty',
-			'attrs'     => array(
-				'style' => array(
-					'css' => '',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when CSS is empty.' );
-	}
-
-	/**
-	 * Tests that custom CSS support returns unchanged block when CSS is whitespace only.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_returns_unchanged_when_css_whitespace_only() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-whitespace',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-whitespace',
-			'attrs'     => array(
-				'style' => array(
-					'css' => '   ',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when CSS is whitespace only.' );
-	}
-
-	/**
-	 * Tests that custom CSS support returns unchanged block when style attribute is missing.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_returns_unchanged_when_no_style_attribute() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-no-style',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-no-style',
-			'attrs'     => array(),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when no style attribute.' );
-	}
-
-	/**
-	 * Tests that render_block filter adds custom CSS class to block content.
-	 *
-	 * @@ticket 64544
-	 */
-	public function test_render_custom_css_class_name_adds_class_to_content() {
-		$block_content = '<div class="wp-block-paragraph">Test content</div>';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(
-				'className' => 'wp-custom-css-123abc',
-			),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertStringContainsString( 'wp-custom-css-123abc', $result, 'Custom CSS class should be added to block content.' );
-	}
-
-	/**
-	 * Tests that render_block filter preserves existing classes when adding custom CSS class.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_render_custom_css_class_name_preserves_existing_classes() {
-		$block_content = '<div class="existing-class another-class">Test content</div>';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(
-				'className' => 'wp-custom-css-456def',
-			),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertStringContainsString( 'existing-class', $result, 'Existing classes should be preserved.' );
-		$this->assertStringContainsString( 'another-class', $result, 'All existing classes should be preserved.' );
-		$this->assertStringContainsString( 'wp-custom-css-456def', $result, 'Custom CSS class should be added.' );
-	}
-
-	/**
-	 * Tests that render_block filter returns unchanged content when no custom CSS class in attrs.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_render_custom_css_class_name_returns_unchanged_when_no_custom_css_class() {
-		$block_content = '<div class="wp-block-paragraph">Test content</div>';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(
-				'className' => 'some-other-class',
-			),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertSame( $block_content, $result, 'Block content should remain unchanged when no custom CSS class.' );
-	}
-
-	/**
-	 * Tests that render_block filter returns unchanged content when className is empty.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_render_custom_css_class_name_returns_unchanged_when_classname_empty() {
-		$block_content = '<div class="wp-block-paragraph">Test content</div>';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertSame( $block_content, $result, 'Block content should remain unchanged when className is empty.' );
-	}
-
-	/**
-	 * Tests that render_block filter returns empty string when content is empty.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_render_custom_css_class_name_returns_empty_when_content_empty() {
-		$block_content = '';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(
-				'className' => 'wp-custom-css-789ghi',
-			),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertSame( '', $result, 'Result should be empty when block content is empty.' );
-	}
-
-	/**
-	 * Tests that custom CSS class is extracted correctly when mixed with other classes.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_render_custom_css_class_name_extracts_class_from_mixed_classnames() {
-		$block_content = '<p>Test content</p>';
-		$block         = array(
-			'blockName' => 'core/paragraph',
-			'attrs'     => array(
-				'className' => 'my-class wp-custom-css-mixed123 another-class',
-			),
-		);
-
-		$result = wp_render_custom_css_class_name( $block_content, $block );
-
-		$this->assertStringContainsString( 'wp-custom-css-mixed123', $result, 'Custom CSS class should be extracted and added.' );
-	}
-
-	/**
-	 * Tests that custom CSS support is enabled by default.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_support_enabled_by_default() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-default',
-			array() // No explicit customCSS support defined.
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-default',
-			'attrs'     => array(
-				'style' => array(
-					'css' => 'font-weight: bold;',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayHasKey( 'className', $result['attrs'], 'Block should have className added by default when customCSS support is not explicitly set.' );
-	}
-
-	/**
-	 * Tests that custom CSS containing HTML opening tags is rejected.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_rejects_html_opening_tags() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-html-open',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-html-open',
-			'attrs'     => array(
-				'style' => array(
-					'css' => '<script>alert(1)</script>',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when CSS contains HTML opening tags.' );
-	}
-
-	/**
-	 * Tests that custom CSS containing HTML closing tags is rejected.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_rejects_html_closing_tags() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-html-close',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-html-close',
-			'attrs'     => array(
-				'style' => array(
-					'css' => 'color: red;</style><script>alert(1)</script>',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added when CSS contains HTML closing tags.' );
-	}
-
-	/**
-	 * Tests that valid CSS without HTML markup is accepted.
-	 *
-	 * @ticket 64544
-	 */
-	public function test_custom_css_accepts_valid_css() {
-		$this->register_custom_css_block_with_support(
-			'test/custom-css-valid',
-			array( 'customCSS' => true )
-		);
-
-		$parsed_block = array(
-			'blockName' => 'test/custom-css-valid',
-			'attrs'     => array(
-				'style' => array(
-					'css' => 'color: red; background: url("image.png"); font-size: 16px;',
-				),
-			),
-		);
-
-		$result = wp_render_custom_css_support_styles( $parsed_block );
-
-		$this->assertArrayHasKey( 'className', $result['attrs'], 'Block should have className added for valid CSS.' );
-	}
-}
diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssClassName.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssClassName.php
new file mode 100644
index 0000000000..c2a2cefb7a
--- /dev/null
+++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssClassName.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * @group block-supports
+ *
+ * @covers ::wp_render_custom_css_class_name
+ */
+class Tests_Block_Supports_WpRenderCustomCssClassName extends WP_UnitTestCase {
+
+	/**
+	 * Tests that the custom CSS class name is applied to block content.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_class_name
+	 *
+	 * @dataProvider data_adds_class_to_content
+	 *
+	 * @param string $block_content The rendered block content.
+	 * @param array  $block         The block data.
+	 * @param string $expected_class The expected class in the output.
+	 */
+	public function test_adds_class_to_content( $block_content, $block, $expected_class ) {
+		$result = wp_render_custom_css_class_name( $block_content, $block );
+
+		$this->assertStringContainsString( $expected_class, $result, 'Custom CSS class should be present in the output.' );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array
+	 */
+	public function data_adds_class_to_content() {
+		return array(
+			'class is added to block content'             => array(
+				'block_content' => '<div class="wp-block-paragraph">Test content</div>',
+				'block'         => array(
+					'blockName' => 'core/paragraph',
+					'attrs'     => array(
+						'className' => 'wp-custom-css-123abc',
+					),
+				),
+				'expected_class' => 'wp-custom-css-123abc',
+			),
+			'class is extracted from mixed class names'   => array(
+				'block_content' => '<p>Test content</p>',
+				'block'         => array(
+					'blockName' => 'core/paragraph',
+					'attrs'     => array(
+						'className' => 'my-class wp-custom-css-mixed123 another-class',
+					),
+				),
+				'expected_class' => 'wp-custom-css-mixed123',
+			),
+		);
+	}
+
+	/**
+	 * Tests that existing classes are preserved when the custom CSS class is added.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_class_name
+	 */
+	public function test_preserves_existing_classes() {
+		$block_content = '<div class="existing-class another-class">Test content</div>';
+		$block         = array(
+			'blockName' => 'core/paragraph',
+			'attrs'     => array(
+				'className' => 'wp-custom-css-456def',
+			),
+		);
+
+		$result = wp_render_custom_css_class_name( $block_content, $block );
+
+		$this->assertStringContainsString( 'existing-class', $result, 'Existing classes should be preserved.' );
+		$this->assertStringContainsString( 'another-class', $result, 'All existing classes should be preserved.' );
+		$this->assertStringContainsString( 'wp-custom-css-456def', $result, 'Custom CSS class should be added.' );
+	}
+
+	/**
+	 * Tests that block content is returned unchanged when no custom CSS class should be applied.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_class_name
+	 *
+	 * @dataProvider data_returns_unchanged_content
+	 *
+	 * @param string $block_content The rendered block content.
+	 * @param array  $block         The block data.
+	 */
+	public function test_returns_unchanged_content( $block_content, $block ) {
+		$result = wp_render_custom_css_class_name( $block_content, $block );
+
+		$this->assertSame( $block_content, $result, 'Block content should remain unchanged.' );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array
+	 */
+	public function data_returns_unchanged_content() {
+		return array(
+			'no custom CSS class in attrs'  => array(
+				'block_content' => '<div class="wp-block-paragraph">Test content</div>',
+				'block'         => array(
+					'blockName' => 'core/paragraph',
+					'attrs'     => array(
+						'className' => 'some-other-class',
+					),
+				),
+			),
+			'className is not set in attrs' => array(
+				'block_content' => '<div class="wp-block-paragraph">Test content</div>',
+				'block'         => array(
+					'blockName' => 'core/paragraph',
+					'attrs'     => array(),
+				),
+			),
+			'block content is empty'        => array(
+				'block_content' => '',
+				'block'         => array(
+					'blockName' => 'core/paragraph',
+					'attrs'     => array(
+						'className' => 'wp-custom-css-789ghi',
+					),
+				),
+			),
+		);
+	}
+}
diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php
new file mode 100644
index 0000000000..5e2822cd4a
--- /dev/null
+++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php
@@ -0,0 +1,266 @@
+<?php
+/**
+ * @group block-supports
+ *
+ * @covers ::wp_render_custom_css_support_styles
+ */
+class Tests_Block_Supports_WpRenderCustomCssSupportStyles extends WP_UnitTestCase {
+	/**
+	 * @var string|null
+	 */
+	private $test_block_name;
+
+	public function set_up() {
+		parent::set_up();
+		$this->test_block_name = null;
+	}
+
+	public function tear_down() {
+		if ( $this->test_block_name ) {
+			unregister_block_type( $this->test_block_name );
+		}
+		$this->test_block_name = null;
+		parent::tear_down();
+	}
+
+	/**
+	 * Tests that custom CSS support adds a class name when valid CSS is present.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_support_styles
+	 *
+	 * @dataProvider data_adds_class_name
+	 *
+	 * @param string $block_name   The test block name to register.
+	 * @param array  $supports     The block support configuration.
+	 * @param array  $parsed_block The parsed block data.
+	 */
+	public function test_adds_class_name( $block_name, $supports, $parsed_block ) {
+		$this->test_block_name = $block_name;
+		register_block_type(
+			$this->test_block_name,
+			array(
+				'api_version' => 3,
+				'attributes'  => array(
+					'style' => array(
+						'type' => 'object',
+					),
+				),
+				'supports'    => $supports,
+			)
+		);
+
+		$result = wp_render_custom_css_support_styles( $parsed_block );
+
+		$this->assertArrayHasKey( 'className', $result['attrs'], 'Block should have className added.' );
+		$this->assertMatchesRegularExpression( '/wp-custom-css-/', $result['attrs']['className'], 'className should contain wp-custom-css- prefix.' );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array
+	 */
+	public function data_adds_class_name() {
+		return array(
+			'class name is added when custom CSS is present'   => array(
+				'block_name'   => 'test/custom-css-block',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-block',
+					'attrs'     => array(
+						'style' => array(
+							'css' => 'color: red;',
+						),
+					),
+				),
+			),
+			'class name is added when support is not explicitly set' => array(
+				'block_name'   => 'test/custom-css-default',
+				'supports'     => array(),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-default',
+					'attrs'     => array(
+						'style' => array(
+							'css' => 'font-weight: bold;',
+						),
+					),
+				),
+			),
+			'class name is added for valid CSS with url() values' => array(
+				'block_name'   => 'test/custom-css-valid',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-valid',
+					'attrs'     => array(
+						'style' => array(
+							'css' => 'color: red; background: url("image.png"); font-size: 16px;',
+						),
+					),
+				),
+			),
+		);
+	}
+
+	/**
+	 * Tests that existing className is preserved when custom CSS class is added.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_support_styles
+	 */
+	public function test_preserves_existing_class_name() {
+		$this->test_block_name = 'test/custom-css-block-existing';
+		register_block_type(
+			$this->test_block_name,
+			array(
+				'api_version' => 3,
+				'attributes'  => array(
+					'style' => array(
+						'type' => 'object',
+					),
+				),
+				'supports'    => array( 'customCSS' => true ),
+			)
+		);
+
+		$parsed_block = array(
+			'blockName' => 'test/custom-css-block-existing',
+			'attrs'     => array(
+				'className' => 'my-existing-class',
+				'style'     => array(
+					'css' => 'color: blue;',
+				),
+			),
+		);
+
+		$result = wp_render_custom_css_support_styles( $parsed_block );
+
+		$this->assertStringContainsString( 'my-existing-class', $result['attrs']['className'], 'Existing className should be preserved.' );
+		$this->assertMatchesRegularExpression( '/wp-custom-css-/', $result['attrs']['className'], 'className should also contain wp-custom-css- prefix.' );
+	}
+
+	/**
+	 * Tests that custom CSS support does not add a class name when CSS should not be applied.
+	 *
+	 * @ticket 64544
+	 *
+	 * @covers ::wp_render_custom_css_support_styles
+	 *
+	 * @dataProvider data_does_not_add_class_name
+	 *
+	 * @param string $block_name   The test block name to register.
+	 * @param array  $supports     The block support configuration.
+	 * @param array  $parsed_block The parsed block data.
+	 */
+	public function test_does_not_add_class_name( $block_name, $supports, $parsed_block ) {
+		$this->test_block_name = $block_name;
+		register_block_type(
+			$this->test_block_name,
+			array(
+				'api_version' => 3,
+				'attributes'  => array(
+					'style' => array(
+						'type' => 'object',
+					),
+				),
+				'supports'    => $supports,
+			)
+		);
+
+		$result = wp_render_custom_css_support_styles( $parsed_block );
+
+		$this->assertArrayNotHasKey( 'className', $result['attrs'], 'Block should not have className added.' );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array
+	 */
+	public function data_does_not_add_class_name() {
+		return array(
+			'support is disabled'                    => array(
+				'block_name'   => 'test/custom-css-disabled',
+				'supports'     => array( 'customCSS' => false ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-disabled',
+					'attrs'     => array(
+						'style' => array(
+							'css' => 'color: green;',
+						),
+					),
+				),
+			),
+			'no CSS attribute present'               => array(
+				'block_name'   => 'test/custom-css-no-css',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-no-css',
+					'attrs'     => array(
+						'style' => array(
+							'color' => 'red',
+						),
+					),
+				),
+			),
+			'CSS is empty'                           => array(
+				'block_name'   => 'test/custom-css-empty',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-empty',
+					'attrs'     => array(
+						'style' => array(
+							'css' => '',
+						),
+					),
+				),
+			),
+			'CSS is whitespace only'                 => array(
+				'block_name'   => 'test/custom-css-whitespace',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-whitespace',
+					'attrs'     => array(
+						'style' => array(
+							'css' => '   ',
+						),
+					),
+				),
+			),
+			'no style attribute'                     => array(
+				'block_name'   => 'test/custom-css-no-style',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-no-style',
+					'attrs'     => array(),
+				),
+			),
+			'CSS contains HTML opening tags'         => array(
+				'block_name'   => 'test/custom-css-html-open',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-html-open',
+					'attrs'     => array(
+						'style' => array(
+							'css' => '<script>alert(1)</script>',
+						),
+					),
+				),
+			),
+			'CSS contains HTML closing tags'         => array(
+				'block_name'   => 'test/custom-css-html-close',
+				'supports'     => array( 'customCSS' => true ),
+				'parsed_block' => array(
+					'blockName' => 'test/custom-css-html-close',
+					'attrs'     => array(
+						'style' => array(
+							'css' => 'color: red;</style><script>alert(1)</script>',
+						),
+					),
+				),
+			),
+		);
+	}
+}

I hope that helps a bit.

@glendaviesnz
Copy link
Author

Thanks for all the pointers @aaronrobertshaw - I think I covered off all those things.

Copy link

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patience here @glendaviesnz!

The tests are passing and I didn't spot any differences in the backported code.

LGTM 🚢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants