From 4c9fe2cdec211713bd89d317d634c6eb012c635e Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 19 Oct 2026 18:19:35 +0100 Subject: [PATCH 1/2] Editor: Register the core entity actions on the server. Loads `register_core_entity_actions()` from the `core-actions` package, which registers the actions of posts, pages, patterns and template parts through the entity actions API at priority 0, so a plugin can unregister or replace them. The build copies its PHP files to `wp-includes/build/scripts/core-actions/`. The post editor also preloads the actions of the post type it edits. Synced from https://github.com/WordPress/gutenberg/pull/84660. See #66343. --- Gruntfile.js | 4 +- src/wp-includes/default-filters.php | 1 + src/wp-includes/entity-actions.php | 32 +- src/wp-settings.php | 3 + tests/phpunit/tests/entity-actions.php | 60 +- .../registerCoreEntityActions.php | 520 ++++++++++++++++++ .../wpAddEntityFieldModulesToEditorScript.php | 3 + 7 files changed, 616 insertions(+), 7 deletions(-) create mode 100644 tests/phpunit/tests/entity-actions/registerCoreEntityActions.php diff --git a/Gruntfile.js b/Gruntfile.js index b720379a23101..9340b45302a33 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -697,7 +697,7 @@ module.exports = function(grunt) { src: 'vendor/composer/ca-bundle/res/cacert.pem', dest: SOURCE_DIR + 'wp-includes/certificates/ca-bundle.crt' }, - // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/). + // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/, core actions). 'gutenberg-php': { options: { process: function( content ) { @@ -716,6 +716,8 @@ module.exports = function(grunt) { 'pages.php', 'constants.php', 'pages/**/*.php', + // The actions Core registers through the entity actions API. + 'scripts/core-actions/**/*.php', ], dest: SOURCE_DIR + 'wp-includes/build/', } ], diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 16afab80fcd58..d2fb5d59a8b7a 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -855,6 +855,7 @@ // Entity Actions API. add_action( 'admin_footer', '_wp_add_entity_action_modules_to_editor_script', 10, 0 ); +add_filter( 'block_editor_rest_api_preload_paths', '_wp_preload_entity_actions', 10, 2 ); // Entity Fields API. add_action( 'admin_footer', '_wp_add_entity_field_modules_to_editor_script', 10, 0 ); diff --git a/src/wp-includes/entity-actions.php b/src/wp-includes/entity-actions.php index 6496048f71d9e..13ec78bc03e33 100644 --- a/src/wp-includes/entity-actions.php +++ b/src/wp-includes/entity-actions.php @@ -15,7 +15,8 @@ * * Actions are registered on the `wp_entity_actions_api_init` action, and only there, * with {@see wp_register_entity_actions()} and {@see wp_unregister_entity_actions()}. The - * other functions read the registry. + * other functions read the registry. WordPress core registers its own + * actions the same way, from packages/core-actions. * * @package WordPress * @subpackage Entity Actions @@ -186,3 +187,32 @@ function _wp_add_entity_action_modules_to_editor_script( $scripts = null ) { $scripts->add_data( 'wp-editor', 'module_dependencies', $dependencies ); } } + +/** + * Preloads the actions of the post type the post editor edits. + * + * The actions menu of the post editor reads them from the `wp/v2/entity-actions` + * route as it loads, see `useEntityActions()` in + * packages/editor/src/dataviews/entity-actions-loader.ts. The path must match the + * request of the `getEntityActionsConfig` core data resolver, see + * packages/core-data/src/resolvers.js. + * + * @since 7.2.0 + * + * @param array $paths REST API paths to preload. + * @param WP_Block_Editor_Context $context Block editor context. + * @return array Filtered preload paths. + */ +function _wp_preload_entity_actions( $paths, $context ) { + if ( 'core/edit-post' === $context->name && isset( $context->post ) ) { + $paths[] = add_query_arg( + array( + 'kind' => 'postType', + 'name' => $context->post->post_type, + ), + '/wp/v2/entity-actions' + ); + } + + return $paths; +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 98ecc5781917f..cc7a1e00a38c3 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -254,6 +254,9 @@ if ( file_exists( ABSPATH . WPINC . '/build/pages.php' ) ) { require ABSPATH . WPINC . '/build/pages.php'; } +if ( file_exists( ABSPATH . WPINC . '/build/scripts/core-actions/index.php' ) ) { + require ABSPATH . WPINC . '/build/scripts/core-actions/index.php'; +} require ABSPATH . WPINC . '/taxonomy.php'; require ABSPATH . WPINC . '/class-wp-taxonomy.php'; require ABSPATH . WPINC . '/class-wp-term.php'; diff --git a/tests/phpunit/tests/entity-actions.php b/tests/phpunit/tests/entity-actions.php index 8283a81b6b64d..17e73686efcf7 100644 --- a/tests/phpunit/tests/entity-actions.php +++ b/tests/phpunit/tests/entity-actions.php @@ -8,6 +8,7 @@ * @group entity-actions * * @covers ::_wp_add_entity_action_modules_to_editor_script + * @covers ::_wp_preload_entity_actions * @covers ::wp_register_entity_actions * @covers ::wp_unregister_entity_actions * @covers WP_Entity_Actions_Registry::initialize @@ -17,12 +18,15 @@ class Tests_Entity_Actions extends WP_UnitTestCase { /** - * Sets up each test: the default actions of the tests are registered at - * priority 0, so every test runs against a registry holding actions - * registered before the callbacks hooked at the default priority run. + * Sets up each test: the actions WordPress registers are replaced with + * the default actions of the tests, hooked at priority 0 as WordPress + * registers its own, so every test runs against a registry holding + * actions registered before the callbacks hooked at the default priority + * run. */ public function set_up() { parent::set_up(); + remove_action( 'wp_entity_actions_api_init', 'register_core_entity_actions', 0 ); add_action( 'wp_entity_actions_api_init', array( __CLASS__, 'register_default_actions' ), 0 ); } @@ -31,8 +35,9 @@ public function set_up() { * * Resetting the registry drops the actions a test registered along with * the defaults; the next read fires `wp_entity_actions_api_init` again and - * registers the defaults anew. The test case removes the callbacks the - * test hooked, the defaults included. + * registers the defaults anew. The test case restores the hooks: it + * removes the callbacks the test hooked, the defaults included, and hooks + * the actions WordPress registers back. */ public function tear_down() { self::reset_registry(); @@ -387,6 +392,51 @@ public function test_action_script_modules_reach_the_import_map() { $this->assertArrayHasKey( 'plugin/feature', $import_map['imports'] ); } + /** + * The post editor preloads the actions of the post type it edits, with + * the path the `getEntityActionsConfig` resolver of core data requests. + * + * @ticket 66343 + */ + public function test_the_post_editor_preloads_the_actions_of_the_post_type() { + $context = new WP_Block_Editor_Context( array( 'post' => self::factory()->post->create_and_get( array( 'post_type' => 'page' ) ) ) ); + + $paths = apply_filters( 'block_editor_rest_api_preload_paths', array(), $context ); + + $this->assertContains( '/wp/v2/entity-actions?kind=postType&name=page', $paths, 'The actions of pages are preloaded.' ); + } + + /** + * The preloaded path serves the actions of the post type. + * + * @ticket 66343 + */ + public function test_the_preloaded_actions_path_serves_the_actions() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + $context = new WP_Block_Editor_Context( array( 'post' => self::factory()->post->create_and_get( array( 'post_type' => 'page' ) ) ) ); + + $paths = _wp_preload_entity_actions( array(), $context ); + $preloaded = array_reduce( $paths, 'rest_preload_api_request', array() ); + + $this->assertSame( + array( 'rename-post', 'move-to-trash' ), + array_column( $preloaded['/wp/v2/entity-actions?kind=postType&name=page']['body']['actions'], 'id' ), + 'The preloaded response holds the actions of pages.' + ); + } + + /** + * Only the post editor preloads the actions: the other editors have no + * post type of their own. + * + * @ticket 66343 + */ + public function test_other_editors_do_not_preload_the_actions() { + $context = new WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) ); + + $this->assertSame( array( '/wp/v2/types' ), _wp_preload_entity_actions( array( '/wp/v2/types' ), $context ), 'The paths are unchanged.' ); + } + /** * Reading the registry fires `wp_entity_actions_api_init` once, whichever * getter is read first and however many times it is read. diff --git a/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php b/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php new file mode 100644 index 0000000000000..188edf7b1d165 --- /dev/null +++ b/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php @@ -0,0 +1,520 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + self::$contributor_id = $factory->user->create( array( 'role' => 'contributor' ) ); + } + + /** + * Deletes shared users. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + self::delete_user( self::$contributor_id ); + } + + /** + * Tears down each test. + * + * Resetting the registry drops the actions registered for the user and + * theme of the test; the next read fires `wp_entity_actions_api_init` again. + */ + public function tear_down() { + self::reset_registry(); + + parent::tear_down(); + } + + /** + * Resets the registry: drops the singleton instance, so the next + * get_instance() creates an empty registry and its first read fires + * `wp_entity_actions_api_init` again. + */ + private static function reset_registry() { + $instance = new ReflectionProperty( WP_Entity_Actions_Registry::class, 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + $instance->setAccessible( true ); + } + $instance->setValue( null, null ); + } + + /** + * Returns the ids of the actions registered for a post type, from a + * registry built anew for the current user and theme, as each request + * builds it. + * + * @param string $post_type The post type. + * @return string[] The ids of the actions, in registration order. + */ + private function get_action_ids( $post_type ) { + self::reset_registry(); + return array_column( wp_get_registered_entity_actions( 'postType', $post_type ), 'id' ); + } + + /** + * The core actions are registered at priority 0, so a plugin hooking the + * action at the default priority sees them. + * + * @ticket 66343 + */ + public function test_the_core_actions_are_registered_at_priority_0() { + $this->assertSame( 0, has_action( 'wp_entity_actions_api_init', 'register_core_entity_actions' ) ); + } + + /** + * Pages are viewable and support revisions, titles, and page attributes, + * and the user can manage the site options: they have every action of the + * `post_type_supports` module, then the actions of the `page` module, in + * the order the actions are shown. + * + * @ticket 66343 + */ + public function test_the_actions_of_pages() { + wp_set_current_user( self::$admin_id ); + + $ids = array_merge( array( 'view-post', 'view-post-revisions', 'rename-post', 'order-pages' ), self::EVERY_POST_TYPE ); + $this->assertSame( array_merge( $ids, self::PAGE_ACTIONS ), $this->get_action_ids( 'page' ), 'Pages have these actions, in this order.' ); + $this->assertSame( + array( + self::POST_TYPE_SUPPORTS => $ids, + self::PAGE => self::PAGE_ACTIONS, + ), + wp_get_registered_entity_action_modules( 'postType', 'page' ), + 'Each action of pages is registered with the module of its folder.' + ); + $this->assertSame( array( 'core' ), array_unique( array_column( array_column( wp_get_registered_entity_actions( 'postType', 'page' ), 'origin' ), 'registeredBy' ) ), 'Every action is registered by core.' ); + } + + /** + * Posts do not support page attributes, so they cannot be ordered. + * + * @ticket 66343 + */ + public function test_the_actions_of_posts() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array_merge( array( 'view-post', 'view-post-revisions', 'rename-post' ), self::EVERY_POST_TYPE ), + $this->get_action_ids( 'post' ), + 'Posts have these actions, in this order.' + ); + } + + /** + * Patterns are not viewable and are duplicated and exported by the + * actions of the `wp_block` module, placed among the others in the order + * the actions are shown. + * + * @ticket 66343 + */ + public function test_the_actions_of_patterns() { + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array_merge( array( 'view-post-revisions', 'duplicate-pattern', 'rename-post', 'export-pattern' ), self::EVERY_POST_TYPE ), + $this->get_action_ids( 'wp_block' ), + 'Patterns have these actions, in this order.' + ); + $this->assertSame( + array( + self::POST_TYPE_SUPPORTS => array_merge( array( 'view-post-revisions', 'rename-post' ), self::EVERY_POST_TYPE ), + self::WP_BLOCK => array( 'duplicate-pattern', 'export-pattern' ), + ), + wp_get_registered_entity_action_modules( 'postType', 'wp_block' ), + 'The actions of patterns come from the `wp_block` module.' + ); + } + + /** + * Templates are not duplicated: the editor creates them from the theme. + * + * @ticket 66343 + */ + public function test_the_actions_of_templates() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + + $this->assertSame( + array_merge( array( 'view-post-revisions', 'rename-post' ), self::EVERY_POST_TYPE ), + $this->get_action_ids( 'wp_template' ), + 'Templates have these actions, in this order.' + ); + } + + /** + * Template parts are duplicated by the action of the `wp_template_part` + * module, with a block theme only. + * + * @ticket 66343 + */ + public function test_template_parts_are_only_duplicated_with_a_block_theme() { + wp_set_current_user( self::$admin_id ); + + switch_theme( 'block-theme' ); + $this->assertSame( + array_merge( array( 'view-post-revisions', 'duplicate-template-part', 'rename-post' ), self::EVERY_POST_TYPE ), + $this->get_action_ids( 'wp_template_part' ), + 'A block theme can duplicate template parts.' + ); + $this->assertSame( + array( 'duplicate-template-part' ), + wp_get_registered_entity_action_modules( 'postType', 'wp_template_part' )[ self::WP_TEMPLATE_PART ], + 'The action comes from the `wp_template_part` module.' + ); + + switch_theme( 'default' ); + $this->assertNotContains( 'duplicate-template-part', $this->get_action_ids( 'wp_template_part' ), 'A classic theme cannot.' ); + } + + /** + * The "Duplicate…" action of the other post types is only registered by + * the Gutenberg plugin. + * + * @ticket 66343 + */ + public function test_posts_are_not_duplicated() { + wp_set_current_user( self::$admin_id ); + + foreach ( get_post_types( array( 'show_in_rest' => true ) ) as $post_type ) { + $this->assertNotContains( 'duplicate-post', $this->get_action_ids( $post_type ), "The $post_type post type has no duplicate action." ); + } + } + + /** + * The duplicate actions are only registered for the users who can create + * the posts they duplicate into: the registry is built for the current + * user. + * + * @ticket 66343 + */ + public function test_the_duplicate_actions_depend_on_the_current_user() { + switch_theme( 'block-theme' ); + + wp_set_current_user( self::$admin_id ); + $this->assertContains( 'duplicate-pattern', $this->get_action_ids( 'wp_block' ), 'Administrators can create patterns.' ); + $this->assertContains( 'duplicate-template-part', $this->get_action_ids( 'wp_template_part' ), 'Administrators can create template parts.' ); + + wp_set_current_user( self::$editor_id ); + $this->assertContains( 'duplicate-pattern', $this->get_action_ids( 'wp_block' ), 'Editors can create patterns.' ); + $this->assertNotContains( 'duplicate-template-part', $this->get_action_ids( 'wp_template_part' ), 'Editors cannot create template parts.' ); + + wp_set_current_user( self::$contributor_id ); + $this->assertNotContains( 'duplicate-pattern', $this->get_action_ids( 'wp_block' ), 'Contributors cannot create patterns.' ); + $this->assertContains( 'export-pattern', $this->get_action_ids( 'wp_block' ), 'Contributors can export them.' ); + } + + /** + * The actions of a custom post type follow what it supports. + * + * @ticket 66343 + */ + public function test_the_actions_of_a_custom_post_type_follow_its_supports() { + register_post_type( + 'test_book', + array( + 'public' => true, + 'show_in_rest' => true, + 'supports' => array( 'title', 'revisions' ), + ) + ); + register_post_type( + 'test_note', + array( + 'show_ui' => true, + 'show_in_rest' => true, + 'supports' => array( 'editor' ), + ) + ); + register_post_type( + 'test_hidden', + array( + 'public' => true, + 'show_in_rest' => false, + ) + ); + wp_set_current_user( self::$admin_id ); + + $this->assertSame( + array_merge( array( 'view-post', 'view-post-revisions', 'rename-post' ), self::EVERY_POST_TYPE ), + $this->get_action_ids( 'test_book' ), + 'A viewable post type with titles and revisions.' + ); + $this->assertSame( + self::EVERY_POST_TYPE, + $this->get_action_ids( 'test_note' ), + 'A post type that is not viewable, without titles or revisions.' + ); + $this->assertSame( array(), $this->get_action_ids( 'test_hidden' ), 'A post type not exposed in the REST API has no action.' ); + } + + /** + * Setting the homepage or the posts page changes the site settings, so + * the actions of the `page` module are only registered for the users who + * can manage the site options. + * + * @ticket 66343 + */ + public function test_the_homepage_actions_depend_on_the_current_user() { + wp_set_current_user( self::$admin_id ); + $this->assertSame( self::PAGE_ACTIONS, array_values( array_intersect( $this->get_action_ids( 'page' ), self::PAGE_ACTIONS ) ), 'Administrators can manage the site options.' ); + + wp_set_current_user( self::$editor_id ); + $this->assertSame( array(), array_intersect( $this->get_action_ids( 'page' ), self::PAGE_ACTIONS ), 'Editors cannot manage the site options.' ); + } + + /** + * A `front-page` template shows on the front page whatever the site + * settings say, so the actions of the `page` module are not registered + * when the theme has one. + * + * @ticket 66343 + */ + public function test_the_homepage_actions_are_not_registered_with_a_front_page_template() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $this->assertContains( 'set-as-homepage', $this->get_action_ids( 'page' ), 'The theme has no `front-page` template.' ); + + $template_id = self::factory()->post->create( + array( + 'post_type' => 'wp_template', + 'post_name' => 'front-page', + 'post_title' => 'Front Page', + 'post_content' => '

