diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php
new file mode 100644
index 0000000000000..bb7b22ebdf2fc
--- /dev/null
+++ b/src/wp-includes/class-wp-comment-type.php
@@ -0,0 +1,240 @@
+name = $comment_type;
+
+ $this->set_props( $args );
+ }
+
+ /**
+ * Sets comment type properties.
+ *
+ * See the register_comment_type() function for accepted arguments for `$args`.
+ *
+ * @since 7.2.0
+ *
+ * @param array|string $args Array or string of arguments for registering a comment type.
+ */
+ public function set_props( $args ) {
+ $args = wp_parse_args( $args );
+
+ /**
+ * Filters the arguments for registering a comment type.
+ *
+ * @since 7.2.0
+ *
+ * @param array $args Array of arguments for registering a comment type.
+ * See the register_comment_type() function for accepted arguments.
+ * @param string $comment_type Comment type key.
+ */
+ $args = apply_filters( 'register_comment_type_args', $args, $this->name );
+
+ $comment_type = $this->name;
+
+ /**
+ * Filters the arguments for registering a specific comment type.
+ *
+ * The dynamic portion of the filter name, `$comment_type`, refers to the comment type key.
+ *
+ * Possible hook names include:
+ *
+ * - `register_comment_comment_type_args`
+ * - `register_pingback_comment_type_args`
+ *
+ * @since 7.2.0
+ *
+ * @param array $args Array of arguments for registering a comment type.
+ * See the register_comment_type() function for accepted arguments.
+ * @param string $comment_type Comment type key.
+ */
+ $args = apply_filters( "register_{$comment_type}_comment_type_args", $args, $this->name );
+
+ /*
+ * Note: 'label' is intentionally omitted from the defaults. Leaving the property
+ * unset (null) lets get_comment_type_labels() fall back to the default labels, the
+ * same way WP_Post_Type and WP_Taxonomy behave. A 'label' default of false would be
+ * treated as a provided value and overwrite the default name with false.
+ */
+ $defaults = array(
+ 'labels' => array(),
+ 'description' => '',
+ 'public' => true,
+ 'internal' => false,
+ '_builtin' => false,
+ );
+
+ $args = array_merge( $defaults, $args );
+
+ $args['name'] = $this->name;
+
+ /*
+ * Comment types are never hierarchical. The property exists only so the shared
+ * label helper can pick a slot, and the hierarchical slot is deliberately null,
+ * so honoring a provided value would resolve every default label to null.
+ */
+ $args['hierarchical'] = false;
+
+ foreach ( $args as $property_name => $property_value ) {
+ $this->$property_name = $property_value;
+ }
+
+ $this->labels = get_comment_type_labels( $this );
+ $this->label = $this->labels->name;
+ }
+
+ /**
+ * Returns the default labels for comment types.
+ *
+ * @since 7.2.0
+ *
+ * @return (string|null)[][] The default labels for comment types.
+ */
+ public static function get_default_labels() {
+ if ( ! empty( self::$default_labels ) ) {
+ return self::$default_labels;
+ }
+
+ self::$default_labels = array(
+ 'name' => array( _x( 'Comments', 'comment type general name' ), null ),
+ 'singular_name' => array( _x( 'Comment', 'comment type singular name' ), null ),
+ );
+
+ return self::$default_labels;
+ }
+
+ /**
+ * Resets the cache for the default labels.
+ *
+ * @since 7.2.0
+ */
+ public static function reset_default_labels() {
+ self::$default_labels = array();
+ }
+}
diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php
index 43bd68ff972a4..77702035862dc 100644
--- a/src/wp-includes/comment-template.php
+++ b/src/wp-includes/comment-template.php
@@ -1179,12 +1179,17 @@ function get_comment_type( $comment_id = 0 ) {
* Displays the comment type of the current comment.
*
* @since 0.71
+ * @since 7.2.0 The default output for a registered non-built-in comment type
+ * falls back to the type's singular name label.
*
* @param string|false $comment_text Optional. String to display for comment type. Default false.
* @param string|false $trackback_text Optional. String to display for trackback type. Default false.
* @param string|false $pingback_text Optional. String to display for pingback type. Default false.
*/
function comment_type( $comment_text = false, $trackback_text = false, $pingback_text = false ) {
+ // Whether the caller supplied custom text for the default comment label.
+ $comment_text_overridden = ( false !== $comment_text );
+
if ( false === $comment_text ) {
$comment_text = _x( 'Comment', 'noun' );
}
@@ -1203,7 +1208,18 @@ function comment_type( $comment_text = false, $trackback_text = false, $pingback
echo $pingback_text;
break;
default:
- echo $comment_text;
+ /*
+ * For a registered, non-built-in comment type, fall back to its singular label
+ * when the caller did not supply custom text. Built-in types and explicit
+ * overrides keep their existing output.
+ */
+ $comment_type_object = $comment_text_overridden ? null : get_comment_type_object( $type );
+
+ if ( $comment_type_object && ! $comment_type_object->_builtin && isset( $comment_type_object->labels->singular_name ) ) {
+ echo esc_html( $comment_type_object->labels->singular_name );
+ } else {
+ echo $comment_text;
+ }
}
}
diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php
index 7557e9258c87f..10998c7c5d396 100644
--- a/src/wp-includes/comment.php
+++ b/src/wp-includes/comment.php
@@ -330,6 +330,389 @@ function get_comments( $args = '' ) {
return $query->query( $args );
}
+/**
+ * Creates the initial comment types when 'init' action is fired.
+ *
+ * See register_comment_type() for accepted arguments.
+ *
+ * @since 7.2.0
+ */
+function create_initial_comment_types() {
+ WP_Comment_Type::reset_default_labels();
+
+ /*
+ * The 'comment', 'pingback', and 'trackback' labels deliberately reuse existing
+ * core translation strings, while 'note' introduces new strings with explicit
+ * contexts. Do not normalize one style to the other.
+ */
+ register_comment_type(
+ 'comment',
+ array(
+ 'label' => __( 'Comments' ),
+ 'labels' => array(
+ 'singular_name' => _x( 'Comment', 'noun' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'pingback',
+ array(
+ 'label' => __( 'Pingbacks' ),
+ 'labels' => array(
+ 'singular_name' => __( 'Pingback' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'trackback',
+ array(
+ 'label' => __( 'Trackbacks' ),
+ 'labels' => array(
+ 'singular_name' => __( 'Trackback' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'note',
+ array(
+ 'label' => _x( 'Notes', 'comment type general name' ),
+ 'labels' => array(
+ 'singular_name' => _x( 'Note', 'comment type singular name' ),
+ ),
+ 'public' => false,
+ 'internal' => true,
+ '_builtin' => true,
+ )
+ );
+}
+
+/**
+ * Registers a comment type.
+ *
+ * Note: Comment type registrations should not be hooked before the {@see 'init'} action.
+ * Registering a comment type earlier can result in its labels being generated before
+ * the current locale's translations are loaded.
+ *
+ * Comment types are stored verbatim in the `comment_type` column of the comments table.
+ * Registration provides labels and metadata for a type; it does not constrain which values
+ * may be stored. Comment types that are stored but never registered keep behaving exactly
+ * as they did before this API existed.
+ *
+ * Each argument drives exactly one layer of behavior, so the flags stay independently
+ * meaningful as more of them are added:
+ *
+ * - `public` states display-surface intent: whether the type is meant to be seen by site
+ * visitors. It does not affect what queries return.
+ * - `internal` states query-surface intent: whether the type should be excluded from
+ * comment queries and counts by default.
+ *
+ * An argument never implies another argument. The one cascade planned for the future is
+ * `show_in_rest`, which will default from `public`.
+ *
+ * Registrations live in a per-process global. Like post types, they are not scoped to a
+ * site on multisite: a type registered by one site's plugins is visible after
+ * switch_to_blog() for the rest of the request.
+ *
+ * Cannot be used to re-register built-in comment types. The names WP_Comment_Query reads
+ * as query tokens ('all', 'comments', 'pings') cannot be registered either.
+ *
+ * @since 7.2.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type Comment type key. Must not exceed 20 characters and may only
+ * contain lowercase alphanumeric characters, dashes, and underscores.
+ * See sanitize_key().
+ * @param array|string $args {
+ * Optional. Array or string of arguments for registering a comment type. Default empty array.
+ *
+ * @type string $label Name of the comment type. Usually plural.
+ * Default is the value of $labels['name'].
+ * @type string[] $labels An array of labels for this comment type. If not set, the
+ * default comment labels are used. See get_comment_type_labels()
+ * for a full list of supported labels.
+ * @type string $description A short descriptive summary of what the comment type is.
+ * Default empty.
+ * @type bool $public Whether the comment type is intended for use publicly either via
+ * the admin interface or by front-end users. Core does not
+ * currently act on this argument. Default true.
+ * @type bool $internal Whether the comment type is for internal use only. Internal
+ * types are meant to be excluded from comment queries and counts
+ * by default. Core does not currently act on this argument.
+ * Default false.
+ * @type bool $_builtin For internal core use only. Marks the type as native to
+ * WordPress, which blocks it from being re-registered or
+ * unregistered. Default false.
+ * }
+ * @return WP_Comment_Type|WP_Error The registered comment type object on success,
+ * WP_Error object on failure.
+ */
+function register_comment_type( $comment_type, $args = array() ) {
+ global $wp_comment_types;
+
+ if ( ! is_array( $wp_comment_types ) ) {
+ $wp_comment_types = array();
+ }
+
+ $args = wp_parse_args( $args );
+
+ // Sanitize comment type name.
+ $comment_type = sanitize_key( $comment_type );
+
+ if ( empty( $comment_type ) || strlen( $comment_type ) > 20 ) {
+ _doing_it_wrong( __FUNCTION__, __( 'Comment type names must be between 1 and 20 characters in length.' ), '7.2.0' );
+ return new WP_Error( 'comment_type_length_invalid', __( 'Comment type names must be between 1 and 20 characters in length.' ) );
+ }
+
+ /*
+ * Re-registering a built-in comment type could strip flags that core relies on
+ * for rendering and query behavior, so it is not allowed. Core's own repeated
+ * registrations (on 'init' and 'change_locale') pass '_builtin' and are exempt.
+ */
+ if ( isset( $wp_comment_types[ $comment_type ] )
+ && $wp_comment_types[ $comment_type ]->_builtin
+ && empty( $args['_builtin'] )
+ ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ /* translators: %s: Comment type key. */
+ __( 'The "%s" comment type is a built-in type and cannot be re-registered.' ),
+ $comment_type
+ ),
+ '7.2.0'
+ );
+ return new WP_Error( 'comment_type_builtin', __( 'Built-in comment types cannot be re-registered.' ) );
+ }
+
+ /*
+ * WP_Comment_Query reads these names as query tokens rather than as literal
+ * comment_type values, so a type registered under one of them could never be
+ * queried for on its own.
+ */
+ if ( in_array( $comment_type, array( 'all', 'comments', 'pings' ), true ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ /* translators: %s: Comment type key. */
+ __( 'The "%s" comment type name is reserved for use by WP_Comment_Query.' ),
+ $comment_type
+ ),
+ '7.2.0'
+ );
+ return new WP_Error( 'comment_type_reserved', __( 'This comment type name is reserved.' ) );
+ }
+
+ $comment_type_object = new WP_Comment_Type( $comment_type, $args );
+
+ $wp_comment_types[ $comment_type ] = $comment_type_object;
+
+ /**
+ * Fires after a comment type is registered.
+ *
+ * @since 7.2.0
+ *
+ * @param string $comment_type Comment type key.
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ */
+ do_action( 'registered_comment_type', $comment_type, $comment_type_object );
+
+ /**
+ * Fires after a specific comment type is registered.
+ *
+ * The dynamic portion of the filter name, `$comment_type`, refers to the comment type key.
+ *
+ * Possible hook names include:
+ *
+ * - `registered_comment_type_comment`
+ * - `registered_comment_type_pingback`
+ *
+ * @since 7.2.0
+ *
+ * @param string $comment_type Comment type key.
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ */
+ do_action( "registered_comment_type_{$comment_type}", $comment_type, $comment_type_object );
+
+ return $comment_type_object;
+}
+
+/**
+ * Unregisters a comment type.
+ *
+ * Cannot be used to unregister built-in comment types.
+ *
+ * @since 7.2.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type Comment type key.
+ * @return true|WP_Error True on success, WP_Error on failure or if the comment type doesn't exist.
+ */
+function unregister_comment_type( $comment_type ) {
+ global $wp_comment_types;
+
+ if ( ! comment_type_exists( $comment_type ) ) {
+ return new WP_Error( 'invalid_comment_type', __( 'Invalid comment type.' ) );
+ }
+
+ $comment_type_object = get_comment_type_object( $comment_type );
+
+ // Do not allow unregistering built-in comment types.
+ if ( $comment_type_object->_builtin ) {
+ return new WP_Error( 'invalid_comment_type', __( 'Unregistering a built-in comment type is not allowed.' ) );
+ }
+
+ unset( $wp_comment_types[ $comment_type ] );
+
+ /**
+ * Fires after a comment type is unregistered.
+ *
+ * @since 7.2.0
+ *
+ * @param string $comment_type Comment type key.
+ */
+ do_action( 'unregistered_comment_type', $comment_type );
+
+ return true;
+}
+
+/**
+ * Retrieves a comment type object by name.
+ *
+ * @since 7.2.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type The name of a registered comment type.
+ * @return WP_Comment_Type|null WP_Comment_Type object if it exists, null otherwise.
+ */
+function get_comment_type_object( $comment_type ) {
+ global $wp_comment_types;
+
+ if ( ! is_scalar( $comment_type ) || empty( $wp_comment_types[ $comment_type ] ) ) {
+ return null;
+ }
+
+ return $wp_comment_types[ $comment_type ];
+}
+
+/**
+ * Retrieves a list of registered comment type names or objects.
+ *
+ * @since 7.2.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param array $args Optional. An array of key => value arguments to match against
+ * the comment type objects. Default empty array.
+ * @param string $output Optional. The type of output to return. Either comment type 'names'
+ * or 'objects'. Default 'names'.
+ * @param string $operator Optional. The logical operation to perform. 'or' means only one
+ * element from the array needs to match; 'and' means all elements
+ * must match; 'not' means no elements may match. Default 'and'.
+ * @return string[]|WP_Comment_Type[] An array of comment type names or objects.
+ */
+function get_comment_types( $args = array(), $output = 'names', $operator = 'and' ) {
+ global $wp_comment_types;
+
+ $field = ( 'names' === $output ) ? 'name' : false;
+
+ return wp_filter_object_list( $wp_comment_types, $args, $operator, $field );
+}
+
+/**
+ * Determines whether a comment type is registered.
+ *
+ * @since 7.2.0
+ *
+ * @param string $comment_type Comment type name.
+ * @return bool Whether the comment type is registered.
+ */
+function comment_type_exists( $comment_type ) {
+ return (bool) get_comment_type_object( $comment_type );
+}
+
+/**
+ * Builds an object with all comment type labels out of a comment type object.
+ *
+ * @since 7.2.0
+ *
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ * @return object {
+ * Comment type labels object.
+ *
+ * @type string $name General name for the comment type, usually plural. The same as and
+ * overridden by `$comment_type_object->label`. Default 'Comments'.
+ * @type string $singular_name Name for one object of this comment type. Default 'Comment'.
+ * @type string $menu_name Label for the menu name. Default is the same as `name`.
+ * }
+ */
+function get_comment_type_labels( $comment_type_object ) {
+ $nohier_vs_hier_defaults = WP_Comment_Type::get_default_labels();
+
+ $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name'];
+
+ $provided_labels = (array) $comment_type_object->labels;
+
+ /*
+ * _get_custom_object_labels() writes every label it derives back onto the object
+ * it is given, including the post-type-only labels removed below. Hand it a copy
+ * so calling this function on a registered comment type leaves the registered
+ * object untouched.
+ */
+ $labels = _get_custom_object_labels( clone $comment_type_object, $nohier_vs_hier_defaults );
+
+ /*
+ * _get_custom_object_labels() derives labels that only apply to post types.
+ * Remove them unless they were explicitly provided at registration.
+ */
+ foreach ( array( 'name_admin_bar', 'all_items', 'archives' ) as $post_type_only_label ) {
+ if ( ! array_key_exists( $post_type_only_label, $provided_labels ) ) {
+ unset( $labels->$post_type_only_label );
+ }
+ }
+
+ $comment_type = $comment_type_object->name;
+
+ $default_labels = clone $labels;
+
+ /**
+ * Filters the labels of a specific comment type.
+ *
+ * The dynamic portion of the hook name, `$comment_type`, refers to the comment type slug.
+ *
+ * Possible hook names include:
+ *
+ * - `comment_type_labels_comment`
+ * - `comment_type_labels_pingback`
+ *
+ * Labels are stored unescaped, mirroring the post type and taxonomy label
+ * contract; callers must escape them on output (for example with esc_html()).
+ *
+ * @since 7.2.0
+ *
+ * @see get_comment_type_labels() for the full list of comment type labels.
+ *
+ * @param object $labels Object with labels for the comment type as member variables.
+ */
+ $labels = apply_filters( "comment_type_labels_{$comment_type}", $labels );
+
+ // Ensure that the filtered labels contain all required default values.
+ $labels = (object) array_merge( (array) $default_labels, (array) $labels );
+
+ return $labels;
+}
+
/**
* Retrieves all of the WordPress supported comment statuses.
*
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 12ca0045b98b4..39e45451b8665 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -528,6 +528,10 @@
add_action( 'split_shared_term', '_wp_check_split_nav_menu_terms', 10, 4 );
add_action( 'wp_split_shared_term_batch', '_wp_batch_split_terms' );
+// Comment types.
+add_action( 'init', 'create_initial_comment_types', 0 ); // Highest priority.
+add_action( 'change_locale', 'create_initial_comment_types' );
+
// Comment type updates.
add_action( 'admin_init', '_wp_check_for_scheduled_update_comment_type' );
add_action( 'wp_update_comment_type_batch', '_wp_batch_update_comment_type' );
diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php
index 2db73e9a20476..fa01a58d0df49 100644
--- a/src/wp-includes/post.php
+++ b/src/wp-includes/post.php
@@ -2221,8 +2221,8 @@ function get_post_type_labels( $post_type_object ) {
}
/**
- * Builds an object with custom-something object (post type, taxonomy) labels
- * out of a custom-something object
+ * Builds an object with custom-something object (post type, taxonomy, comment type)
+ * labels out of a custom-something object
*
* @since 3.0.0
* @access private
diff --git a/src/wp-settings.php b/src/wp-settings.php
index 29b7588b96b4f..884243e45ba0a 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -233,6 +233,7 @@
require ABSPATH . WPINC . '/comment.php';
require ABSPATH . WPINC . '/class-wp-comment.php';
require ABSPATH . WPINC . '/class-wp-comment-query.php';
+require ABSPATH . WPINC . '/class-wp-comment-type.php';
require ABSPATH . WPINC . '/class-walker-comment.php';
require ABSPATH . WPINC . '/comment-template.php';
require ABSPATH . WPINC . '/rewrite.php';
@@ -560,10 +561,11 @@
// Create common globals.
require ABSPATH . WPINC . '/vars.php';
-// Make taxonomies and posts available to plugins and themes.
+// Make taxonomies, posts, and comment types available to plugins and themes.
// @plugin authors: warning: these get registered again on the init hook.
create_initial_taxonomies();
create_initial_post_types();
+create_initial_comment_types();
wp_start_scraping_edited_file_errors();
diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php
index c87e5e7fd077c..9e7b1351412a6 100644
--- a/tests/phpunit/includes/abstract-testcase.php
+++ b/tests/phpunit/includes/abstract-testcase.php
@@ -123,15 +123,16 @@ public function set_up() {
$this->clean_up_global_scope();
/*
- * When running core tests, ensure that post types and taxonomies
- * are reset for each test. We skip this step for non-core tests,
- * given the large number of plugins that register post types and
- * taxonomies at 'init'.
+ * When running core tests, ensure that post types, taxonomies,
+ * and comment types are reset for each test. We skip this step
+ * for non-core tests, given the large number of plugins that
+ * register post types and taxonomies at 'init'.
*/
if ( defined( 'WP_RUN_CORE_TESTS' ) && WP_RUN_CORE_TESTS ) {
$this->reset_post_types();
$this->reset_taxonomies();
$this->reset_post_statuses();
+ $this->reset_comment_types();
$this->reset__SERVER();
if ( $wp_rewrite->permalink_structure ) {
@@ -350,6 +351,24 @@ protected function reset_taxonomies() {
create_initial_taxonomies();
}
+ /**
+ * Unregisters existing comment types and registers defaults.
+ *
+ * Run before each test in order to clean up the global scope, in case
+ * a test forgets to unregister a comment type on its own, or fails before
+ * it has a chance to do so.
+ *
+ * The registry is emptied outright rather than unregistered type by type, since a
+ * test can register a custom type with '_builtin' set and that type would otherwise
+ * survive into the next test. Registering a comment type creates no hooks, rewrite
+ * rules, or meta boxes, so dropping the registry is complete cleanup.
+ */
+ protected function reset_comment_types() {
+ $GLOBALS['wp_comment_types'] = array();
+
+ create_initial_comment_types();
+ }
+
/**
* Unregisters non-built-in post statuses.
*/
diff --git a/tests/phpunit/tests/comment/commentType.php b/tests/phpunit/tests/comment/commentType.php
new file mode 100644
index 0000000000000..7d52cc9a20bdb
--- /dev/null
+++ b/tests/phpunit/tests/comment/commentType.php
@@ -0,0 +1,145 @@
+post->create();
+ }
+
+ /**
+ * Returns the output of comment_type() for a comment of the given type.
+ *
+ * @param string $type Comment type stored on the comment.
+ * @param mixed ...$args Optional arguments passed through to comment_type().
+ * @return string Captured output.
+ */
+ private function get_comment_type_output( $type, ...$args ) {
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => self::$post_id,
+ 'comment_type' => $type,
+ )
+ );
+
+ $GLOBALS['comment'] = get_comment( $comment_id );
+
+ ob_start();
+ comment_type( ...$args );
+ $output = ob_get_clean();
+
+ unset( $GLOBALS['comment'] );
+
+ return $output;
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_built_in_types_output_is_unchanged() {
+ $this->assertSame( 'Comment', $this->get_comment_type_output( 'comment' ) );
+ $this->assertSame( 'Trackback', $this->get_comment_type_output( 'trackback' ) );
+ $this->assertSame( 'Pingback', $this->get_comment_type_output( 'pingback' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_custom_text_overrides_are_respected() {
+ $this->assertSame( 'C', $this->get_comment_type_output( 'comment', 'C', 'T', 'P' ) );
+ $this->assertSame( 'T', $this->get_comment_type_output( 'trackback', 'C', 'T', 'P' ) );
+ $this->assertSame( 'P', $this->get_comment_type_output( 'pingback', 'C', 'T', 'P' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_registered_custom_type_outputs_its_label() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $this->assertSame( 'Foo', $this->get_comment_type_output( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_unregistered_custom_type_falls_back_to_default_label() {
+ $this->assertSame( _x( 'Comment', 'noun' ), $this->get_comment_type_output( 'bar' ) );
+ }
+
+ /**
+ * A comment stored with the legacy empty string type is treated as 'comment'.
+ *
+ * @ticket 35214
+ */
+ public function test_legacy_empty_type_outputs_comment() {
+ $this->assertSame( 'Comment', $this->get_comment_type_output( '' ) );
+ }
+
+ /**
+ * The label fallback must not apply to built-in types: 'note' has 'Note' labels
+ * but comment_type() output stays 'Comment'.
+ *
+ * @ticket 35214
+ */
+ public function test_built_in_note_type_outputs_default_comment_text() {
+ $this->assertSame( 'Comment', $this->get_comment_type_output( 'note' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_custom_text_override_wins_over_registered_label() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $this->assertSame( 'Custom', $this->get_comment_type_output( 'foo', 'Custom' ) );
+ }
+
+ /**
+ * The registered label is escaped on output to guard against HTML/script injection.
+ *
+ * @ticket 35214
+ */
+ public function test_registered_label_is_escaped_on_output() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => '
Foo',
+ ),
+ )
+ );
+
+ $this->assertSame(
+ esc_html( '
Foo' ),
+ $this->get_comment_type_output( 'foo' )
+ );
+ }
+}
diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php
new file mode 100644
index 0000000000000..58fdb791127b0
--- /dev/null
+++ b/tests/phpunit/tests/comment/types.php
@@ -0,0 +1,718 @@
+assertNull( get_comment_type_object( 'foo' ) );
+
+ register_comment_type( 'foo' );
+
+ $cobj = get_comment_type_object( 'foo' );
+ $this->assertInstanceOf( 'WP_Comment_Type', $cobj );
+ $this->assertSame( 'foo', $cobj->name );
+
+ // Test some defaults.
+ $this->assertTrue( $cobj->public );
+ $this->assertFalse( $cobj->internal );
+ $this->assertFalse( $cobj->_builtin );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ * @covers ::get_comment_type_labels
+ */
+ public function test_register_comment_type_without_labels_uses_default_labels() {
+ register_comment_type( 'foo' );
+
+ $cobj = get_comment_type_object( 'foo' );
+
+ $this->assertSame( 'Comments', $cobj->label );
+ $this->assertSame( 'Comments', $cobj->labels->name );
+ $this->assertSame( 'Comment', $cobj->labels->singular_name );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_register_comment_type_return_value() {
+ $this->assertInstanceOf( 'WP_Comment_Type', register_comment_type( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ *
+ * @expectedIncorrectUsage register_comment_type
+ */
+ public function test_register_comment_type_with_too_long_name() {
+ $this->assertInstanceOf( 'WP_Error', register_comment_type( 'comment_type_with_a_too_long_name' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ *
+ * @expectedIncorrectUsage register_comment_type
+ */
+ public function test_register_comment_type_with_empty_name() {
+ $this->assertInstanceOf( 'WP_Error', register_comment_type( '' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::create_initial_comment_types
+ */
+ public function test_built_in_comment_types_are_registered() {
+ $this->assertTrue( comment_type_exists( 'comment' ) );
+ $this->assertTrue( comment_type_exists( 'pingback' ) );
+ $this->assertTrue( comment_type_exists( 'trackback' ) );
+ $this->assertTrue( comment_type_exists( 'note' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::create_initial_comment_types
+ */
+ public function test_built_in_note_type_is_internal_and_non_public() {
+ $note = get_comment_type_object( 'note' );
+
+ $this->assertTrue( $note->internal );
+ $this->assertFalse( $note->public );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::comment_type_exists
+ */
+ public function test_comment_type_exists() {
+ $this->assertFalse( comment_type_exists( 'foo' ) );
+
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( comment_type_exists( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_names() {
+ register_comment_type( 'foo' );
+
+ $types = get_comment_types();
+
+ $this->assertContains( 'comment', $types );
+ $this->assertContains( 'foo', $types );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_objects() {
+ register_comment_type( 'foo' );
+
+ $types = get_comment_types( array(), 'objects' );
+
+ $this->assertInstanceOf( 'WP_Comment_Type', $types['foo'] );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_filtered_by_property() {
+ register_comment_type( 'foo', array( 'public' => false ) );
+
+ $public = get_comment_types( array( 'public' => true ) );
+
+ $this->assertContains( 'comment', $public );
+ $this->assertNotContains( 'foo', $public );
+ $this->assertNotContains( 'note', $public );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type() {
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( unregister_comment_type( 'foo' ) );
+ $this->assertNull( get_comment_type_object( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type_unknown_returns_error() {
+ $this->assertWPError( unregister_comment_type( 'does_not_exist' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type_twice_returns_error() {
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( unregister_comment_type( 'foo' ) );
+ $this->assertWPError( unregister_comment_type( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ *
+ * @dataProvider data_built_in_comment_types
+ */
+ public function test_unregister_built_in_comment_type_is_not_allowed( $comment_type ) {
+ $this->assertWPError( unregister_comment_type( $comment_type ) );
+ $this->assertTrue( comment_type_exists( $comment_type ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_built_in_comment_types() {
+ return array(
+ array( 'comment' ),
+ array( 'pingback' ),
+ array( 'trackback' ),
+ array( 'note' ),
+ );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ *
+ * @expectedIncorrectUsage register_comment_type
+ *
+ * @dataProvider data_built_in_comment_types
+ */
+ public function test_register_built_in_comment_type_is_rejected( $comment_type ) {
+ $original_label = get_comment_type_object( $comment_type )->label;
+
+ $result = register_comment_type( $comment_type, array( 'label' => 'Hijacked' ) );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'comment_type_builtin', $result->get_error_code() );
+ $this->assertSame( $original_label, get_comment_type_object( $comment_type )->label );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_register_comment_type_twice_overwrites_previous_registration() {
+ register_comment_type( 'foo', array( 'label' => 'First' ) );
+ register_comment_type( 'foo', array( 'label' => 'Second' ) );
+
+ $this->assertSame( 'Second', get_comment_type_object( 'foo' )->label );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ * @covers ::unregister_comment_type
+ */
+ public function test_register_after_unregister_succeeds() {
+ register_comment_type( 'foo' );
+ unregister_comment_type( 'foo' );
+
+ $this->assertInstanceOf( 'WP_Comment_Type', register_comment_type( 'foo' ) );
+ $this->assertTrue( comment_type_exists( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_registered_comment_type_actions_fire() {
+ $action = new MockAction();
+ $action_for_foo = new MockAction();
+
+ add_action( 'registered_comment_type', array( $action, 'action' ) );
+ add_action( 'registered_comment_type_foo', array( $action_for_foo, 'action' ) );
+
+ register_comment_type( 'foo' );
+
+ $this->assertSame( 1, $action->get_call_count() );
+ $this->assertSame( 1, $action_for_foo->get_call_count() );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregistered_comment_type_action_fires() {
+ register_comment_type( 'foo' );
+
+ $action = new MockAction();
+ add_action( 'unregistered_comment_type', array( $action, 'action' ) );
+
+ unregister_comment_type( 'foo' );
+
+ $this->assertSame( 1, $action->get_call_count() );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_register_comment_type_with_20_character_name_succeeds() {
+ $comment_type = str_repeat( 'a', 20 );
+
+ $this->assertInstanceOf( 'WP_Comment_Type', register_comment_type( $comment_type ) );
+ $this->assertTrue( comment_type_exists( $comment_type ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_register_comment_type_name_is_sanitized() {
+ $comment_type_object = register_comment_type( 'Foo Bar!' );
+
+ $this->assertSame( 'foobar', $comment_type_object->name );
+ $this->assertFalse( comment_type_exists( 'Foo Bar!' ) );
+ $this->assertTrue( comment_type_exists( 'foobar' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_with_or_operator() {
+ register_comment_type( 'foo', array( 'public' => false ) );
+
+ $types = get_comment_types(
+ array(
+ 'public' => true,
+ 'internal' => true,
+ ),
+ 'names',
+ 'or'
+ );
+
+ // 'comment' matches on public, 'note' matches on internal.
+ $this->assertContains( 'comment', $types );
+ $this->assertContains( 'note', $types );
+ $this->assertNotContains( 'foo', $types );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_with_not_operator() {
+ register_comment_type( 'foo', array( 'internal' => true ) );
+
+ $types = get_comment_types( array( 'internal' => true ), 'names', 'not' );
+
+ $this->assertContains( 'comment', $types );
+ $this->assertNotContains( 'note', $types );
+ $this->assertNotContains( 'foo', $types );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ */
+ public function test_get_comment_types_names_output_is_keyed_by_type_name() {
+ register_comment_type( 'foo' );
+
+ $types = get_comment_types();
+
+ $this->assertSame( 'foo', $types['foo'] );
+ $this->assertSame( 'comment', $types['comment'] );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_object
+ */
+ public function test_get_comment_type_object_with_non_scalar_returns_null() {
+ $this->assertNull( get_comment_type_object( array() ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::create_initial_comment_types
+ */
+ public function test_create_initial_comment_types_is_idempotent() {
+ create_initial_comment_types();
+ create_initial_comment_types();
+
+ $this->assertCount( 4, get_comment_types( array( '_builtin' => true ) ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_labels_are_built_from_args() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'label' => 'Foos',
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $cobj = get_comment_type_object( 'foo' );
+
+ $this->assertSame( 'Foos', $cobj->label );
+ $this->assertSame( 'Foos', $cobj->labels->name );
+ $this->assertSame( 'Foo', $cobj->labels->singular_name );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_comment_type_labels_filter() {
+ add_filter(
+ 'comment_type_labels_foo',
+ static function ( $labels ) {
+ $labels->singular_name = 'Filtered Foo';
+ return $labels;
+ }
+ );
+
+ register_comment_type( 'foo' );
+
+ $this->assertSame( 'Filtered Foo', get_comment_type_object( 'foo' )->labels->singular_name );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_label_only_registration_populates_label_fallback_chain() {
+ register_comment_type( 'foo', array( 'label' => 'Foos' ) );
+
+ $labels = get_comment_type_object( 'foo' )->labels;
+
+ $this->assertSame( 'Foos', $labels->name );
+ $this->assertSame( 'Foos', $labels->singular_name );
+ $this->assertSame( 'Foos', $labels->menu_name );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_labels_do_not_include_post_type_only_labels() {
+ register_comment_type( 'foo', array( 'label' => 'Foos' ) );
+
+ $labels = get_comment_type_object( 'foo' )->labels;
+
+ $this->assertObjectNotHasProperty( 'name_admin_bar', $labels );
+ $this->assertObjectNotHasProperty( 'all_items', $labels );
+ $this->assertObjectNotHasProperty( 'archives', $labels );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_get_comment_type_labels_does_not_modify_a_registered_type() {
+ register_comment_type( 'foo', array( 'label' => 'Foos' ) );
+
+ $comment_type_object = get_comment_type_object( 'foo' );
+ $registered_labels = get_object_vars( $comment_type_object->labels );
+
+ $first = get_comment_type_labels( $comment_type_object );
+ $second = get_comment_type_labels( $comment_type_object );
+
+ $this->assertSame( $registered_labels, get_object_vars( $comment_type_object->labels ), 'The registered labels should not change.' );
+ $this->assertEquals( $first, $second, 'Repeated calls should return the same labels.' );
+ $this->assertObjectNotHasProperty( 'archives', $second, 'Post-type-only labels should not appear on repeated calls.' );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_labels_do_not_spawn_post_type_only_labels_from_menu_name() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'label' => 'Foos',
+ 'labels' => array(
+ 'menu_name' => 'Foo Menu',
+ ),
+ )
+ );
+
+ $labels = get_comment_type_object( 'foo' )->labels;
+
+ $this->assertSame( 'Foo Menu', $labels->menu_name );
+ $this->assertObjectNotHasProperty( 'all_items', $labels );
+ $this->assertObjectNotHasProperty( 'archives', $labels );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_comment_type_labels
+ */
+ public function test_comment_type_labels_filter_missing_name_is_backfilled() {
+ add_filter(
+ 'comment_type_labels_foo',
+ static function ( $labels ) {
+ unset( $labels->name );
+ return $labels;
+ }
+ );
+
+ register_comment_type( 'foo', array( 'label' => 'Foos' ) );
+
+ $this->assertSame( 'Foos', get_comment_type_object( 'foo' )->labels->name );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_registered_comment_type_action_receives_type_and_object() {
+ $action = new MockAction();
+
+ add_action( 'registered_comment_type', array( $action, 'action' ), 10, 2 );
+
+ register_comment_type( 'foo' );
+
+ $args = $action->get_args();
+
+ $this->assertSame( 'foo', $args[0][0] );
+ $this->assertInstanceOf( 'WP_Comment_Type', $args[0][1] );
+ $this->assertSame( 'foo', $args[0][1]->name );
+ }
+
+ /**
+ * Comment types are never hierarchical. The default labels reserve the hierarchical
+ * slot as null, so honoring a provided value would resolve every label to null.
+ *
+ * @ticket 35214
+ *
+ * @covers WP_Comment_Type::set_props
+ */
+ public function test_register_comment_type_ignores_hierarchical_argument() {
+ register_comment_type( 'foo', array( 'hierarchical' => true ) );
+
+ $cobj = get_comment_type_object( 'foo' );
+
+ $this->assertFalse( $cobj->hierarchical, 'A comment type should never be hierarchical.' );
+ $this->assertSame( 'Comments', $cobj->label, 'The default label should survive the argument.' );
+ $this->assertSame( 'Comments', $cobj->labels->name );
+ $this->assertSame( 'Comment', $cobj->labels->singular_name );
+ }
+
+ /**
+ * The names WP_Comment_Query reads as query tokens cannot be registered, since a type
+ * stored under one of them could never be queried for on its own.
+ *
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ *
+ * @dataProvider data_reserved_comment_type_names
+ *
+ * @expectedIncorrectUsage register_comment_type
+ *
+ * @param string $comment_type Reserved comment type name.
+ */
+ public function test_register_reserved_comment_type_is_rejected( string $comment_type ) {
+ $result = register_comment_type( $comment_type );
+
+ $this->assertInstanceOf( 'WP_Error', $result );
+ $this->assertSame( 'comment_type_reserved', $result->get_error_code() );
+ $this->assertNull( get_comment_type_object( $comment_type ) );
+ }
+
+ /**
+ * Data provider for test_register_reserved_comment_type_is_rejected().
+ *
+ * @return array
+ */
+ public function data_reserved_comment_type_names(): array {
+ return array(
+ 'all type token' => array( 'all' ),
+ 'comments alias' => array( 'comments' ),
+ 'pings bucket key' => array( 'pings' ),
+ );
+ }
+
+ /**
+ * The built-in guard reads the raw arguments, so passing '_builtin' bypasses it. This
+ * matches register_post_type(), where '_builtin' is an accepted internal-use argument
+ * with no guard at all. Pinned so the follow-ups that give '_builtin' more meaning
+ * cannot change it by accident.
+ *
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_register_comment_type_builtin_argument_bypasses_the_built_in_guard() {
+ $result = register_comment_type(
+ 'pingback',
+ array(
+ '_builtin' => true,
+ 'label' => 'Hijacked',
+ )
+ );
+
+ $this->assertInstanceOf( 'WP_Comment_Type', $result, 'Passing _builtin bypasses the guard.' );
+ $this->assertSame( 'Hijacked', get_comment_type_object( 'pingback' )->label );
+ }
+
+ /**
+ * Passing '_builtin' on a custom type makes it behave like a built-in for everyone
+ * else: it can no longer be re-registered or unregistered.
+ *
+ * @ticket 35214
+ *
+ * @covers ::register_comment_type
+ * @covers ::unregister_comment_type
+ *
+ * @expectedIncorrectUsage register_comment_type
+ */
+ public function test_register_comment_type_builtin_argument_locks_a_custom_type() {
+ register_comment_type( 'foo', array( '_builtin' => true ) );
+
+ $this->assertInstanceOf( 'WP_Error', register_comment_type( 'foo' ) );
+ $this->assertInstanceOf( 'WP_Error', unregister_comment_type( 'foo' ) );
+ }
+
+ /**
+ * Built-in labels are rebuilt on a locale change rather than served from the static
+ * default-labels cache.
+ *
+ * @ticket 35214
+ *
+ * @covers ::create_initial_comment_types
+ */
+ public function test_built_in_labels_are_rebuilt_on_locale_change() {
+ $original = get_comment_type_object( 'comment' )->label;
+
+ add_filter(
+ 'gettext',
+ static function ( $translation, $text ) {
+ return 'Comments' === $text ? 'Kommentare' : $translation;
+ },
+ 10,
+ 2
+ );
+
+ do_action( 'change_locale', 'de_DE' );
+
+ $this->assertSame(
+ 'Kommentare',
+ get_comment_type_object( 'comment' )->label,
+ 'A locale change should rebuild the built-in labels.'
+ );
+ $this->assertNotSame( $original, get_comment_type_object( 'comment' )->label );
+ }
+
+ /**
+ * Before wp-settings.php registers the built-ins, the registry global does not exist.
+ * Both accessors have to cope with that rather than warn.
+ *
+ * @ticket 35214
+ *
+ * @covers ::get_comment_types
+ * @covers ::get_comment_type_object
+ */
+ public function test_accessors_handle_an_unset_registry() {
+ $registry = $GLOBALS['wp_comment_types'];
+ unset( $GLOBALS['wp_comment_types'] );
+
+ try {
+ $this->assertSame( array(), get_comment_types() );
+ $this->assertNull( get_comment_type_object( 'comment' ) );
+ } finally {
+ $GLOBALS['wp_comment_types'] = $registry;
+ }
+ }
+
+ /**
+ * The registry is a per-process global, so it is not scoped to a site. This matches
+ * post types, and is worth pinning because comment counts and query exclusions are
+ * per-site data.
+ *
+ * @ticket 35214
+ *
+ * @group ms-required
+ *
+ * @covers ::register_comment_type
+ */
+ public function test_registry_is_not_scoped_to_a_site() {
+ register_comment_type( 'foo' );
+
+ $blog_id = self::factory()->blog->create();
+
+ switch_to_blog( $blog_id );
+
+ $registered_after_switch = comment_type_exists( 'foo' );
+
+ restore_current_blog();
+
+ $this->assertTrue(
+ $registered_after_switch,
+ 'A registered comment type should still be registered after switch_to_blog().'
+ );
+ }
+}
diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php
new file mode 100644
index 0000000000000..dc316aac4a735
--- /dev/null
+++ b/tests/phpunit/tests/comment/wpCommentType.php
@@ -0,0 +1,124 @@
+assertSame( 'foo', $comment_type->name );
+ $this->assertTrue( $comment_type->public );
+ $this->assertFalse( $comment_type->internal );
+ $this->assertFalse( $comment_type->_builtin );
+ $this->assertFalse( $comment_type->hierarchical );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_set_props_overrides_defaults() {
+ $comment_type = new WP_Comment_Type(
+ 'foo',
+ array(
+ 'public' => false,
+ 'internal' => true,
+ 'description' => 'A test comment type.',
+ )
+ );
+
+ $this->assertFalse( $comment_type->public );
+ $this->assertTrue( $comment_type->internal );
+ $this->assertSame( 'A test comment type.', $comment_type->description );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_register_comment_type_args_filter() {
+ $filter = static function ( $args ) {
+ $args['public'] = false;
+ return $args;
+ };
+
+ add_filter( 'register_comment_type_args', $filter );
+ $comment_type = new WP_Comment_Type( 'foo' );
+ remove_filter( 'register_comment_type_args', $filter );
+
+ $this->assertFalse( $comment_type->public );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_register_specific_comment_type_args_filter() {
+ $filter = static function ( $args ) {
+ $args['description'] = 'Filtered description.';
+ return $args;
+ };
+
+ add_filter( 'register_foo_comment_type_args', $filter );
+ $comment_type = new WP_Comment_Type( 'foo' );
+ $other_type = new WP_Comment_Type( 'bar' );
+ remove_filter( 'register_foo_comment_type_args', $filter );
+
+ $this->assertSame( 'Filtered description.', $comment_type->description );
+ $this->assertSame( '', $other_type->description );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_default_labels
+ * @covers ::reset_default_labels
+ */
+ public function test_get_default_labels_returns_expected_defaults() {
+ WP_Comment_Type::reset_default_labels();
+
+ $labels = WP_Comment_Type::get_default_labels();
+
+ $this->assertSame( 'Comments', $labels['name'][0] );
+ $this->assertSame( 'Comment', $labels['singular_name'][0] );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_default_labels
+ * @covers ::reset_default_labels
+ */
+ public function test_reset_default_labels_clears_cache() {
+ // Poison the static cache so a stale value is observable.
+ $property = new ReflectionProperty( WP_Comment_Type::class, 'default_labels' );
+ if ( PHP_VERSION_ID < 80100 ) {
+ $property->setAccessible( true );
+ }
+ $property->setValue( null, array( 'name' => array( 'Poisoned', null ) ) );
+
+ $labels = WP_Comment_Type::get_default_labels();
+ $this->assertSame( 'Poisoned', $labels['name'][0], 'The poisoned cache should be served as-is.' );
+
+ WP_Comment_Type::reset_default_labels();
+
+ // A fresh call rebuilds the defaults from translation functions.
+ $labels = WP_Comment_Type::get_default_labels();
+ $this->assertSame( 'Comments', $labels['name'][0], 'Resetting should rebuild the default labels.' );
+ }
+}