@@ -391,4 +391,152 @@ public function test_filter_block_bindings_source_value() {
391391 'The block content should show the filtered value. '
392392 );
393393 }
394+
395+ /**
396+ * Renders a list item with a source value bound to its content attribute.
397+ *
398+ * @param mixed $source_value The source value.
399+ * @param string $block_content The block content.
400+ * @return string Rendered block content.
401+ */
402+ private function render_list_item_with_source_value ( $ source_value , $ block_content ) {
403+ register_block_bindings_source (
404+ self ::SOURCE_NAME ,
405+ array (
406+ 'label ' => self ::SOURCE_LABEL ,
407+ 'get_value_callback ' => function () use ( $ source_value ) {
408+ return $ source_value ;
409+ },
410+ )
411+ );
412+
413+ $ parsed_blocks = parse_blocks ( $ block_content );
414+ $ block = new WP_Block ( $ parsed_blocks [0 ] );
415+ return trim ( $ block ->render () );
416+ }
417+
418+ /**
419+ * Tests if list item content is updated with a plain text value returned by the source.
420+ *
421+ * @covers WP_Block::render
422+ */
423+ public function test_update_list_item_with_plain_text_value_from_source () {
424+ $ block_content = <<<HTML
425+ <!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
426+ <li>This should not appear</li>
427+ <!-- /wp:list-item -->
428+ HTML ;
429+ $ result = $ this ->render_list_item_with_source_value ( 'test source value ' , $ block_content );
430+
431+ $ this ->assertSame (
432+ '<li>test source value</li> ' ,
433+ $ result ,
434+ 'The list item content should be replaced by the source text. '
435+ );
436+ }
437+ /**
438+ * Tests if raw nested list markup is replaced when it is not a list inner block.
439+ *
440+ * @covers WP_Block::render
441+ */
442+ public function test_update_list_item_with_raw_nested_list_markup_without_inner_block_replaces_markup () {
443+ $ block_content = <<<HTML
444+ <!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
445+ <li>Default content<ul><li>Raw nested list should be replaced</li></ul></li>
446+ <!-- /wp:list-item -->
447+ HTML ;
448+ $ result = $ this ->render_list_item_with_source_value ( 'Bound text ' , $ block_content );
449+
450+ $ this ->assertSame (
451+ '<li>Bound text</li> ' ,
452+ $ result ,
453+ 'Raw nested list markup should be replaced with the rest of the list item content. '
454+ );
455+ }
456+
457+ /**
458+ * Tests if source image markup renders after post KSES sanitization.
459+ *
460+ * @covers WP_Block::render
461+ */
462+ public function test_update_list_item_source_image_renders_after_kses () {
463+ $ block_content = <<<HTML
464+ <!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
465+ <li>This should not appear</li>
466+ <!-- /wp:list-item -->
467+ HTML ;
468+ $ result = $ this ->render_list_item_with_source_value (
469+ 'Bound <img class="wp-image-42" src="https://example.com/inline-image.jpg" alt="Inline image" /> ' ,
470+ $ block_content
471+ );
472+
473+ $ this ->assertMatchesRegularExpression (
474+ '#<li>Bound <img[^>]*class="wp-image-42"[^>]*src="https://example\.com/inline-image\.jpg"[^>]*alt="Inline image"[^>]*/?></li># ' ,
475+ $ result ,
476+ 'The source image should render in the list item content after sanitization. '
477+ );
478+ }
479+
480+ /**
481+ * Tests if unsafe source markup is sanitized from list item content.
482+ *
483+ * @covers WP_Block::render
484+ */
485+ public function test_update_list_item_unsafe_source_markup_is_sanitized () {
486+ $ block_content = <<<HTML
487+ <!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
488+ <li>This should not appear</li>
489+ <!-- /wp:list-item -->
490+ HTML ;
491+ $ result = $ this ->render_list_item_with_source_value (
492+ 'Bound <img src="https://example.com/inline-image.jpg" alt="Inline image" onerror="alert(1)" /> <script>alert("Unsafe HTML")</script> ' ,
493+ $ block_content
494+ );
495+
496+ $ this ->assertStringNotContainsString (
497+ '<script ' ,
498+ $ result ,
499+ 'Script tags should be stripped from list item source content. '
500+ );
501+ $ this ->assertStringNotContainsString (
502+ 'onerror ' ,
503+ $ result ,
504+ 'Event handler attributes should be stripped from list item source content. '
505+ );
506+ $ this ->assertMatchesRegularExpression (
507+ '#<img[^>]*src="https://example\.com/inline-image\.jpg"[^>]*alt="Inline image"[^>]*/?># ' ,
508+ $ result ,
509+ 'Safe image markup should remain after sanitization. '
510+ );
511+ }
512+
513+ /**
514+ * Tests if original inline images are replaced by bound list item text.
515+ *
516+ * @covers WP_Block::render
517+ */
518+ public function test_update_list_item_original_inline_image_is_replaced_by_source_text () {
519+ $ block_content = <<<HTML
520+ <!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
521+ <li>Original <img src="https://example.com/original-image.jpg" alt="Original inline image" /> content</li>
522+ <!-- /wp:list-item -->
523+ HTML ;
524+ $ result = $ this ->render_list_item_with_source_value ( 'Bound text ' , $ block_content );
525+
526+ $ this ->assertSame (
527+ '<li>Bound text</li> ' ,
528+ $ result ,
529+ 'The original inline image should be replaced with the rest of the list item content. '
530+ );
531+ $ this ->assertStringNotContainsString (
532+ 'original-image.jpg ' ,
533+ $ result ,
534+ 'The original inline image source should not remain after binding replacement. '
535+ );
536+ $ this ->assertStringNotContainsString (
537+ 'Original inline image ' ,
538+ $ result ,
539+ 'The original inline image alt text should not remain after binding replacement. '
540+ );
541+ }
394542}
0 commit comments