Front page

', + ) + ); + wp_set_post_terms( $template_id, get_stylesheet(), 'wp_theme' ); + + $this->assertSame( array(), array_intersect( $this->get_action_ids( 'page' ), self::PAGE_ACTIONS ), 'The `front-page` template removes the actions.' ); + } + + /** + * The definitions are the ones of the `action.php` files, with the id + * first. + * + * @ticket 66343 + */ + public function test_the_definitions_come_from_the_action_files() { + wp_set_current_user( self::$admin_id ); + + $actions = array_column( wp_get_registered_entity_actions( 'postType', 'page' ), null, 'id' ); + $this->assertSame( + array( + 'id' => 'move-to-trash', + 'label' => 'Trash…', + 'isPrimary' => true, + 'supportsBulk' => true, + 'hideModalHeader' => true, + 'modalFocusOnMount' => 'firstContentElement', + 'origin' => array( 'registeredBy' => 'core' ), + ), + $actions['move-to-trash'], + 'The definition of the trash action.' + ); + $this->assertSame( + array( + 'id' => 'view-post-revisions', + 'label' => 'View revisions', + 'context' => 'list', + 'origin' => array( 'registeredBy' => 'core' ), + ), + $actions['view-post-revisions'], + 'The definition of the revisions action.' + ); + } + + /** + * The labels are translated with the default text domain of WordPress. + * + * @ticket 66343 + */ + public function test_the_labels_are_translated_with_the_default_text_domain() { + $domains = array(); + add_filter( + 'gettext', + static function ( $translation, $text, $domain ) use ( &$domains ) { + if ( 'Trash…' === $text ) { + $domains[] = $domain; + } + return $translation; + }, + 10, + 3 + ); + add_filter( + 'gettext_with_context', + static function ( $translation, $text, $context, $domain ) use ( &$domains ) { + if ( 'Trash…' === $text ) { + $domains[] = $domain; + } + return $translation; + }, + 10, + 4 + ); + wp_set_current_user( self::$admin_id ); + + $this->get_action_ids( 'page' ); + + $this->assertSame( array( 'default' ), array_unique( $domains ), 'The label of the trash action is translated in the default text domain.' ); + } + + /** + * The REST route serves the core actions of each post type, with their + * script modules, in a response that validates against its schema. + * + * @ticket 66343 + */ + public function test_the_rest_route_serves_the_core_actions() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $schema = ( new WP_REST_Entity_Actions_Controller() )->get_item_schema(); + + $modules = array( + 'page' => array( self::POST_TYPE_SUPPORTS, self::PAGE ), + 'wp_block' => array( self::POST_TYPE_SUPPORTS, self::WP_BLOCK ), + 'wp_template_part' => array( self::POST_TYPE_SUPPORTS, self::WP_TEMPLATE_PART ), + ); + foreach ( $modules as $post_type => $module_ids ) { + self::reset_registry(); + $request = new WP_REST_Request( 'GET', '/wp/v2/entity-actions' ); + $request->set_query_params( + array( + 'kind' => 'postType', + 'name' => $post_type, + ) + ); + $data = rest_get_server()->dispatch( $request )->get_data(); + + $this->assertSame( array_column( wp_get_registered_entity_actions( 'postType', $post_type ), 'id' ), array_column( $data['actions'], 'id' ), "The actions of $post_type are served." ); + $this->assertSame( $module_ids, array_column( $data['script_modules'], 'id' ), "The script modules of $post_type are served." ); + $this->assertTrue( rest_validate_value_from_schema( $data, $schema ), "The response for $post_type matches the schema." ); + } + } + + /** + * The script modules of the core actions are registered, so the editor + * script can add them to the import map. + * + * @ticket 66343 + */ + public function test_the_script_modules_of_the_core_actions_are_registered() { + wp_set_current_user( self::$admin_id ); + + foreach ( wp_get_all_registered_entity_action_modules()['postType'] as $post_type => $modules ) { + foreach ( $modules as $module ) { + $this->assertNotNull( wp_script_modules()->get_registered( $module ), "The $module module of $post_type is registered." ); + } + } + } + + /** + * A plugin hooking the action at the default priority can remove a core + * action, and replace one: it unregisters the action and registers its + * own definition with the script module of the core action, which keeps + * its JavaScript parts. The replaced action moves after the others. + * + * @ticket 66343 + */ + public function test_a_plugin_can_remove_and_replace_core_actions() { + $callback = static function () { + wp_unregister_entity_actions( 'postType', 'page', array( 'order-pages' ) ); + $trash = wp_unregister_entity_actions( 'postType', 'page', array( 'move-to-trash' ) ); + wp_register_entity_actions( + 'test-plugin', + 'postType', + 'page', + array( array_merge( $trash[0], array( 'label' => 'Bin…' ) ) ), + '@wordpress/core-actions/post_type_supports' + ); + }; + add_action( 'wp_entity_actions_api_init', $callback ); + wp_set_current_user( self::$admin_id ); + + $ids = array( 'view-post', 'view-post-revisions', 'rename-post', 'restore', 'reset-post', 'delete-post', 'permanently-delete' ); + $this->assertSame( array_merge( $ids, self::PAGE_ACTIONS, array( 'move-to-trash' ) ), $this->get_action_ids( 'page' ), 'The order action is removed and the trash action moves last.' ); + $this->assertSame( + array( + self::POST_TYPE_SUPPORTS => array_merge( $ids, array( 'move-to-trash' ) ), + self::PAGE => self::PAGE_ACTIONS, + ), + wp_get_registered_entity_action_modules( 'postType', 'page' ), + 'The replaced action keeps the module of the core action.' + ); + + $actions = array_column( wp_get_registered_entity_actions( 'postType', 'page' ), null, 'id' ); + $this->assertSame( 'Bin…', $actions['move-to-trash']['label'], 'The label of the plugin is used.' ); + $this->assertSame( 'test-plugin', $actions['move-to-trash']['origin']['registeredBy'], 'The plugin registered the action.' ); + $this->assertTrue( $actions['move-to-trash']['isPrimary'], 'The rest of the core definition is kept.' ); + } +} diff --git a/tests/phpunit/tests/entity-fields/wpAddEntityFieldModulesToEditorScript.php b/tests/phpunit/tests/entity-fields/wpAddEntityFieldModulesToEditorScript.php index 8dc91728ff123..f345156089e5d 100644 --- a/tests/phpunit/tests/entity-fields/wpAddEntityFieldModulesToEditorScript.php +++ b/tests/phpunit/tests/entity-fields/wpAddEntityFieldModulesToEditorScript.php @@ -79,6 +79,9 @@ public function test_the_admin_footer_action_uses_the_global_scripts() { $wp_scripts->enqueue( 'wp-editor' ); $wp_scripts->add_data( 'wp-editor', 'module_dependencies', array() ); + // The core entity actions add their own modules to the editor script. + remove_action( 'admin_footer', '_wp_add_entity_action_modules_to_editor_script', 10 ); + // Other callbacks of the action may print markup. get_echo( 'do_action', array( 'admin_footer', 'site-editor.php' ) ); From 8fc887447d3076079c9bb782e02d216c1bf99ddc Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 19 Oct 2026 19:50:47 +0100 Subject: [PATCH 2/2] Tests: Skip the core entity actions tests until the Gutenberg pin includes them. The core actions are copied from the Gutenberg build, so their tests are skipped while the pinned build does not include them. See #66343. --- .../entity-actions/registerCoreEntityActions.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php b/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php index 188edf7b1d165..c7b960d2349fe 100644 --- a/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php +++ b/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php @@ -84,6 +84,20 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$contributor_id ); } + /** + * Sets up each test. + * + * The core actions are copied from the Gutenberg build, so the tests are + * skipped until the pinned build includes them. + */ + public function set_up() { + parent::set_up(); + + if ( ! function_exists( 'register_core_entity_actions' ) ) { + $this->markTestSkipped( 'The pinned Gutenberg build does not include the core actions.' ); + } + } + /** * Tears down each test. *