From c80fcb0a95957009ed6c0fdc83cc200c41937ac4 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 7 Oct 2026 19:21:24 +0400 Subject: [PATCH 1/2] Themes: Record the registered blocks on the first theme.json origin build. WP_Theme_JSON_Resolver::get_core_data(), get_block_data(), get_theme_data() and get_user_data() tested the cached object for null before calling has_same_registered_blocks(). On the first call the object is null, so the check never ran and never recorded the registered blocks. The second call ran the check, found nothing recorded, and rebuilt the origin's data. Call has_same_registered_blocks() first, so the first build records the registered blocks and the second call reuses the data. A block type registered between calls still rebuilds it. The test data provider case "When both caches are not empty" left the blocks cache empty and expected a rebuild; it now fills the cache and expects none. --- .../class-wp-theme-json-resolver.php | 8 +-- .../tests/theme/wpThemeJsonResolver.php | 66 ++++++++++++++++--- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 05eedbcd40397..974a82f813782 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -158,7 +158,7 @@ protected static function translate( $theme_json, $domain = 'default' ) { * @return WP_Theme_JSON Entity that holds core data. */ public static function get_core_data() { - if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) { + if ( static::has_same_registered_blocks( 'core' ) && null !== static::$core ) { return static::$core; } @@ -250,7 +250,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() $options = wp_parse_args( $options, array( 'with_supports' => true ) ); - if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { + if ( ! static::has_same_registered_blocks( 'theme' ) || null === static::$theme ) { $wp_theme = wp_get_theme(); $theme_json_file = $wp_theme->get_file_path( 'theme.json' ); if ( is_readable( $theme_json_file ) ) { @@ -395,7 +395,7 @@ public static function get_block_data() { $registry = WP_Block_Type_Registry::get_instance(); $blocks = $registry->get_all_registered(); - if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) { + if ( static::has_same_registered_blocks( 'blocks' ) && null !== static::$blocks ) { return static::$blocks; } @@ -543,7 +543,7 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post * @return WP_Theme_JSON Entity that holds styles for user data. */ public static function get_user_data() { - if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) { + if ( static::has_same_registered_blocks( 'user' ) && null !== static::$user ) { return static::$user; } diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 50a240982715b..d73d6a3b1b924 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -114,6 +114,10 @@ public function tear_down() { unset( $GLOBALS['wp_themes'] ); remove_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) ); + if ( WP_Block_Type_Registry::get_instance()->is_registered( 'test/seeded' ) ) { + unregister_block_type( 'test/seeded' ); + } + // Reset data between tests. wp_clean_theme_json_cache(); parent::tear_down(); @@ -421,12 +425,10 @@ public function test_get_core_data( $should_fire_filter, $core_is_cached, $block WP_Theme_JSON_Resolver::get_core_data(); } - // If should cache registered blocks, then set them up before running the tests. - if ( $blocks_are_cached ) { - $blocks_cache = static::$property_blocks_cache->getValue(); - $blocks_cache['core'] = $this->get_registered_block_names(); - static::$property_blocks_cache->setValue( null, $blocks_cache ); - } + // Set the blocks cache to the registered blocks, or empty it: caching core also fills it. + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache['core'] = $blocks_are_cached ? $this->get_registered_block_names() : array(); + static::$property_blocks_cache->setValue( null, $blocks_cache ); $expected_filter_count = did_filter( 'wp_theme_json_data_default' ); $actual = WP_Theme_JSON_Resolver::get_core_data(); @@ -462,13 +464,61 @@ public static function data_get_core_data() { 'blocks_are_cached' => false, ), 'When both caches are not empty' => array( - 'should_fire_filter' => true, + 'should_fire_filter' => false, 'core_is_cached' => true, - 'blocks_are_cached' => false, + 'blocks_are_cached' => true, ), ); } + /** + * The first call to an origin getter should record the registered blocks, + * so the second call reuses the data instead of rebuilding it. + * + * @dataProvider data_origin_getters + * + * @covers WP_Theme_JSON_Resolver::get_core_data + * @covers WP_Theme_JSON_Resolver::get_block_data + * @covers WP_Theme_JSON_Resolver::get_theme_data + * @covers WP_Theme_JSON_Resolver::get_user_data + * + * @param string $getter Resolver method name. + * @param array $args Arguments for the method. + * @param string $filter Filter the method applies when it builds the data. + */ + public function test_origin_data_is_built_once_while_registered_blocks_are_unchanged( $getter, array $args, $filter ) { + wp_clean_theme_json_cache(); + $callback = array( WP_Theme_JSON_Resolver::class, $getter ); + $filter_count = did_filter( $filter ); + + $first = call_user_func_array( $callback, $args ); + $this->assertSame( $filter_count + 1, did_filter( $filter ), 'The first call should build the data' ); + + $second = call_user_func_array( $callback, $args ); + $this->assertSame( $filter_count + 1, did_filter( $filter ), 'The second call should not rebuild the data' ); + $this->assertSame( $first, $second, 'The second call should return the same object' ); + + register_block_type( 'test/seeded' ); + + $third = call_user_func_array( $callback, $args ); + $this->assertSame( $filter_count + 2, did_filter( $filter ), 'Registering a block type should rebuild the data' ); + $this->assertNotSame( $first, $third, 'The rebuilt data should be a new object' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_origin_getters() { + return array( + 'core' => array( 'get_core_data', array(), 'wp_theme_json_data_default' ), + 'blocks' => array( 'get_block_data', array(), 'wp_theme_json_data_blocks' ), + 'theme' => array( 'get_theme_data', array( array(), array( 'with_supports' => false ) ), 'wp_theme_json_data_theme' ), + 'user' => array( 'get_user_data', array(), 'wp_theme_json_data_user' ), + ); + } + /** * @ticket 54336 * @ticket 60118 From d52f8b9a382a6704bebd2f2738e1d4f6b42519f0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 9 Oct 2026 16:11:26 +0400 Subject: [PATCH 2/2] Tests: Add the ticket and types to the theme.json origin getter test. --- tests/phpunit/tests/theme/wpThemeJsonResolver.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index d73d6a3b1b924..46ac478095185 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -475,6 +475,8 @@ public static function data_get_core_data() { * The first call to an origin getter should record the registered blocks, * so the second call reuses the data instead of rebuilding it. * + * @ticket 62249 + * * @dataProvider data_origin_getters * * @covers WP_Theme_JSON_Resolver::get_core_data @@ -482,11 +484,11 @@ public static function data_get_core_data() { * @covers WP_Theme_JSON_Resolver::get_theme_data * @covers WP_Theme_JSON_Resolver::get_user_data * - * @param string $getter Resolver method name. - * @param array $args Arguments for the method. - * @param string $filter Filter the method applies when it builds the data. + * @param string $getter Resolver method name. + * @param list $args Arguments for the method. + * @param string $filter Filter the method applies when it builds the data. */ - public function test_origin_data_is_built_once_while_registered_blocks_are_unchanged( $getter, array $args, $filter ) { + public function test_origin_data_is_built_once_while_registered_blocks_are_unchanged( string $getter, array $args, string $filter ): void { wp_clean_theme_json_cache(); $callback = array( WP_Theme_JSON_Resolver::class, $getter ); $filter_count = did_filter( $filter ); @@ -508,9 +510,9 @@ public function test_origin_data_is_built_once_while_registered_blocks_are_uncha /** * Data provider. * - * @return array + * @return array, 2: string}> */ - public static function data_origin_getters() { + public static function data_origin_getters(): array { return array( 'core' => array( 'get_core_data', array(), 'wp_theme_json_data_default' ), 'blocks' => array( 'get_block_data', array(), 'wp_theme_json_data_blocks' ),