Make WordPress Core

Changeset 60953

Timestamp:
10/17/2025 02:33:53 PM (5 weeks ago)
Author:
luisherranz
Message:

Interactivity API: Support lazy-loaded derived state props.

Serialize on the server the paths of derived state closures accessed during directive processing and include them in the serialized data. During the Interactivity API hydration, if a directive reads one of those paths before its JavaScript getter is available, keep the server-rendered value intact instead of replacing it with undefined.

props darerodz.
Fixes #63898.

Location:
trunk
Files:
3 edited

Legend:

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

    r60741 r60953  
    6060
    6161    /**
     62     * Keeps track of all derived state closures accessed during server-side rendering.
     63     *
     64     * This data is serialized and sent to the client as part of the interactivity
     65     * data, and is handled later in the client to support derived state props that
     66     * are lazily hydrated.
     67     *
     68     * @since 6.9.0
     69     * @var array
     70     */
     71    private $derived_state_closures = array();
     72
     73    /**
    6274     * Flag that indicates whether the `data-wp-router-region` directive has
    6375     * been found in the HTML and processed.
     
    237249     *
    238250     * @since 6.7.0
     251     * @since 6.9.0 Serializes derived state props accessed during directive processing.
    239252     *
    240253     * @param array $data Data to filter.
     
    242255     */
    243256    public function filter_script_module_interactivity_data( array $data ): array {
    244         if ( empty( $this->state_data ) && empty( $this->config_data ) ) {
     257        if (
     258            empty( $this->state_data ) &&
     259            empty( $this->config_data ) &&
     260            empty( $this->derived_state_closures )
     261        ) {
    245262            return $data;
    246263        }
     
    264281        if ( ! empty( $state ) ) {
    265282            $data['state'] = $state;
     283        }
     284
     285        $derived_props = array();
     286        foreach ( $this->derived_state_closures as $key => $value ) {
     287            if ( ! empty( $value ) ) {
     288                $derived_props[ $key ] = $value;
     289            }
     290        }
     291        if ( ! empty( $derived_props ) ) {
     292            $data['derivedStateClosures'] = $derived_props;
    266293        }
    267294
     
    599626        $path_segments = explode( '.', $path );
    600627        $current       = $store;
    601         foreach ( $path_segments as $path_segment ) {
     628        foreach ( $path_segments as $index => $path_segment ) {
    602629            /*
    603630             * Special case for numeric arrays and strings. Add length
     
    648675                try {
    649676                    $current = $current();
     677
     678                    /*
     679                     * Tracks derived state properties that are accessed during
     680                     * rendering.
     681                     *
     682                     * @since 6.9.0
     683                     */
     684                    $this->derived_state_closures[ $ns ] = $this->derived_state_closures[ $ns ] ?? array();
     685
     686                    // Builds path for the current property and add it to tracking if not already present.
     687                    $current_path = implode( '.', array_slice( $path_segments, 0, $index + 1 ) );
     688                    if ( ! in_array( $current_path, $this->derived_state_closures[ $ns ], true ) ) {
     689                        $this->derived_state_closures[ $ns ][] = $current_path;
     690                    }
    650691                } catch ( Throwable $e ) {
    651692                    _doing_it_wrong(
     
    11611202     *
    11621203     * @since 6.5.0
     1204     * @since 6.9.0 Include the list path in the rendered `data-wp-each-child` directives.
    11631205     *
    11641206     * @param WP_Interactivity_API_Directives_Processor $p               The directives processor instance.
     
    12061248
    12071249            // Extracts the namespace from the directive attribute value.
    1208             $namespace_value         = end( $this->namespace_stack );
    1209             list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value )
     1250            $namespace_value                = end( $this->namespace_stack );
     1251            list( $namespace_value, $path ) = is_string( $attribute_value ) && ! empty( $attribute_value )
    12101252                ? $this->extract_directive_value( $attribute_value, $namespace_value )
    12111253                : array( $namespace_value, null );
     
    12291271                }
    12301272
    1231                 // Adds the `data-wp-each-child` to each top-level tag.
     1273                /*
     1274                 * Adds the `data-wp-each-child` directive to each top-level tag
     1275                 * rendered by this `data-wp-each` directive. The value is the
     1276                 * `data-wp-each` directive's namespace and path.
     1277                 *
     1278                 * Nested `data-wp-each` directives could render
     1279                 * `data-wp-each-child` elements at the top level as well, and
     1280                 * they should be overwritten.
     1281                 *
     1282                 * @since 6.9.0
     1283                 */
    12321284                $i = new WP_Interactivity_API_Directives_Processor( $processed_item );
    12331285                while ( $i->next_tag() ) {
    1234                     $i->set_attribute( 'data-wp-each-child', true );
     1286                    $i->set_attribute( 'data-wp-each-child', $namespace_value . '::' . $path );
    12351287                    $i->next_balanced_tag_closer_tag();
    12361288                }
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php

    r58594 r60953  
    8989                '<span data-wp-text="myPlugin::context.item"></span>' .
    9090            '</template>' .
    91             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    92             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
     91            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     92            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
    9393            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    9494        $new      = $this->interactivity->process_directives( $original );
     
    141141                    '<span data-wp-bind--id="myPlugin::context.id" data-wp-text="myPlugin::context.item"></span>' .
    142142                '</template>' .
    143                 '<span data-wp-each-child id="some-id" data-wp-bind--id="myPlugin::context.id" data-wp-text="myPlugin::context.item">1</span>' .
    144                 '<span data-wp-each-child id="some-id" data-wp-bind--id="myPlugin::context.id" data-wp-text="myPlugin::context.item">2</span>' .
     143                '<span data-wp-each-child="myPlugin::state.list" id="some-id" data-wp-bind--id="myPlugin::context.id" data-wp-text="myPlugin::context.item">1</span>' .
     144                '<span data-wp-each-child="myPlugin::state.list" id="some-id" data-wp-bind--id="myPlugin::context.id" data-wp-text="myPlugin::context.item">2</span>' .
    145145                '<div id="after-wp-each" data-wp-bind--id="myPlugin::context.after" data-wp-text="myPlugin::context.item">New text</div>' .
    146146            '</div>';
     
    169169                    '<span data-wp-text="myPlugin::context.item"></span>' .
    170170                '</template>' .
    171                 '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    172                 '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
     171                '<span data-wp-each-child="myPlugin::context.list" data-wp-text="myPlugin::context.item">1</span>' .
     172                '<span data-wp-each-child="myPlugin::context.list" data-wp-text="myPlugin::context.item">2</span>' .
    173173                '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>' .
    174174            '</div>';
     
    197197                    '<span data-wp-text="context.item"></span>' .
    198198                '</template>' .
    199                 '<span data-wp-each-child data-wp-text="context.item">1</span>' .
    200                 '<span data-wp-each-child data-wp-text="context.item">2</span>' .
     199                '<span data-wp-each-child="myPlugin::state.list" data-wp-text="context.item">1</span>' .
     200                '<span data-wp-each-child="myPlugin::state.list" data-wp-text="context.item">2</span>' .
    201201                '<div id="after-wp-each" data-wp-bind--id="state.after">Text</div>' .
    202202            '</div>';
     
    224224                '<span data-wp-text="myPlugin::context.item"></span>' .
    225225            '</template>' .
    226             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    227             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    228             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
    229             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
     226            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     227            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     228            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
     229            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
    230230            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    231231        $new      = $this->interactivity->process_directives( $original );
     
    252252                '<img data-wp-bind--id="myPlugin::context.item">' .
    253253            '</template>' .
    254             '<img data-wp-each-child id="1" data-wp-bind--id="myPlugin::context.item">' .
    255             '<img data-wp-each-child id="1" data-wp-bind--id="myPlugin::context.item">' .
    256             '<img data-wp-each-child id="2" data-wp-bind--id="myPlugin::context.item">' .
    257             '<img data-wp-each-child id="2" data-wp-bind--id="myPlugin::context.item">' .
     254            '<img data-wp-each-child="myPlugin::state.list" id="1" data-wp-bind--id="myPlugin::context.item">' .
     255            '<img data-wp-each-child="myPlugin::state.list" id="1" data-wp-bind--id="myPlugin::context.item">' .
     256            '<img data-wp-each-child="myPlugin::state.list" id="2" data-wp-bind--id="myPlugin::context.item">' .
     257            '<img data-wp-each-child="myPlugin::state.list" id="2" data-wp-bind--id="myPlugin::context.item">' .
    258258            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    259259        $new      = $this->interactivity->process_directives( $original );
     
    281281                '<span data-wp-text="myPlugin::context.item"></span>' .
    282282            '</template>' .
    283             '<img data-wp-each-child id="1" data-wp-bind--id="myPlugin::context.item">' .
    284             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    285             '<img data-wp-each-child id="2" data-wp-bind--id="myPlugin::context.item">' .
    286             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
     283            '<img data-wp-each-child="myPlugin::state.list" id="1" data-wp-bind--id="myPlugin::context.item">' .
     284            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     285            '<img data-wp-each-child="myPlugin::state.list" id="2" data-wp-bind--id="myPlugin::context.item">' .
     286            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
    287287            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    288288        $new      = $this->interactivity->process_directives( $original );
     
    311311                '</div>' .
    312312            '</template>' .
    313             '<div data-wp-each-child id="1" data-wp-bind--id="myPlugin::context.item">' .
     313            '<div data-wp-each-child="myPlugin::state.list" id="1" data-wp-bind--id="myPlugin::context.item">' .
    314314                'id: <span data-wp-text="myPlugin::context.item">1</span>' .
    315315            '</div>' .
    316             '<div data-wp-each-child id="2" data-wp-bind--id="myPlugin::context.item">' .
     316            '<div data-wp-each-child="myPlugin::state.list" id="2" data-wp-bind--id="myPlugin::context.item">' .
    317317                'id: <span data-wp-text="myPlugin::context.item">2</span>' .
    318318            '</div>' .
     
    356356                '<span data-wp-text="myPlugin::context.item.name"></span>' .
    357357            '</template>' .
    358             '<span data-wp-each-child data-wp-text="myPlugin::context.item.id">1</span>' .
    359             '<span data-wp-each-child data-wp-text="myPlugin::context.item.name">one</span>' .
    360             '<span data-wp-each-child data-wp-text="myPlugin::context.item.id">2</span>' .
    361             '<span data-wp-each-child data-wp-text="myPlugin::context.item.name">two</span>' .
     358            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item.id">1</span>' .
     359            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item.name">one</span>' .
     360            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item.id">2</span>' .
     361            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item.name">two</span>' .
    362362            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    363363        $new      = $this->interactivity->process_directives( $original );
     
    382382                '<span data-wp-text="myPlugin::context.myitem"></span>' .
    383383            '</template>' .
    384             '<span data-wp-each-child data-wp-text="myPlugin::context.myitem">1</span>' .
    385             '<span data-wp-each-child data-wp-text="myPlugin::context.myitem">2</span>' .
     384            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.myitem">1</span>' .
     385            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.myitem">2</span>' .
    386386            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    387387        $new      = $this->interactivity->process_directives( $original );
     
    407407                '<span data-wp-text="myPlugin::context.myItem"></span>' .
    408408            '</template>' .
    409             '<span data-wp-each-child data-wp-text="myPlugin::context.myItem">1</span>' .
    410             '<span data-wp-each-child data-wp-text="myPlugin::context.myItem">2</span>' .
     409            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.myItem">1</span>' .
     410            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.myItem">2</span>' .
    411411            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    412412        $new      = $this->interactivity->process_directives( $original );
     
    474474                '</template>' .
    475475            '</template>' .
    476             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">1</span>' .
    477             '<template data-wp-each-child data-wp-each--item2="myPlugin::state.list2">' .
     476            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">1</span>' .
     477            '<template data-wp-each-child="myPlugin::state.list" data-wp-each--item2="myPlugin::state.list2">' .
    478478                '<span data-wp-text="myPlugin::context.item2"></span>' .
    479479            '</template>' .
    480             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">3</span>' .
    481             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">4</span>' .
    482             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">2</span>' .
    483             '<template data-wp-each-child data-wp-each--item2="myPlugin::state.list2">' .
     480            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">3</span>' .
     481            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">4</span>' .
     482            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">2</span>' .
     483            '<template data-wp-each-child="myPlugin::state.list" data-wp-each--item2="myPlugin::state.list2">' .
    484484                '<span data-wp-text="myPlugin::context.item2"></span>' .
    485485            '</template>' .
    486             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">3</span>' .
    487             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">4</span>' .
     486            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">3</span>' .
     487            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">4</span>' .
    488488            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    489489        $new      = $this->interactivity->process_directives( $original );
     
    516516                '</template>' .
    517517            '</template>' .
    518             '<template data-wp-each-child data-wp-each--item2="myPlugin::state.list2">' .
     518            '<template data-wp-each-child="myPlugin::state.list" data-wp-each--item2="myPlugin::state.list2">' .
    519519                '<span data-wp-text="myPlugin::context.item1"></span>' .
    520520                '<span data-wp-text="myPlugin::context.item2"></span>' .
    521521            '</template>' .
    522             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">1</span>' .
    523             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">3</span>' .
    524             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">1</span>' .
    525             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">4</span>' .
    526             '<template data-wp-each-child data-wp-each--item2="myPlugin::state.list2">' .
     522            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">1</span>' .
     523            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">3</span>' .
     524            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">1</span>' .
     525            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">4</span>' .
     526            '<template data-wp-each-child="myPlugin::state.list" data-wp-each--item2="myPlugin::state.list2">' .
    527527                '<span data-wp-text="myPlugin::context.item1"></span>' .
    528528                '<span data-wp-text="myPlugin::context.item2"></span>' .
    529529            '</template>' .
    530             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">2</span>' .
    531             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">3</span>' .
    532             '<span data-wp-each-child data-wp-text="myPlugin::context.item1">2</span>' .
    533             '<span data-wp-each-child data-wp-text="myPlugin::context.item2">4</span>' .
     530            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">2</span>' .
     531            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">3</span>' .
     532            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item1">2</span>' .
     533            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item2">4</span>' .
    534534            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    535535        $new      = $this->interactivity->process_directives( $original );
     
    560560                '</template>' .
    561561            '</template>' .
    562             '<template data-wp-each-child data-wp-each--number="myPlugin::context.list">' .
     562            '<template data-wp-each-child="myPlugin::state.list2" data-wp-each--number="myPlugin::context.list">' .
    563563                '<span data-wp-text="myPlugin::context.number"></span>' .
    564564            '</template>' .
    565             '<span data-wp-each-child data-wp-text="myPlugin::context.number">1</span>' .
    566             '<span data-wp-each-child data-wp-text="myPlugin::context.number">2</span>' .
    567             '<template data-wp-each-child data-wp-each--number="myPlugin::context.list">' .
     565            '<span data-wp-each-child="myPlugin::state.list2" data-wp-text="myPlugin::context.number">1</span>' .
     566            '<span data-wp-each-child="myPlugin::state.list2" data-wp-text="myPlugin::context.number">2</span>' .
     567            '<template data-wp-each-child="myPlugin::state.list2" data-wp-each--number="myPlugin::context.list">' .
    568568                '<span data-wp-text="myPlugin::context.number"></span>' .
    569569            '</template>' .
    570             '<span data-wp-each-child data-wp-text="myPlugin::context.number">3</span>' .
    571             '<span data-wp-each-child data-wp-text="myPlugin::context.number">4</span>' .
     570            '<span data-wp-each-child="myPlugin::state.list2" data-wp-text="myPlugin::context.number">3</span>' .
     571            '<span data-wp-each-child="myPlugin::state.list2" data-wp-text="myPlugin::context.number">4</span>' .
    572572            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    573573        $new      = $this->interactivity->process_directives( $original );
     
    672672                '<span data-wp-text="myPlugin::context.item"></span>' .
    673673            '</template>' .
    674             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    675             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
    676             '<div data-wp-bind--id="myPlugin::state.after">Text</div>';
    677         $expected = '' .
    678             '<template data-wp-each="myPlugin::state.list">' .
    679                 '<span data-wp-text="myPlugin::context.item"></span>' .
    680             '</template>' .
    681             '<span data-wp-each-child data-wp-text="myPlugin::context.item">1</span>' .
    682             '<span data-wp-each-child data-wp-text="myPlugin::context.item">2</span>' .
     674            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     675            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
     676            '<div data-wp-bind--id="myPlugin::state.after">Text</div>';
     677        $expected = '' .
     678            '<template data-wp-each="myPlugin::state.list">' .
     679                '<span data-wp-text="myPlugin::context.item"></span>' .
     680            '</template>' .
     681            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">1</span>' .
     682            '<span data-wp-each-child="myPlugin::state.list" data-wp-text="myPlugin::context.item">2</span>' .
    683683            '<div id="after-wp-each" data-wp-bind--id="myPlugin::state.after">Text</div>';
    684684        $new      = $this->interactivity->process_directives( $original );
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php

    r60729 r60953  
    2626        parent::set_up();
    2727        $this->interactivity = new WP_Interactivity_API();
     28        wp_default_script_modules();
    2829    }
    2930
     
    315316
    316317        $this->assertSame( array( 'state' => array( 'myPlugin' => array( 'emptyArray' => array() ) ) ), $filter->get_args()[0][0] );
     318    }
     319    /**
     320     * Tests that derived state props invoked during directive evaluation are
     321     * serialized correctly.
     322     *
     323     * @ticket 63898
     324     */
     325    public function test_invoked_derived_state_props_are_serialized() {
     326        $returns_whatever = function () {
     327            return 'whatever';
     328        };
     329
     330        $returns_array = function () {
     331            return array( 'prop' => 'whatever' );
     332        };
     333
     334        $filter = $this->get_script_data_filter_result(
     335            function () use ( $returns_whatever, $returns_array ) {
     336                $this->interactivity->state(
     337                    'pluginWithInvokedDerivedState',
     338                    array(
     339                        'derivedProp' => $returns_whatever,
     340                        'nested'      => array(
     341                            'derivedProp'             => $returns_whatever,
     342                            'derivedPropReturnsArray' => $returns_array,
     343                        ),
     344                    )
     345                );
     346
     347                $this->interactivity->state(
     348                    'pluginWithInvokedDerivedStateReturningArray',
     349                    array(
     350                        'derivedProp' => $returns_whatever,
     351                        'nested'      => array(
     352                            'derivedProp'             => $returns_whatever,
     353                            'derivedPropReturnsArray' => $returns_array,
     354                        ),
     355                    )
     356                );
     357
     358                $this->interactivity->state(
     359                    'pluginWithoutInvokedDerivedState',
     360                    array(
     361                        'derivedProp' => $returns_whatever,
     362                        'nested'      => array(
     363                            'derivedProp' => $returns_whatever,
     364                        ),
     365                    )
     366                );
     367
     368                $this->set_internal_context_stack( array() );
     369
     370                // Multiple evaluations should be serialized only once.
     371                $this->set_internal_namespace_stack( 'pluginWithInvokedDerivedState' );
     372                $this->evaluate( 'state.derivedProp' );
     373                $this->evaluate( 'state.derivedProp' );
     374                $this->evaluate( 'state.nested.derivedProp' );
     375                $this->evaluate( 'state.nested.derivedProp' );
     376
     377                // Only the path part that points to a derived state prop should be serialized.
     378                $this->set_internal_namespace_stack( 'pluginWithInvokedDerivedStateReturningArray' );
     379                $this->evaluate( 'state.nested.derivedProp.prop' );
     380            }
     381        );
     382
     383        $this->assertSame(
     384            array(
     385                'state'                => array(
     386                    'pluginWithInvokedDerivedState'    => array(
     387                        'derivedProp' => $returns_whatever,
     388                        'nested'      => array(
     389                            'derivedProp'             => $returns_whatever,
     390                            'derivedPropReturnsArray' => $returns_array,
     391                        ),
     392                    ),
     393                    'pluginWithInvokedDerivedStateReturningArray' => array(
     394                        'derivedProp' => $returns_whatever,
     395                        'nested'      => array(
     396                            'derivedProp'             => $returns_whatever,
     397                            'derivedPropReturnsArray' => $returns_array,
     398                        ),
     399                    ),
     400                    'pluginWithoutInvokedDerivedState' => array(
     401                        'derivedProp' => $returns_whatever,
     402                        'nested'      => array(
     403                            'derivedProp' => $returns_whatever,
     404                        ),
     405                    ),
     406                ),
     407                'derivedStateClosures' => array(
     408                    'pluginWithInvokedDerivedState' => array(
     409                        'state.derivedProp',
     410                        'state.nested.derivedProp',
     411                    ),
     412                    'pluginWithInvokedDerivedStateReturningArray' => array(
     413                        'state.nested.derivedProp',
     414                    ),
     415                ),
     416            ),
     417            $filter->get_args()[0][0]
     418        );
    317419    }
    318420
Note: See TracChangeset for help on using the changeset viewer.