diff --git a/lib/experimental/guidelines/class-gutenberg-guidelines-post-type.php b/lib/experimental/guidelines/class-gutenberg-guidelines-post-type.php index 1c4a1af7ab570c..7bc810d0692ef7 100644 --- a/lib/experimental/guidelines/class-gutenberg-guidelines-post-type.php +++ b/lib/experimental/guidelines/class-gutenberg-guidelines-post-type.php @@ -149,12 +149,41 @@ public static function register() { 'show_admin_column' => true, 'show_in_nav_menus' => false, 'show_in_rest' => true, - 'default_term' => array( - 'name' => __( 'Artifact', 'gutenberg' ), - 'slug' => self::TERM_ARTIFACT, - ), ) ); + + add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'ensure_default_type_term' ) ); + } + + /** + * Ensures a guideline post always has a type term. + * + * Assigns the `artifact` fallback when the post was saved without one. + * The REST controller sets `content` explicitly for the singleton, so + * this only applies to posts inserted through other paths. + * + * The taxonomy intentionally does not use `default_term` at registration + * time: on multisite installations with many sites, that triggers cache + * clearing work across sites even for taxonomies with zero posts. + * + * @param int $post_id Post ID. + */ + public static function ensure_default_type_term( $post_id ) { + if ( wp_is_post_revision( $post_id ) ) { + return; + } + + $terms = get_the_terms( $post_id, self::TAXONOMY ); + if ( is_wp_error( $terms ) || ! empty( $terms ) ) { + return; + } + + $term_id = self::get_or_create_term_id( self::TERM_ARTIFACT, __( 'Artifact', 'gutenberg' ) ); + if ( is_wp_error( $term_id ) ) { + return; + } + + wp_set_object_terms( $post_id, array( $term_id ), self::TAXONOMY ); } /** diff --git a/phpunit/experimental/guidelines/class-gutenberg-guidelines-post-type-test.php b/phpunit/experimental/guidelines/class-gutenberg-guidelines-post-type-test.php new file mode 100644 index 00000000000000..f50d1f45c7026d --- /dev/null +++ b/phpunit/experimental/guidelines/class-gutenberg-guidelines-post-type-test.php @@ -0,0 +1,191 @@ +user->create( array( 'role' => 'administrator' ) ); + } + + /** + * Clean up class fixtures. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + /** + * Clean up guidelines posts and taxonomy terms after each test. + */ + public function tear_down() { + $posts = get_posts( + array( + 'post_type' => Gutenberg_Guidelines_Post_Type::POST_TYPE, + 'post_status' => array( 'publish', 'draft' ), + 'posts_per_page' => -1, + ) + ); + foreach ( $posts as $post ) { + wp_delete_post( $post->ID, true ); + } + + $terms = get_terms( + array( + 'taxonomy' => Gutenberg_Guidelines_Post_Type::TAXONOMY, + 'hide_empty' => false, + ) + ); + if ( ! is_wp_error( $terms ) ) { + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, Gutenberg_Guidelines_Post_Type::TAXONOMY ); + } + } + + parent::tear_down(); + } + + /** + * The taxonomy is registered for the guidelines post type. + */ + public function test_taxonomy_is_registered() { + $this->assertTrue( taxonomy_exists( Gutenberg_Guidelines_Post_Type::TAXONOMY ) ); + } + + /** + * Regression test: the taxonomy must not be registered with `default_term`. + * + * Using `default_term` triggers cross-site `clean_term_cache` work on + * multisite installs and is the root cause this fix addresses. + */ + public function test_taxonomy_does_not_use_default_term() { + $taxonomy = get_taxonomy( Gutenberg_Guidelines_Post_Type::TAXONOMY ); + + $this->assertNotFalse( $taxonomy ); + $this->assertEmpty( $taxonomy->default_term ); + } + + /** + * A post inserted without an explicit type gets the artifact fallback. + */ + public function test_artifact_assigned_when_no_term_set() { + $post_id = wp_insert_post( + array( + 'post_type' => Gutenberg_Guidelines_Post_Type::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'No-type guideline', + ) + ); + + $this->assertIsInt( $post_id ); + $this->assertGreaterThan( 0, $post_id ); + + $terms = wp_get_object_terms( $post_id, Gutenberg_Guidelines_Post_Type::TAXONOMY, array( 'fields' => 'slugs' ) ); + + $this->assertSame( array( Gutenberg_Guidelines_Post_Type::TERM_ARTIFACT ), $terms ); + } + + /** + * A post inserted with an explicit type keeps that type. + */ + public function test_explicit_term_is_preserved() { + wp_set_current_user( self::$admin_id ); + + $content_term_id = Gutenberg_Guidelines_Post_Type::get_or_create_term_id( + Gutenberg_Guidelines_Post_Type::TERM_CONTENT, + 'Content' + ); + $this->assertIsInt( $content_term_id ); + + $post_id = wp_insert_post( + array( + 'post_type' => Gutenberg_Guidelines_Post_Type::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'Content guideline', + 'tax_input' => array( + Gutenberg_Guidelines_Post_Type::TAXONOMY => array( $content_term_id ), + ), + ), + true + ); + + $this->assertIsInt( $post_id ); + $this->assertNotWPError( $post_id ); + + $terms = wp_get_object_terms( $post_id, Gutenberg_Guidelines_Post_Type::TAXONOMY, array( 'fields' => 'slugs' ) ); + + $this->assertSame( array( Gutenberg_Guidelines_Post_Type::TERM_CONTENT ), $terms ); + } + + /** + * Updates to an existing post do not overwrite an already-assigned term. + */ + public function test_term_is_not_overwritten_on_update() { + wp_set_current_user( self::$admin_id ); + + $content_term_id = Gutenberg_Guidelines_Post_Type::get_or_create_term_id( + Gutenberg_Guidelines_Post_Type::TERM_CONTENT, + 'Content' + ); + + $post_id = wp_insert_post( + array( + 'post_type' => Gutenberg_Guidelines_Post_Type::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'Content guideline', + 'tax_input' => array( + Gutenberg_Guidelines_Post_Type::TAXONOMY => array( $content_term_id ), + ), + ), + true + ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_title' => 'Updated title', + ) + ); + + $terms = wp_get_object_terms( $post_id, Gutenberg_Guidelines_Post_Type::TAXONOMY, array( 'fields' => 'slugs' ) ); + + $this->assertSame( array( Gutenberg_Guidelines_Post_Type::TERM_CONTENT ), $terms ); + } + + /** + * The fallback is skipped for revisions (including autosaves, which are + * stored as revisions). + */ + public function test_revision_is_ignored() { + wp_set_current_user( self::$admin_id ); + + $post_id = wp_insert_post( + array( + 'post_type' => Gutenberg_Guidelines_Post_Type::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'Guideline with revision', + ) + ); + + $revision_id = wp_save_post_revision( $post_id ); + $this->assertIsInt( $revision_id ); + $this->assertGreaterThan( 0, $revision_id ); + + Gutenberg_Guidelines_Post_Type::ensure_default_type_term( $revision_id ); + + $terms = wp_get_object_terms( $revision_id, Gutenberg_Guidelines_Post_Type::TAXONOMY ); + $this->assertSame( array(), $terms ); + } +}