Make WordPress Core

Changeset 62522


Ignore:
Timestamp:
06/18/2026 10:36:34 AM (3 months ago)
Author:
cbravobernal
Message:

Block Bindings: Preserve nested inner blocks when binding rich text.

WP_Block::replace_html() replaced the entire element matched by a rich-text attribute's selector, dropping any markup produced by inner blocks rendered inside that element (e.g. a List nested inside a List Item).

Props cbravobernal, jonsurrell.
Fixes #65406.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block.php

    r61431 r62522  
    365365         *
    366366         * @since 6.5.0
    367          *
    368          * @param string $block_content  Block content.
    369          * @param string $attribute_name The attribute name to replace.
    370          * @param mixed  $source_value   The value used to replace in the HTML.
     367         * @since 7.1.0 Added the optional `$inner_block_offsets` parameter.
     368         *
     369         * @param string $block_content       Block content.
     370         * @param string $attribute_name      The attribute name to replace.
     371         * @param mixed  $source_value        The value used to replace in the HTML.
     372         * @param int[]  $inner_block_offsets Optional. Byte offsets where each inner block's
     373         *                                    rendered output begins. Default empty array.
    371374         * @return string The modified block content.
    372375         */
    373         private function replace_html( string $block_content, string $attribute_name, $source_value ) {
     376        private function replace_html( string $block_content, string $attribute_name, $source_value, array $inner_block_offsets = array() ) {
    374377                $block_type = $this->block_type;
    375378                if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
     
    397400                                                )
    398401                                        ) ) {
    399                                                 // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available.
     402                                                /*
     403                                                 * TODO: Use `WP_HTML_Processor::set_inner_html()` once it's available.
     404                                                 * Any replacement must preserve already-rendered inner block
     405                                                 * markup verbatim (it may come from dynamic blocks), so it
     406                                                 * cannot re-serialize the element's contents. Until an API with
     407                                                 * that guarantee exists, the replacement is spliced by byte
     408                                                 * offset, leaving inner block output untouched.
     409                                                 */
    400410                                                $block_reader->release_bookmark( 'iterate-selectors' );
    401                                                 $block_reader->replace_rich_text( wp_kses_post( $source_value ) );
     411                                                $block_reader->replace_rich_text( wp_kses_post( $source_value ), $inner_block_offsets );
    402412                                                return $block_reader->get_updated_html();
    403413                                        } else {
     
    434444                         * matching closer with the provided rich text.
    435445                         *
    436                          * @param string $rich_text The rich text to replace the original content with.
     446                         * If byte offsets of inner blocks' rendered output are provided, the
     447                         * replacement stops at the first inner block found inside the element,
     448                         * preserving any markup produced by nested inner blocks (e.g. a List
     449                         * block nested inside a List Item).
     450                         *
     451                         * @param string $rich_text           The rich text to replace the original content with.
     452                         * @param int[]  $inner_block_offsets Optional. Byte offsets in the source HTML where
     453                         *                                    inner blocks' rendered output begins. Default empty array.
    437454                         * @return bool True on success.
    438455                         */
    439                         public function replace_rich_text( $rich_text ) {
     456                        public function replace_rich_text( $rich_text, $inner_block_offsets = array() ) {
    440457                                if ( $this->is_tag_closer() || ! $this->expects_closer() ) {
    441458                                        return false;
     
    461478                                $tag_closer = $this->bookmarks['__wp_block_bindings'];
    462479                                $end        = $tag_closer->start;
     480
     481                                /*
     482                                 * Stop at the first inner block that renders inside this element so
     483                                 * its markup is preserved. The block's own rich text always precedes
     484                                 * its inner blocks, so replacing up to the first inner block offset
     485                                 * replaces only that rich text. Offsets are recorded during render in
     486                                 * the same byte coordinates as this fragment, and are in ascending
     487                                 * order, so the first match is the earliest inner block.
     488                                 *
     489                                 * The lower bound is inclusive of `$start`: when an inner block
     490                                 * begins immediately, with no leading rich text, the (empty) rich
     491                                 * text is still replaced instead of the inner block markup.
     492                                 */
     493                                foreach ( $inner_block_offsets as $inner_block_offset ) {
     494                                        if ( $inner_block_offset >= $start && $inner_block_offset < $end ) {
     495                                                $end = $inner_block_offset;
     496                                                break;
     497                                        }
     498                                }
    463499
    464500                                $this->lexical_updates[] = new WP_HTML_Text_Replacement(
     
    480516         * @since 5.5.0
    481517         * @since 6.5.0 Added block bindings processing.
     518         * @since 7.1.0 Preserve inner blocks when binding a rich text attribute.
    482519         *
    483520         * @global WP_Post $post Global post object.
     
    539576                $block_content = '';
    540577
     578                /*
     579                 * Byte offsets in $block_content where each inner block's rendered output
     580                 * begins. Block bindings rich-text replacement uses these to stop at the
     581                 * first inner block inside a selector, so it replaces only the block's own
     582                 * rich text and never the markup produced by nested inner blocks.
     583                 *
     584                 * They are only collected when the block has bound attributes to resolve;
     585                 * otherwise they are never read, so recording them would add work to every
     586                 * block render for no benefit.
     587                 */
     588                $inner_block_offsets = array();
     589                $collect_offsets     = ! empty( $computed_attributes );
     590
    541591                if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) {
    542592                        $index = 0;
     
    546596                                        $block_content .= $chunk;
    547597                                } else {
     598                                        if ( $collect_offsets ) {
     599                                                $inner_block_offsets[] = strlen( $block_content );
     600                                        }
    548601                                        $inner_block  = $this->inner_blocks[ $index ];
    549602                                        $parent_block = $this;
     
    584637                if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {
    585638                        foreach ( $computed_attributes as $attribute_name => $source_value ) {
    586                                 $block_content = $this->replace_html( $block_content, $attribute_name, $source_value );
     639                                $updated_block_content = $this->replace_html( $block_content, $attribute_name, $source_value, $inner_block_offsets );
     640
     641                                /*
     642                                 * The offsets describe $block_content as it was assembled. A
     643                                 * replacement that modifies the markup shifts byte positions, so
     644                                 * once the content changes the remaining attributes fall back to
     645                                 * offset-free replacement rather than clamp at a stale position.
     646                                 * Attributes that leave the markup untouched keep the offsets
     647                                 * valid: the computed `metadata` attribute produced by a pattern
     648                                 * overrides `__default` binding has no HTML source, so it must
     649                                 * not invalidate the offsets for the rich text that follows it.
     650                                 */
     651                                if ( $updated_block_content !== $block_content ) {
     652                                        $inner_block_offsets = array();
     653                                }
     654
     655                                $block_content = $updated_block_content;
    587656                        }
    588657                }
  • trunk/tests/phpunit/tests/block-bindings/render.php

    r62518 r62522  
    1313
    1414        const SOURCE_NAME  = 'test/source';
    15         const SOURCE_LABEL = array(
    16                 'label' => 'Test source',
    17         );
     15        const SOURCE_LABEL = 'Test source';
    1816
    1917        /**
     
    434432                );
    435433        }
     434
     435        /**
     436         * Provides fuzz-style nested list fixtures for rich text binding tests.
     437         *
     438         * The fixtures vary whether fallback rich text exists before the first inner
     439         * block, whether that fallback contains raw markup or multibyte text, whether
     440         * nested lists are ordered, and whether siblings surround the bound item.
     441         *
     442         * @return array[]
     443         */
     444        public function data_rich_text_binding_preserves_nested_inner_blocks() {
     445                $child_list = self::build_list_block(
     446                        array(
     447                                self::build_list_item_block( 'Nested child' ),
     448                        )
     449                );
     450
     451                $deep_child_list = self::build_list_block(
     452                        array(
     453                                self::build_list_item_block(
     454                                        'Nested parent' . self::build_list_block(
     455                                                array(
     456                                                        self::build_list_item_block( 'Nested grandchild' ),
     457                                                )
     458                                        )
     459                                ),
     460                        )
     461                );
     462
     463                $ordered_child_list = self::build_list_block(
     464                        array(
     465                                self::build_list_item_block( 'Ordered child' ),
     466                                self::build_list_item_block( 'Second ordered child' ),
     467                        ),
     468                        array(
     469                                'ordered' => true,
     470                                'start'   => 3,
     471                        )
     472                );
     473
     474                return array(
     475                        'nested list after fallback text'          => array(
     476                                'block_content'           => self::build_list_block(
     477                                        array(
     478                                                self::build_list_item_block( 'Default content' . $child_list, true ),
     479                                        )
     480                                ),
     481                                'bound_value'             => 'Bound list item',
     482                                'expected_rendered_block' => <<<HTML
     483<ul class="wp-block-list">
     484<li>Bound list item
     485<ul class="wp-block-list">
     486<li>Nested child</li>
     487</ul>
     488</li>
     489</ul>
     490HTML
     491                                ,
     492                                'removed_strings'         => array( 'Default content' ),
     493                                'preserved_strings'       => array( 'Nested child' ),
     494                        ),
     495                        'raw markup before nested list'            => array(
     496                                'block_content'           => self::build_list_block(
     497                                        array(
     498                                                self::build_list_item_block( 'Default content<ul><li>Raw markup to replace</li></ul>' . $child_list, true ),
     499                                        )
     500                                ),
     501                                'bound_value'             => 'Bound list item',
     502                                'expected_rendered_block' => <<<HTML
     503<ul class="wp-block-list">
     504<li>Bound list item
     505<ul class="wp-block-list">
     506<li>Nested child</li>
     507</ul>
     508</li>
     509</ul>
     510HTML
     511                                ,
     512                                'removed_strings'         => array( 'Default content', 'Raw markup to replace' ),
     513                                'preserved_strings'       => array( 'Nested child' ),
     514                        ),
     515                        'inner block starts at rich text boundary' => array(
     516                                'block_content'           => self::build_list_block(
     517                                        array(
     518                                                self::build_list_item_block( $child_list, true ),
     519                                        )
     520                                ),
     521                                'bound_value'             => 'Bound list item',
     522                                'expected_rendered_block' => <<<HTML
     523<ul class="wp-block-list">
     524<li>Bound list item
     525<ul class="wp-block-list">
     526<li>Nested child</li>
     527</ul>
     528</li>
     529</ul>
     530HTML
     531                                ,
     532                                'removed_strings'         => array(),
     533                                'preserved_strings'       => array( 'Nested child' ),
     534                        ),
     535                        'multibyte fallback before nested list'    => array(
     536                                'block_content'           => self::build_list_block(
     537                                        array(
     538                                                self::build_list_item_block( 'Café fallback before <strong>nested</strong> list' . $child_list, true ),
     539                                        )
     540                                ),
     541                                'bound_value'             => 'Bound <em>línea</em>',
     542                                'expected_rendered_block' => <<<HTML
     543<ul class="wp-block-list">
     544<li>Bound <em>línea</em>
     545<ul class="wp-block-list">
     546<li>Nested child</li>
     547</ul>
     548</li>
     549</ul>
     550HTML
     551                                ,
     552                                'removed_strings'         => array( 'Café fallback', '<strong>nested</strong>' ),
     553                                'preserved_strings'       => array( 'Nested child', 'Bound <em>línea</em>' ),
     554                        ),
     555                        'deep nested list with sibling item'       => array(
     556                                'block_content'           => self::build_list_block(
     557                                        array(
     558                                                self::build_list_item_block( 'Default parent' . $deep_child_list, true ),
     559                                                self::build_list_item_block( 'Sibling stays' ),
     560                                        )
     561                                ),
     562                                'bound_value'             => 'Bound parent',
     563                                'expected_rendered_block' => <<<HTML
     564<ul class="wp-block-list">
     565<li>Bound parent
     566<ul class="wp-block-list">
     567<li>Nested parent
     568<ul class="wp-block-list">
     569<li>Nested grandchild</li>
     570</ul>
     571</li>
     572</ul>
     573</li>
     574
     575<li>Sibling stays</li>
     576</ul>
     577HTML
     578                                ,
     579                                'removed_strings'         => array( 'Default parent' ),
     580                                'preserved_strings'       => array( 'Nested parent', 'Nested grandchild', 'Sibling stays' ),
     581                        ),
     582                        'ordered nested list with attributes'      => array(
     583                                'block_content'           => self::build_list_block(
     584                                        array(
     585                                                self::build_list_item_block( '<span>Default ordered parent</span>' . $ordered_child_list, true ),
     586                                        )
     587                                ),
     588                                'bound_value'             => 'Bound ordered parent',
     589                                'expected_rendered_block' => <<<HTML
     590<ul class="wp-block-list">
     591<li>Bound ordered parent
     592<ol class="wp-block-list" start="3">
     593<li>Ordered child</li>
     594
     595<li>Second ordered child</li>
     596</ol>
     597</li>
     598</ul>
     599HTML
     600                                ,
     601                                'removed_strings'         => array( 'Default ordered parent' ),
     602                                'preserved_strings'       => array( 'Ordered child', 'Second ordered child', 'start="3"' ),
     603                        ),
     604                );
     605        }
     606
     607        /**
     608         * Tests that binding a List Item block's rich text preserves nested List
     609         * inner blocks rendered inside the same `<li>` element.
     610         *
     611         * @ticket 65406
     612         *
     613         * @covers WP_Block::render
     614         *
     615         * @dataProvider data_rich_text_binding_preserves_nested_inner_blocks
     616         */
     617        public function test_rich_text_binding_preserves_nested_inner_blocks( $block_content, $bound_value, $expected_rendered_block, $removed_strings, $preserved_strings ) {
     618                register_block_bindings_source(
     619                        self::SOURCE_NAME,
     620                        array(
     621                                'label'              => self::SOURCE_LABEL,
     622                                'get_value_callback' => static function () use ( $bound_value ) {
     623                                        return $bound_value;
     624                                },
     625                        )
     626                );
     627
     628                $parsed_blocks = parse_blocks( $block_content );
     629                $block         = new WP_Block( $parsed_blocks[0] );
     630                $result        = $block->render();
     631
     632                foreach ( $removed_strings as $removed_string ) {
     633                        $this->assertStringNotContainsString(
     634                                $removed_string,
     635                                $result,
     636                                "Fallback content '{$removed_string}' should be replaced by the source value."
     637                        );
     638                }
     639
     640                foreach ( $preserved_strings as $preserved_string ) {
     641                        $this->assertStringContainsString(
     642                                $preserved_string,
     643                                $result,
     644                                "Nested inner block content '{$preserved_string}' should be preserved."
     645                        );
     646                }
     647
     648                $this->assertEqualHTML(
     649                        $expected_rendered_block,
     650                        trim( $result ),
     651                        '<body>',
     652                        'The bound list item rich text should be replaced without dropping nested inner blocks.'
     653                );
     654                $this->assertSame(
     655                        $bound_value,
     656                        $block->inner_blocks[0]->attributes['content'],
     657                        'The bound list item content attribute should be updated with the source value.'
     658                );
     659        }
     660
     661        /**
     662         * Tests that inner-block preservation is block-agnostic.
     663         *
     664         * The replacement logic has no block-specific handling: it relies only on
     665         * where inner blocks render. This registers an arbitrary block whose bound
     666         * rich text and an inner block share the same element, and confirms the inner
     667         * block is preserved exactly as it is for `core/list-item`.
     668         *
     669         * @ticket 65406
     670         *
     671         * @covers WP_Block::render
     672         */
     673        public function test_rich_text_binding_preserves_inner_blocks_for_any_block() {
     674                register_block_type(
     675                        'test/rich-text-with-inner-blocks',
     676                        array(
     677                                'attributes' => array(
     678                                        'content' => array(
     679                                                'type'     => 'rich-text',
     680                                                'source'   => 'rich-text',
     681                                                'selector' => 'div',
     682                                        ),
     683                                ),
     684                        )
     685                );
     686
     687                $supported_attributes_filter = static function ( $supported_attributes, $block_type ) {
     688                        if ( 'test/rich-text-with-inner-blocks' === $block_type ) {
     689                                $supported_attributes[] = 'content';
     690                        }
     691                        return $supported_attributes;
     692                };
     693
     694                add_filter(
     695                        'block_bindings_supported_attributes',
     696                        $supported_attributes_filter,
     697                        10,
     698                        2
     699                );
     700
     701                register_block_bindings_source(
     702                        self::SOURCE_NAME,
     703                        array(
     704                                'label'              => self::SOURCE_LABEL,
     705                                'get_value_callback' => static function () {
     706                                        return 'Bound value';
     707                                },
     708                        )
     709                );
     710
     711                $block_content = <<<HTML
     712<!-- wp:test/rich-text-with-inner-blocks {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
     713<div><!-- wp:paragraph -->
     714<p>Inner paragraph stays</p>
     715<!-- /wp:paragraph --></div>
     716<!-- /wp:test/rich-text-with-inner-blocks -->
     717HTML;
     718
     719                $parsed_blocks = parse_blocks( $block_content );
     720                $block         = new WP_Block( $parsed_blocks[0] );
     721                $result        = $block->render();
     722
     723                remove_filter( 'block_bindings_supported_attributes', $supported_attributes_filter, 10 );
     724                unregister_block_type( 'test/rich-text-with-inner-blocks' );
     725
     726                $expected_rendered_block = <<<HTML
     727<div>Bound value
     728<p class="wp-block-paragraph">Inner paragraph stays</p>
     729</div>
     730HTML;
     731
     732                $this->assertEqualHTML(
     733                        $expected_rendered_block,
     734                        trim( $result ),
     735                        '<body>',
     736                        'The inner block should be preserved for any block, not just core/list-item.'
     737                );
     738        }
     739
     740        /**
     741         * Tests that a pattern overrides `__default` binding preserves nested List
     742         * inner blocks.
     743         *
     744         * Pattern overrides expand the `__default` binding into computed attributes
     745         * that include the rewritten `metadata` attribute alongside `content`. The
     746         * `metadata` attribute has no HTML source, so its no-op replacement must not
     747         * invalidate the inner-block offsets used to preserve the nested list when
     748         * `content` is replaced afterwards.
     749         *
     750         * @ticket 65406
     751         *
     752         * @covers WP_Block::render
     753         */
     754        public function test_pattern_overrides_binding_preserves_nested_inner_blocks() {
     755                $block_content = <<<HTML
     756<!-- wp:list -->
     757<ul class="wp-block-list"><!-- wp:list-item {"metadata":{"bindings":{"__default":{"source":"core/pattern-overrides"}},"name":"Editable List Item"}} -->
     758<li>Default content<!-- wp:list -->
     759<ul class="wp-block-list"><!-- wp:list-item -->
     760<li>Nested child</li>
     761<!-- /wp:list-item --></ul>
     762<!-- /wp:list --></li>
     763<!-- /wp:list-item --></ul>
     764<!-- /wp:list -->
     765HTML;
     766
     767                $parsed_blocks = parse_blocks( $block_content );
     768                $block         = new WP_Block(
     769                        $parsed_blocks[0],
     770                        array(
     771                                'pattern/overrides' => array(
     772                                        'Editable List Item' => array( 'content' => 'Pattern <em>override</em>' ),
     773                                ),
     774                        )
     775                );
     776                $result        = $block->render();
     777
     778                $expected_rendered_block = <<<HTML
     779<ul class="wp-block-list">
     780<li>Pattern <em>override</em>
     781<ul class="wp-block-list">
     782<li>Nested child</li>
     783</ul>
     784</li>
     785</ul>
     786HTML;
     787
     788                $this->assertEqualHTML(
     789                        $expected_rendered_block,
     790                        trim( $result ),
     791                        '<body>',
     792                        'The pattern override should replace the list item rich text without dropping the nested list.'
     793                );
     794                $this->assertSame(
     795                        'Pattern <em>override</em>',
     796                        $block->inner_blocks[0]->attributes['content'],
     797                        'The list item content attribute should be updated with the pattern override value.'
     798                );
     799        }
     800
     801        /**
     802         * Tests that binding degrades safely when rich text does not precede the
     803         * inner block.
     804         *
     805         * The replacement assumes a block's own rich text comes before its inner
     806         * blocks, which holds for a normally authored List Item. When markup is
     807         * authored with the nested list first, the replacement stops at that inner
     808         * block: the bound value is written ahead of it and the trailing rich text
     809         * is left in place. The result is an incomplete replacement, never broken
     810         * structure, and the nested inner block is still preserved.
     811         *
     812         * @ticket 65406
     813         *
     814         * @covers WP_Block::render
     815         */
     816        public function test_rich_text_binding_with_inner_block_before_text() {
     817                $block_content = <<<HTML
     818<!-- wp:list -->
     819<ul class="wp-block-list"><!-- wp:list-item {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
     820<li><!-- wp:list -->
     821<ul class="wp-block-list"><!-- wp:list-item -->
     822<li>Nested child</li>
     823<!-- /wp:list-item --></ul>
     824<!-- /wp:list -->trailing text</li>
     825<!-- /wp:list-item --></ul>
     826<!-- /wp:list -->
     827HTML;
     828
     829                register_block_bindings_source(
     830                        self::SOURCE_NAME,
     831                        array(
     832                                'label'              => self::SOURCE_LABEL,
     833                                'get_value_callback' => static function () {
     834                                        return 'Bound value';
     835                                },
     836                        )
     837                );
     838
     839                $parsed_blocks = parse_blocks( $block_content );
     840                $block         = new WP_Block( $parsed_blocks[0] );
     841                $result        = $block->render();
     842
     843                $expected_rendered_block = <<<HTML
     844<ul class="wp-block-list">
     845<li>Bound value
     846<ul class="wp-block-list">
     847<li>Nested child</li>
     848</ul>
     849trailing text</li>
     850</ul>
     851HTML;
     852
     853                $this->assertEqualHTML(
     854                        $expected_rendered_block,
     855                        trim( $result ),
     856                        '<body>',
     857                        'The bound value should be written before the inner block while preserving the nested list and the trailing rich text.'
     858                );
     859                $this->assertStringContainsString(
     860                        'Nested child',
     861                        $result,
     862                        'The nested list inner block should be preserved.'
     863                );
     864                $this->assertStringContainsString(
     865                        'trailing text',
     866                        $result,
     867                        'Rich text after the inner block should be left untouched.'
     868                );
     869        }
     870
     871        /**
     872         * Builds List block markup.
     873         *
     874         * @param string[] $items      Serialized List Item blocks.
     875         * @param array    $attributes Optional List block attributes.
     876         * @return string Serialized List block markup.
     877         */
     878        private static function build_list_block( $items, $attributes = array() ) {
     879                $is_ordered       = ! empty( $attributes['ordered'] );
     880                $tag_name         = $is_ordered ? 'ol' : 'ul';
     881                $block_attributes = $attributes ? ' ' . wp_json_encode( $attributes ) : '';
     882                $html_attributes  = ' class="wp-block-list"';
     883
     884                if ( isset( $attributes['start'] ) ) {
     885                        $html_attributes .= ' start="' . (int) $attributes['start'] . '"';
     886                }
     887
     888                return sprintf(
     889                        "<!-- wp:list%s -->\n<%s%s>%s</%s>\n<!-- /wp:list -->",
     890                        $block_attributes,
     891                        $tag_name,
     892                        $html_attributes,
     893                        implode( '', $items ),
     894                        $tag_name
     895                );
     896        }
     897
     898        /**
     899         * Builds List Item block markup.
     900         *
     901         * @param string $content  List item inner HTML.
     902         * @param bool   $is_bound Optional. Whether to bind the content attribute.
     903         * @return string Serialized List Item block markup.
     904         */
     905        private static function build_list_item_block( $content, $is_bound = false ) {
     906                $block_attributes = $is_bound ? ' {"metadata":{"bindings":{"content":{"source":"test/source"}}}}' : '';
     907
     908                return sprintf(
     909                        "<!-- wp:list-item%s -->\n<li>%s</li>\n<!-- /wp:list-item -->",
     910                        $block_attributes,
     911                        $content
     912                );
     913        }
    436914}
  • trunk/tests/phpunit/tests/block-bindings/wp-block-get-block-bindings-processor.php

    r60729 r62522  
    3737                $this->assertEquals(
    3838                        $button_wrapper_opener . 'The hardest button to button' . $button_wrapper_closer,
     39                        $processor->get_updated_html()
     40                );
     41        }
     42
     43        /**
     44         * @ticket 65406
     45         */
     46        public function test_replace_rich_text_stops_at_inner_block_offset() {
     47                $item_opener = '<li>';
     48                $rich_text   = 'This should not appear';
     49                $nested_list = '<ul class="wp-block-list"><li>Nested child</li></ul>';
     50                $item_closer = '</li>';
     51
     52                $processor = self::$get_block_bindings_processor_method->invoke(
     53                        null,
     54                        $item_opener . $rich_text . $nested_list . $item_closer
     55                );
     56                $processor->next_tag( array( 'tag_name' => 'li' ) );
     57
     58                $this->assertTrue(
     59                        $processor->replace_rich_text(
     60                                'New list item content',
     61                                array( strlen( $item_opener . $rich_text ) )
     62                        )
     63                );
     64                $this->assertEquals(
     65                        $item_opener . 'New list item content' . $nested_list . $item_closer,
    3966                        $processor->get_updated_html()
    4067                );
Note: See TracChangeset for help on using the changeset viewer.