` that end_el() closes under the 'div' style.
+ *
+ * @ticket 35214
+ */
+ public function test_render_callback_pairs_with_the_div_style(): void {
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => static function ( $comment, $args ) {
+ $tag = ( 'div' === $args['style'] ) ? 'div' : 'li';
+
+ echo '<' . $tag . ' class="review">' . esc_html( $comment->comment_content );
+ },
+ )
+ );
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_content' => 'Rendered by callback',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ), array( 'style' => 'div' ) );
+
+ $this->assertStringContainsString(
+ '
Rendered by callback
',
+ $output
+ );
+ }
+
+ /**
+ * The register_comment_type_args filter can add a render_callback to a built-in type.
+ *
+ * @ticket 35214
+ */
+ public function test_render_callback_can_be_added_to_a_built_in_type_by_filter(): void {
+ add_filter(
+ 'register_comment_type_args',
+ static function ( $args, $comment_type ) {
+ if ( 'comment' === $comment_type ) {
+ $args['render_callback'] = static function () {
+ echo '
';
+ };
+ }
+
+ return $args;
+ },
+ 10,
+ 2
+ );
+
+ // Rebuild the built-ins so the filter applies to them.
+ create_initial_comment_types();
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'comment',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertStringContainsString( '', $output );
+ }
+
+ /**
+ * An explicit wp_list_comments() callback takes precedence over a type's render_callback.
+ *
+ * @ticket 35214
+ */
+ public function test_explicit_callback_takes_precedence_over_render_callback(): void {
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => static function () {
+ echo 'FROM_TYPE_CALLBACK';
+ },
+ )
+ );
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments(
+ array( get_comment( $comment_id ) ),
+ array(
+ 'callback' => static function () {
+ echo 'FROM_LIST_CALLBACK';
+ },
+ )
+ );
+
+ $this->assertStringContainsString( 'FROM_LIST_CALLBACK', $output );
+ $this->assertStringNotContainsString( 'FROM_TYPE_CALLBACK', $output );
+ }
+
+ /**
+ * Built-in comment types without a render_callback render normally.
+ *
+ * @ticket 35214
+ */
+ public function test_comment_type_without_render_callback_renders_normally(): void {
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'comment',
+ 'comment_content' => 'A normal comment',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertStringContainsString( 'A normal comment', $output );
+ }
+
+ /**
+ * The render_callback receives the comment, the arguments array, and the depth,
+ * matching the wp_list_comments() `callback` contract.
+ *
+ * @ticket 35214
+ */
+ public function test_render_callback_receives_comment_args_and_depth(): void {
+ $received = array();
+
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => static function ( $comment, $args, $depth ) use ( &$received ) {
+ $received = array(
+ 'comment' => $comment,
+ 'args' => $args,
+ 'depth' => $depth,
+ );
+ echo '';
+ },
+ )
+ );
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertInstanceOf( 'WP_Comment', $received['comment'], 'The callback should receive the comment object.' );
+ $this->assertSame( (string) $comment_id, (string) $received['comment']->comment_ID, 'The callback should receive the rendered comment.' );
+ $this->assertIsArray( $received['args'], 'The callback should receive the arguments array.' );
+ $this->assertSame( 1, $received['depth'], 'A top-level comment should be rendered at depth 1.' );
+ }
+
+ /**
+ * A child comment renders inside the element opened by its parent's render_callback.
+ *
+ * @ticket 35214
+ */
+ public function test_child_comment_renders_within_render_callback_parent_element(): void {
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => static function ( $comment ) {
+ echo '' . esc_html( $comment->comment_content );
+ },
+ )
+ );
+
+ $parent_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_content' => 'Parent review',
+ 'comment_approved' => '1',
+ )
+ );
+ $child_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_content' => 'Child review',
+ 'comment_approved' => '1',
+ 'comment_parent' => $parent_id,
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $parent_id ), get_comment( $child_id ) ) );
+
+ $parent_pos = strpos( $output, 'Parent review' );
+ $children_pos = strpos( $output, '' );
+ $child_pos = strpos( $output, 'Child review' );
+
+ $this->assertNotFalse( $children_pos, 'A children list should be rendered.' );
+ $this->assertGreaterThan( $parent_pos, $children_pos, 'The children list should open after the parent content.' );
+ $this->assertGreaterThan( $children_pos, $child_pos, 'The child should render inside the children list.' );
+ $this->assertSame( 2, substr_count( $output, '
' ), 'Each element should be closed exactly once by end_el().' );
+ }
+
+ /**
+ * A comment stored with the legacy empty string type renders the default markup.
+ *
+ * @ticket 35214
+ */
+ public function test_legacy_empty_type_renders_default_markup(): void {
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => '',
+ 'comment_content' => 'Legacy comment',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertStringContainsString( 'Legacy comment', $output );
+ $this->assertStringContainsString( '', $output );
+ }
+
+ /**
+ * A render_callback that is not callable is ignored and the comment renders normally.
+ *
+ * @ticket 35214
+ */
+ public function test_non_callable_render_callback_is_ignored(): void {
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => 'this_is_not_a_callable_function',
+ )
+ );
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_content' => 'Falls back to normal rendering',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertStringContainsString( 'Falls back to normal rendering', $output );
+ }
+
+ /**
+ * A string returned by a render_callback is discarded.
+ *
+ * @ticket 35214
+ */
+ public function test_render_callback_return_value_is_discarded(): void {
+ register_comment_type(
+ 'review',
+ array(
+ 'render_callback' => static function () {
+ return '
RETURNED_MARKER';
+ },
+ )
+ );
+
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_type' => 'review',
+ 'comment_content' => 'Return value test',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments( array( get_comment( $comment_id ) ) );
+
+ $this->assertStringNotContainsString( 'RETURNED_MARKER', $output );
+ }
}
class Comment_Callback_Test_Helper {
diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php
index bae6b5100d248..4fc4947c49bb8 100644
--- a/tests/phpunit/tests/comment/wpCommentType.php
+++ b/tests/phpunit/tests/comment/wpCommentType.php
@@ -23,6 +23,19 @@ public function test_instance_defaults(): void {
$this->assertFalse( $comment_type->internal );
$this->assertFalse( $comment_type->_builtin );
$this->assertFalse( $comment_type->hierarchical );
+ $this->assertNull( $comment_type->render_callback );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_render_callback_is_stored(): void {
+ $callback = static function () {};
+ $comment_type = new WP_Comment_Type( 'foo', array( 'render_callback' => $callback ) );
+
+ $this->assertSame( $callback, $comment_type->render_callback );
}
/**