Make WordPress Core


Ignore:
Timestamp:
10/21/2025 12:43:51 AM (3 months ago)
Author:
westonruter
Message:

Script Loader: Add support for printing script modules at wp_footer.

This brings API parity with WP_Scripts by implementing opt-in support for printing in the footer via an in_footer argument. This argument can be supplied via the $args array passed to wp_enqueue_script_module() or wp_register_script_module(), alongside the existing fetchpriority key introduced in #61734. It can also be set for previously-registered script modules via WP_Script_Modules::set_in_footer(). This is not applicable to classic themes since modules are enqueued while blocks are rendered after wp_head has completed, so all script modules are printed in the footer anyway; the importmap script must be printed after all script modules have been enqueued.

Script modules used for interactive blocks (with the Interactivity API) are automatically printed in the footer. Such script modules should be deprioritized because they are not in the critical rendering path due to interactive blocks using server-side rendering. Script modules remain printed at wp_head by default, although this default should be revisited since they have deferred execution (and they are printed in the footer for classic themes already, as previously noted). Moving a script module to the footer ensures that its loading does not contend with the loading of critical resources, such as the LCP element's image resource, and LCP is improved as a result.

This also improves specificity of some PHP types, it ensures that script modules can't be registered with an empty ID, and it prevents printing script modules with empty src URLs.

Developed in https://github.com/WordPress/wordpress-develop/pull/9867

Follow-up to [60704].

Props b1ink0, westonruter, jonsurrell, peterwilsoncc, vipulpatil, mindctrl.
See #61734.
Fixes #63486.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60951 r60999  
    5757     */
    5858    public function get_enqueued_script_modules(): array {
    59         $modules = array();
    60 
    61         $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ) );
    62         while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
    63             $this->assertSame( 'module', $p->get_attribute( 'type' ) );
    64             $this->assertIsString( $p->get_attribute( 'id' ) );
    65             $this->assertIsString( $p->get_attribute( 'src' ) );
    66             $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) );
    67 
    68             $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
    69             $fetchpriority  = $p->get_attribute( 'fetchpriority' );
    70             $modules[ $id ] = array_merge(
    71                 array(
    72                     'url'           => $p->get_attribute( 'src' ),
    73                     'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
    74                 ),
    75                 ...array_map(
    76                     static function ( $attribute_name ) use ( $p ) {
    77                         return array( $attribute_name => $p->get_attribute( $attribute_name ) );
    78                     },
    79                     $p->get_attribute_names_with_prefix( 'data-' )
    80                 )
    81             );
    82         }
     59        $get_modules = function ( string $html, bool $in_footer ): array {
     60            $modules = array();
     61            $p       = new WP_HTML_Tag_Processor( $html );
     62            while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
     63                $this->assertSame( 'module', $p->get_attribute( 'type' ) );
     64                $this->assertIsString( $p->get_attribute( 'id' ) );
     65                $this->assertIsString( $p->get_attribute( 'src' ) );
     66                $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) );
     67
     68                $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
     69                $fetchpriority  = $p->get_attribute( 'fetchpriority' );
     70                $modules[ $id ] = array_merge(
     71                    array(
     72                        'url'           => $p->get_attribute( 'src' ),
     73                        'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     74                        'in_footer'     => $in_footer,
     75                    ),
     76                    ...array_map(
     77                        static function ( $attribute_name ) use ( $p ) {
     78                            return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     79                        },
     80                        $p->get_attribute_names_with_prefix( 'data-' )
     81                    )
     82                );
     83            }
     84            return $modules;
     85        };
     86
     87        $modules = array_merge(
     88            $get_modules( get_echo( array( $this->script_modules, 'print_head_enqueued_script_modules' ) ), false ),
     89            $get_modules( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ), true )
     90        );
    8391
    8492        return $modules;
     
    148156
    149157    /**
     158     * Test wp_register_script_module() with empty ID.
     159     *
     160     * @ticket 63486
     161     *
     162     * @expectedIncorrectUsage WP_Script_Modules::register
     163     *
     164     * @covers ::wp_register_script_module
     165     * @covers WP_Script_Modules::register
     166     */
     167    public function test_register_with_empty_id() {
     168        wp_register_script_module( '', '/null-and-void.js' );
     169        $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) );
     170    }
     171
     172    /**
     173     * Test wp_enqueue_script_module() with empty ID.
     174     *
     175     * @ticket 63486
     176     *
     177     * @expectedIncorrectUsage WP_Script_Modules::enqueue
     178     *
     179     * @covers ::wp_enqueue_script_module
     180     * @covers WP_Script_Modules::enqueue
     181     */
     182    public function test_enqueue_with_empty_id() {
     183        wp_enqueue_script_module( '', '/null-and-void.js' );
     184        $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) );
     185        $this->assertNotContains( '', wp_script_modules()->get_queue() );
     186    }
     187
     188    /**
    150189     * Tests various ways of registering, enqueueing, dequeuing, and deregistering a script module.
    151190     *
     
    153192     *
    154193     * @ticket 56313
     194     * @ticket 63486
    155195     *
    156196     * @dataProvider data_test_register_and_enqueue_script_module
     
    164204     * @covers ::wp_deregister_script_module()
    165205     * @covers WP_Script_Modules::deregister()
     206     * @covers WP_Script_Modules::get_queue()
     207     * @covers WP_Script_Modules::get_marked_for_enqueue()
    166208     * @covers WP_Script_Modules::set_fetchpriority()
     209     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    167210     * @covers WP_Script_Modules::print_enqueued_script_modules()
    168211     * @covers WP_Script_Modules::print_import_map()
     
    180223            }
    181224        };
     225
     226        $reflection_class       = new ReflectionClass( wp_script_modules() );
     227        $get_marked_for_enqueue = $reflection_class->getMethod( 'get_marked_for_enqueue' );
     228        if ( PHP_VERSION_ID < 80100 ) {
     229            $get_marked_for_enqueue->setAccessible( true );
     230        }
    182231
    183232        $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) {
     
    201250        // Minimal args.
    202251        $register_and_enqueue( 'a', '/a.js' );
     252        $this->assertSame( array( 'a' ), wp_script_modules()->get_queue(), 'Expected queue to match.' );
     253        $marked_for_enqueue = $get_marked_for_enqueue->invoke( wp_script_modules() );
     254        $this->assertSame( wp_script_modules()->get_queue(), array_keys( $marked_for_enqueue ), 'Expected get_queue() to match keys returned by get_marked_for_enqueue().' );
     255        $this->assertIsArray( $marked_for_enqueue['a'], 'Expected script module "a" to have an array entry.' );
     256        $this->assertSame( '/a.js', $marked_for_enqueue['a']['src'], 'Expected script module "a" to have the given src.' );
    203257
    204258        // One Dependency.
     
    206260        $register_and_enqueue( 'b', '/b.js', array( 'b-dep' ) );
    207261        $this->assertTrue( wp_script_modules()->set_fetchpriority( 'b', 'low' ) );
     262        $this->assertSame( array( 'a', 'b' ), wp_script_modules()->get_queue() );
    208263
    209264        // Two dependencies with different formats and a false version.
     
    222277            false
    223278        );
     279        $this->assertSame( array( 'a', 'b', 'c' ), wp_script_modules()->get_queue() );
    224280
    225281        // Two dependencies, one imported statically and the other dynamically, with a null version.
     
    241297            null
    242298        );
     299        $this->assertSame( array( 'a', 'b', 'c', 'd' ), wp_script_modules()->get_queue() );
    243300
    244301        // No dependencies, with a string version version.
     
    249306            '1.0.0'
    250307        );
     308        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e' ), wp_script_modules()->get_queue() );
    251309
    252310        // No dependencies, with a string version and fetch priority.
     
    258316            array( 'fetchpriority' => 'auto' )
    259317        );
     318        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f' ), wp_script_modules()->get_queue() );
    260319
    261320        // No dependencies, with a string version and fetch priority of low.
     
    267326            array( 'fetchpriority' => 'low' )
    268327        );
     328        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g' ), wp_script_modules()->get_queue() );
    269329
    270330        // No dependencies, with a string version and fetch priority of high.
     
    276336            array( 'fetchpriority' => 'high' )
    277337        );
     338        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() );
     339
     340        // Register and enqueue something which we'll dequeue right away.
     341        $register_and_enqueue(
     342            'i',
     343            '/i.js',
     344            array(),
     345            '3.0.0'
     346        );
     347        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() );
     348
     349        // Register and enqueue something which we'll deregister right away.
     350        $register_and_enqueue(
     351            'j',
     352            '/j.js',
     353            array(),
     354            '3.0.0'
     355        );
     356        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' ), wp_script_modules()->get_queue() );
     357
     358        // Make sure unregister functions work.
     359        $deregister_id = 'j';
     360        $this->assertArrayHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) );
     361        if ( $use_global_function ) {
     362            wp_deregister_script_module( $deregister_id );
     363        } else {
     364            wp_script_modules()->deregister( $deregister_id );
     365        }
     366        $this->assertArrayNotHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) );
     367        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() );
     368
     369        // Make sure dequeue functions work.
     370        $dequeue_id = 'i';
     371        $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) );
     372        if ( $use_global_function ) {
     373            wp_dequeue_script_module( $dequeue_id );
     374        } else {
     375            wp_script_modules()->dequeue( $dequeue_id );
     376        }
     377        $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) );
     378        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() );
    278379
    279380        $actual = array(
     
    309410                        'url'           => '/a.js?ver=99.9.9',
    310411                        'fetchpriority' => 'auto',
     412                        'in_footer'     => false,
    311413                    ),
    312414                    'b' => array(
    313415                        'url'           => '/b.js?ver=99.9.9',
    314416                        'fetchpriority' => 'low',
     417                        'in_footer'     => false,
    315418                    ),
    316419                    'c' => array(
    317420                        'url'           => '/c.js?ver=99.9.9',
    318421                        'fetchpriority' => 'auto',
     422                        'in_footer'     => false,
    319423                    ),
    320424                    'd' => array(
    321425                        'url'           => '/d.js',
    322426                        'fetchpriority' => 'auto',
     427                        'in_footer'     => false,
    323428                    ),
    324429                    'e' => array(
    325430                        'url'           => '/e.js?ver=1.0.0',
    326431                        'fetchpriority' => 'auto',
     432                        'in_footer'     => false,
    327433                    ),
    328434                    'f' => array(
    329435                        'url'           => '/f.js?ver=2.0.0',
    330436                        'fetchpriority' => 'auto',
     437                        'in_footer'     => false,
    331438                    ),
    332439                    'g' => array(
    333440                        'url'           => '/g.js?ver=2.0.0',
    334441                        'fetchpriority' => 'low',
     442                        'in_footer'     => false,
    335443                    ),
    336444                    'h' => array(
    337445                        'url'           => '/h.js?ver=3.0.0',
    338446                        'fetchpriority' => 'high',
     447                        'in_footer'     => false,
    339448                    ),
    340449                ),
     
    346455                    'd-dynamic-dep' => '/d-dynamic-dep.js?ver=99.9.9',
    347456                ),
    348             ),
    349             $actual,
    350             "Snapshot:\n" . var_export( $actual, true )
    351         );
    352 
    353         // Dequeue the first half of the scripts.
    354         foreach ( array( 'a', 'b', 'c', 'd' ) as $id ) {
    355             if ( $use_global_function ) {
    356                 wp_dequeue_script_module( $id );
    357             } else {
    358                 wp_script_modules()->dequeue( $id );
    359             }
    360         }
    361 
    362         $actual = array(
    363             'preload_links' => $this->get_preloaded_script_modules(),
    364             'script_tags'   => $this->get_enqueued_script_modules(),
    365             'import_map'    => $this->get_import_map(),
    366         );
    367         $this->assertSame(
    368             array(
    369                 'preload_links' => array(),
    370                 'script_tags'   => array(
    371                     'e' => array(
    372                         'url'           => '/e.js?ver=1.0.0',
    373                         'fetchpriority' => 'auto',
    374                     ),
    375                     'f' => array(
    376                         'url'           => '/f.js?ver=2.0.0',
    377                         'fetchpriority' => 'auto',
    378                     ),
    379                     'g' => array(
    380                         'url'           => '/g.js?ver=2.0.0',
    381                         'fetchpriority' => 'low',
    382                     ),
    383                     'h' => array(
    384                         'url'           => '/h.js?ver=3.0.0',
    385                         'fetchpriority' => 'high',
    386                     ),
    387                 ),
    388                 'import_map'    => array(),
    389             ),
    390             $actual,
    391             "Snapshot:\n" . var_export( $actual, true )
    392         );
    393 
    394         // Unregister the remaining scripts.
    395         foreach ( array( 'e', 'f', 'g', 'h' ) as $id ) {
    396             if ( $use_global_function ) {
    397                 wp_dequeue_script_module( $id );
    398             } else {
    399                 wp_script_modules()->dequeue( $id );
    400             }
    401         }
    402 
    403         $actual = array(
    404             'preload_links' => $this->get_preloaded_script_modules(),
    405             'script_tags'   => $this->get_enqueued_script_modules(),
    406             'import_map'    => $this->get_import_map(),
    407         );
    408         $this->assertSame(
    409             array(
    410                 'preload_links' => array(),
    411                 'script_tags'   => array(),
    412                 'import_map'    => array(),
    413457            ),
    414458            $actual,
     
    443487     *
    444488     * @ticket 56313
     489     * @ticket 63486
    445490     *
    446491     * @covers WP_Script_Modules::register()
    447492     * @covers WP_Script_Modules::enqueue()
     493     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    448494     * @covers WP_Script_Modules::print_enqueued_script_modules()
    449495     * @covers WP_Script_Modules::set_fetchpriority()
     496     * @covers WP_Script_Modules::set_in_footer()
    450497     */
    451498    public function test_wp_enqueue_script_module() {
    452499        $this->script_modules->register( 'foo', '/foo.js' );
    453         $this->script_modules->register( 'bar', '/bar.js', array(), false, array( 'fetchpriority' => 'high' ) );
     500        $this->script_modules->register(
     501            'bar',
     502            '/bar.js',
     503            array(),
     504            false,
     505            array(
     506                'fetchpriority' => 'high',
     507                'in_footer'     => true,
     508            )
     509        );
    454510        $this->script_modules->register( 'baz', '/baz.js' );
    455511        $this->assertTrue( $this->script_modules->set_fetchpriority( 'baz', 'low' ) );
     512        $this->assertTrue( $this->script_modules->set_in_footer( 'baz', true ) );
    456513        $this->script_modules->enqueue( 'foo' );
    457514        $this->script_modules->enqueue( 'bar' );
     
    463520        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
    464521        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
     522        $this->assertFalse( $enqueued_script_modules['foo']['in_footer'] );
    465523        $this->assertStringStartsWith( '/bar.js', $enqueued_script_modules['bar']['url'] );
    466524        $this->assertSame( 'high', $enqueued_script_modules['bar']['fetchpriority'] );
     525        $this->assertTrue( $enqueued_script_modules['bar']['in_footer'] );
    467526        $this->assertStringStartsWith( '/baz.js', $enqueued_script_modules['baz']['url'] );
    468527        $this->assertSame( 'low', $enqueued_script_modules['baz']['fetchpriority'] );
     528        $this->assertTrue( $enqueued_script_modules['baz']['in_footer'] );
     529    }
     530
     531    /**
     532     * Tests that no script is printed for a script without a src.
     533     *
     534     * @ticket 63486
     535     *
     536     * @covers WP_Script_Modules::register()
     537     * @covers WP_Script_Modules::enqueue()
     538     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     539     * @covers WP_Script_Modules::print_enqueued_script_modules()
     540     * @covers WP_Script_Modules::get_src()
     541     */
     542    public function test_wp_enqueue_script_module_with_empty_src() {
     543        wp_enqueue_script_module( 'with-src', '/src.js' );
     544        wp_register_script_module( 'without-src', '' );
     545        wp_register_script_module( 'without-src-but-filtered', '' );
     546        wp_enqueue_script_module( 'without-src' );
     547        wp_enqueue_script_module( 'without-src-but-filtered' );
     548        $this->assertSame( array( 'with-src', 'without-src', 'without-src-but-filtered' ), wp_script_modules()->get_queue() );
     549        add_filter(
     550            'script_module_loader_src',
     551            static function ( $src, $id ) {
     552                if ( 'without-src-but-filtered' === $id ) {
     553                    $src = '/was-empty-but-added-via-filter.js';
     554                }
     555                return $src;
     556            },
     557            10,
     558            2
     559        );
     560        $actual = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     561        $this->assertEqualHTML(
     562            '
     563                <script type="module" src="/src.js?ver=6.9-alpha-60093-src" id="with-src-js-module"></script>
     564                <script type="module" src="/was-empty-but-added-via-filter.js" id="without-src-but-filtered-js-module"></script>
     565            ',
     566            $actual,
     567            '<body>',
     568            "Expected only one SCRIPT tag to be printed. Snapshot:\n$actual"
     569        );
    469570    }
    470571
     
    473574    *
    474575    * @ticket 56313
     576    * @ticket 63486
    475577    *
    476578    * @covers WP_Script_Modules::register()
    477579    * @covers WP_Script_Modules::enqueue()
    478580    * @covers WP_Script_Modules::dequeue()
     581    * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    479582    * @covers WP_Script_Modules::print_enqueued_script_modules()
    480583    */
     
    571674    *
    572675    * @ticket 56313
     676    * @ticket 63486
    573677    *
    574678    * @covers WP_Script_Modules::register()
    575679    * @covers WP_Script_Modules::enqueue()
     680    * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    576681    * @covers WP_Script_Modules::print_enqueued_script_modules()
    577682    */
     
    594699     *
    595700     * @ticket 56313
     701     * @ticket 63486
    596702     *
    597703     * @covers WP_Script_Modules::register()
    598704     * @covers WP_Script_Modules::enqueue()
    599705     * @covers WP_Script_Modules::dequeue()
     706     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    600707     * @covers WP_Script_Modules::print_enqueued_script_modules()
    601708     */
     
    9611068     *
    9621069     * @ticket 56313
     1070     * @ticket 63486
    9631071     *
    9641072     * @covers WP_Script_Modules::register()
    9651073     * @covers WP_Script_Modules::enqueue()
     1074     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    9661075     * @covers WP_Script_Modules::print_enqueued_script_modules()
    9671076     * @covers WP_Script_Modules::print_import_map()
     
    9991108     *
    10001109     * @ticket 56313
     1110     * @ticket 63486
    10011111     *
    10021112     * @covers WP_Script_Modules::enqueue()
     1113     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10031114     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10041115     */
     
    10171128     *
    10181129     * @ticket 56313
     1130     * @ticket 63486
    10191131     *
    10201132     * @covers WP_Script_Modules::enqueue()
     1133     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10211134     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10221135     */
     
    10361149     *
    10371150     * @ticket 56313
     1151     * @ticket 63486
    10381152     *
    10391153     * @covers WP_Script_Modules::enqueue()
     1154     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10401155     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10411156     */
     
    10621177     *
    10631178     * @ticket 56313
     1179     * @ticket 63486
    10641180     *
    10651181     * @covers WP_Script_Modules::register()
    10661182     * @covers WP_Script_Modules::enqueue()
     1183     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10671184     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10681185     * @covers WP_Script_Modules::print_import_map()
     
    13161433
    13171434    /**
     1435     * Tests ways of setting in_footer.
     1436     *
     1437     * @ticket 63486
     1438     * @ticket 63486
     1439     *
     1440     * @covers ::wp_register_script_module
     1441     * @covers ::wp_enqueue_script_module
     1442     * @covers WP_Script_Modules::set_in_footer
     1443     */
     1444    public function test_in_footer_methods() {
     1445        wp_register_script_module( 'default', '/default.js', array(), null );
     1446        wp_enqueue_script_module( 'default' );
     1447
     1448        wp_register_script_module( 'in-footer-via-register', '/in-footer-via-register.js', array(), null, array( 'in_footer' => true ) );
     1449        wp_enqueue_script_module( 'in-footer-via-register' );
     1450
     1451        wp_enqueue_script_module( 'in-footer-via-enqueue', '/in-footer-via-enqueue.js', array(), null, array( 'in_footer' => true ) );
     1452
     1453        wp_enqueue_script_module( 'not-in-footer-via-enqueue', '/not-in-footer-via-enqueue.js', array(), null, array( 'in_footer' => false ) );
     1454
     1455        wp_enqueue_script_module( 'in-footer-via-override', '/in-footer-via-override.js' );
     1456        wp_script_modules()->set_in_footer( 'in-footer-via-override', true );
     1457
     1458        wp_enqueue_script_module( 'not-in-footer-via-override', '/not-in-footer-via-override.js', array(), null, array( 'in_footer' => true ) );
     1459        wp_script_modules()->set_in_footer( 'not-in-footer-via-override', false );
     1460
     1461        $actual_head   = get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) );
     1462        $actual_footer = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1463
     1464        $this->assertEqualHTML(
     1465            $actual_head,
     1466            '
     1467                <script type="module" src="/default.js" id="default-js-module"></script>
     1468                <script type="module" src="/not-in-footer-via-enqueue.js" id="not-in-footer-via-enqueue-js-module"></script>
     1469                <script type="module" src="/not-in-footer-via-override.js" id="not-in-footer-via-override-js-module"></script>
     1470            ',
     1471            '<body>',
     1472            "Expected equal script modules in the HEAD. Snapshot:\n$actual_head"
     1473        );
     1474        $this->assertEqualHTML(
     1475            $actual_footer,
     1476            '
     1477                <script type="module" src="/in-footer-via-register.js" id="in-footer-via-register-js-module"></script>
     1478                <script type="module" src="/in-footer-via-enqueue.js" id="in-footer-via-enqueue-js-module"></script>
     1479                <script type="module" src="/in-footer-via-override.js?ver=6.9-alpha-60093-src" id="in-footer-via-override-js-module"></script>
     1480            ',
     1481            '<body>',
     1482            "Expected equal script modules in the footer. Snapshot:\n$actual_footer"
     1483        );
     1484    }
     1485
     1486    /**
    13181487     * Tests that a script module with an invalid fetchpriority value gets a value of auto.
    13191488     *
     
    13781547                            'url'                   => '/bajo.js',
    13791548                            'fetchpriority'         => 'high',
     1549                            'in_footer'             => false,
    13801550                            'data-wp-fetchpriority' => 'low',
    13811551                        ),
     
    14001570                            'url'                   => '/auto.js',
    14011571                            'fetchpriority'         => 'high',
     1572                            'in_footer'             => false,
    14021573                            'data-wp-fetchpriority' => 'auto',
    14031574                        ),
     
    14131584                'expected' => array(
    14141585                    'preload_links' => array(
    1415                         'auto' => array(
    1416                             'url'           => '/auto.js',
    1417                             'fetchpriority' => 'high',
    1418                         ),
    14191586                        'bajo' => array(
    14201587                            'url'                   => '/bajo.js',
     
    14221589                            'data-wp-fetchpriority' => 'low',
    14231590                        ),
     1591                        'auto' => array(
     1592                            'url'           => '/auto.js',
     1593                            'fetchpriority' => 'high',
     1594                        ),
    14241595                    ),
    14251596                    'script_tags'   => array(
     
    14271598                            'url'           => '/alto.js',
    14281599                            'fetchpriority' => 'high',
     1600                            'in_footer'     => false,
    14291601                        ),
    14301602                    ),
     
    15401712        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    15411713        $expected = '
    1542             <link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
    1543             <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
     1714            <link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
    15441715            <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
    15451716            <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="low">
    1546             <link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
     1717            <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
     1718            <link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
    15471719            <link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high">
    15481720            <script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
     
    15851757        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    15861758        $expected = '
    1587             <link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
    15881759            <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="low">
    15891760            <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1761            <link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
    15901762            <link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
     1763            <link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
    15911764            <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="high">
    1592             <link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
    15931765            <script type="module" src="/x.js" id="x-js-module" fetchpriority="low"></script>
    15941766            <script type="module" src="/y.js" id="y-js-module"></script>
     
    16011773     * Tests that default script modules are printed as expected.
    16021774     *
     1775     * @ticket 63486
     1776     *
    16031777     * @covers ::wp_default_script_modules
    16041778     * @covers WP_Script_Modules::print_script_module_preloads
     1779     * @covers WP_Script_Modules::print_head_enqueued_script_modules
    16051780     * @covers WP_Script_Modules::print_enqueued_script_modules
    16061781     */
     
    16101785        wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
    16111786
    1612         $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
    1613         $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    1614 
    1615         $actual = $this->normalize_markup_for_snapshot( $actual );
    1616 
    1617         $expected = '
    1618             <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
    1619             <script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
    1620             <script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
    1621         ';
    1622         $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1787        $actual_preloads = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ) );
     1788        $this->assertEqualHTML(
     1789            '
     1790                <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
     1791            ',
     1792            $actual_preloads,
     1793            '<body>',
     1794            "Snapshot:\n$actual_preloads"
     1795        );
     1796
     1797        $actual_head_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) ) );
     1798        $this->assertEqualHTML(
     1799            '',
     1800            $actual_head_script_modules,
     1801            '<body>',
     1802            "Snapshot:\n$actual_head_script_modules"
     1803        );
     1804
     1805        $actual_footer_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ) );
     1806        $this->assertEqualHTML(
     1807            '
     1808                <script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
     1809                <script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
     1810            ',
     1811            $actual_footer_script_modules,
     1812            '<body>',
     1813            "Snapshot:\n$actual_footer_script_modules"
     1814        );
    16231815    }
    16241816
     
    16471839        $expected = '
    16481840            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1841            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    16491842            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    1650             <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    16511843            <script type="module" src="/super-important-module.js" id="super-important-js-module" fetchpriority="high"></script>
    16521844        ';
     
    17311923        );
    17321924    }
     1925
     1926    /**
     1927     * Tests various ways of printing and dependency ordering of script modules.
     1928     *
     1929     * This ensures that the global function aliases pass all the same parameters as the class methods.
     1930     *
     1931     * @ticket 63486
     1932     *
     1933     * @dataProvider data_test_register_and_enqueue_script_module
     1934     *
     1935     * @covers ::wp_register_script_module()
     1936     * @covers WP_Script_Modules::register()
     1937     * @covers ::wp_enqueue_script_module()
     1938     * @covers WP_Script_Modules::enqueue()
     1939     * @covers ::wp_dequeue_script_module()
     1940     * @covers WP_Script_Modules::dequeue()
     1941     * @covers ::wp_deregister_script_module()
     1942     * @covers WP_Script_Modules::deregister()
     1943     * @covers WP_Script_Modules::set_fetchpriority()
     1944     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     1945     * @covers WP_Script_Modules::print_enqueued_script_modules()
     1946     * @covers WP_Script_Modules::print_import_map()
     1947     * @covers WP_Script_Modules::print_script_module_preloads()
     1948     */
     1949    public function test_script_module_printing_and_dependency_ordering( bool $use_global_function, bool $only_enqueue ) {
     1950        global $wp_version;
     1951        $wp_version = '99.9.9';
     1952
     1953        $register = static function ( ...$args ) use ( $use_global_function ) {
     1954            if ( $use_global_function ) {
     1955                wp_register_script_module( ...$args );
     1956            } else {
     1957                wp_script_modules()->register( ...$args );
     1958            }
     1959        };
     1960
     1961        $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) {
     1962            if ( $use_global_function ) {
     1963                if ( $only_enqueue ) {
     1964                    wp_enqueue_script_module( ...$args );
     1965                } else {
     1966                    wp_register_script_module( ...$args );
     1967                    wp_enqueue_script_module( $args[0] );
     1968                }
     1969            } else {
     1970                if ( $only_enqueue ) {
     1971                    wp_script_modules()->enqueue( ...$args );
     1972                } else {
     1973                    wp_script_modules()->register( ...$args );
     1974                    wp_script_modules()->enqueue( $args[0] );
     1975                }
     1976            }
     1977        };
     1978
     1979        $deregister = static function ( array $ids ) use ( $use_global_function ) {
     1980            foreach ( $ids as $id ) {
     1981                if ( $use_global_function ) {
     1982                    wp_deregister_script_module( $id );
     1983                } else {
     1984                    wp_script_modules()->deregister( $id );
     1985                }
     1986            }
     1987        };
     1988
     1989        // Test script module is placed in footer when in_footer is true.
     1990        $register_and_enqueue( 'a', '/a.js', array(), '1.0.0', array( 'in_footer' => true ) );
     1991
     1992        $actual = array(
     1993            'preload_links' => $this->get_preloaded_script_modules(),
     1994            'script_tags'   => $this->get_enqueued_script_modules(),
     1995            'import_map'    => $this->get_import_map(),
     1996        );
     1997        $this->assertSame(
     1998            array(
     1999                'preload_links' => array(),
     2000                'script_tags'   => array(
     2001                    'a' => array(
     2002                        'url'           => '/a.js?ver=1.0.0',
     2003                        'fetchpriority' => 'auto',
     2004                        'in_footer'     => true,
     2005                    ),
     2006                ),
     2007                'import_map'    => array(),
     2008            ),
     2009            $actual,
     2010            "Snapshot:\n" . var_export( $actual, true )
     2011        );
     2012
     2013        $deregister( array( 'a' ) );
     2014
     2015        // Test that dependant also gets placed in footer when its dependency is in footer.
     2016        $register_and_enqueue( 'b', '/b.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2017        $register_and_enqueue( 'c', '/c.js', array( 'b' ), '1.0.0' );
     2018
     2019        $actual = array(
     2020            'preload_links' => $this->get_preloaded_script_modules(),
     2021            'script_tags'   => $this->get_enqueued_script_modules(),
     2022            'import_map'    => $this->get_import_map(),
     2023        );
     2024        $this->assertSame(
     2025            array(
     2026                'preload_links' => array(),
     2027                'script_tags'   => array(
     2028                    'b' => array(
     2029                        'url'           => '/b.js?ver=1.0.0',
     2030                        'fetchpriority' => 'auto',
     2031                        'in_footer'     => true,
     2032                    ),
     2033                    'c' => array(
     2034                        'url'           => '/c.js?ver=1.0.0',
     2035                        'fetchpriority' => 'auto',
     2036                        'in_footer'     => true,
     2037                    ),
     2038                ),
     2039                'import_map'    => array(
     2040                    'b' => '/b.js?ver=1.0.0',
     2041                ),
     2042            ),
     2043            $actual,
     2044            "Snapshot:\n" . var_export( $actual, true )
     2045        );
     2046
     2047        $deregister( array( 'b', 'c ' ) );
     2048
     2049        // Test that registered dependency in footer doesn't place dependant in footer.
     2050        $register( 'd', '/d.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2051        $register_and_enqueue( 'e', '/e.js', array( 'd' ), '1.0.0' );
     2052
     2053        $actual = array(
     2054            'preload_links' => $this->get_preloaded_script_modules(),
     2055            'script_tags'   => $this->get_enqueued_script_modules(),
     2056            'import_map'    => $this->get_import_map(),
     2057        );
     2058        $this->assertSame(
     2059            array(
     2060                'preload_links' => array(
     2061                    'd' => array(
     2062                        'url'           => '/d.js?ver=1.0.0',
     2063                        'fetchpriority' => 'auto',
     2064                    ),
     2065                ),
     2066                'script_tags'   => array(
     2067                    'e' => array(
     2068                        'url'           => '/e.js?ver=1.0.0',
     2069                        'fetchpriority' => 'auto',
     2070                        'in_footer'     => false,
     2071                    ),
     2072                ),
     2073                'import_map'    => array(
     2074                    'd' => '/d.js?ver=1.0.0',
     2075                ),
     2076            ),
     2077            $actual,
     2078            "Snapshot:\n" . var_export( $actual, true )
     2079        );
     2080
     2081        $deregister( array( 'd', 'e' ) );
     2082
     2083        // Test if one of the dependency is in footer, the dependant and other dependant dependencies are also placed in footer.
     2084        $register_and_enqueue( 'f', '/f.js', array(), '1.0.0' );
     2085        $register_and_enqueue( 'g', '/g.js', array( 'f' ), '1.0.0', array( 'in_footer' => true ) );
     2086        $register_and_enqueue( 'h', '/h.js', array( 'g' ), '1.0.0' );
     2087        $register_and_enqueue( 'i', '/i.js', array( 'h' ), '1.0.0' );
     2088
     2089        $actual = array(
     2090            'preload_links' => $this->get_preloaded_script_modules(),
     2091            'script_tags'   => $this->get_enqueued_script_modules(),
     2092            'import_map'    => $this->get_import_map(),
     2093        );
     2094
     2095        $this->assertSame(
     2096            array(
     2097                'preload_links' => array(),
     2098                'script_tags'   => array(
     2099                    'f' => array(
     2100                        'url'           => '/f.js?ver=1.0.0',
     2101                        'fetchpriority' => 'auto',
     2102                        'in_footer'     => false,
     2103                    ),
     2104                    'g' => array(
     2105                        'url'           => '/g.js?ver=1.0.0',
     2106                        'fetchpriority' => 'auto',
     2107                        'in_footer'     => true,
     2108                    ),
     2109                    'h' => array(
     2110                        'url'           => '/h.js?ver=1.0.0',
     2111                        'fetchpriority' => 'auto',
     2112                        'in_footer'     => true,
     2113                    ),
     2114                    'i' => array(
     2115                        'url'           => '/i.js?ver=1.0.0',
     2116                        'fetchpriority' => 'auto',
     2117                        'in_footer'     => true,
     2118                    ),
     2119                ),
     2120                'import_map'    => array(
     2121                    'f' => '/f.js?ver=1.0.0',
     2122                    'g' => '/g.js?ver=1.0.0',
     2123                    'h' => '/h.js?ver=1.0.0',
     2124                ),
     2125            ),
     2126            $actual,
     2127            "Snapshot:\n" . var_export( $actual, true )
     2128        );
     2129
     2130        $deregister( array( 'f', 'g', 'h', 'i' ) );
     2131
     2132        // Test dependency ordering when all scripts modules are enqueued in head.
     2133        // Expected order: j, k, l, m.
     2134        $register_and_enqueue( 'm', '/m.js', array( 'j', 'l' ), '1.0.0' );
     2135        $register_and_enqueue( 'k', '/k.js', array( 'j' ), '1.0.0' );
     2136        $register_and_enqueue( 'l', '/l.js', array( 'k' ), '1.0.0' );
     2137        $register_and_enqueue( 'j', '/j.js', array(), '1.0.0' );
     2138
     2139        $actual = array(
     2140            'preload_links' => $this->get_preloaded_script_modules(),
     2141            'script_tags'   => $this->get_enqueued_script_modules(),
     2142            'import_map'    => $this->get_import_map(),
     2143        );
     2144
     2145        $this->assertSame(
     2146            array(
     2147                'preload_links' => array(),
     2148                'script_tags'   => array(
     2149                    'j' => array(
     2150                        'url'           => '/j.js?ver=1.0.0',
     2151                        'fetchpriority' => 'auto',
     2152                        'in_footer'     => false,
     2153                    ),
     2154                    'k' => array(
     2155                        'url'           => '/k.js?ver=1.0.0',
     2156                        'fetchpriority' => 'auto',
     2157                        'in_footer'     => false,
     2158                    ),
     2159                    'l' => array(
     2160                        'url'           => '/l.js?ver=1.0.0',
     2161                        'fetchpriority' => 'auto',
     2162                        'in_footer'     => false,
     2163                    ),
     2164                    'm' => array(
     2165                        'url'           => '/m.js?ver=1.0.0',
     2166                        'fetchpriority' => 'auto',
     2167                        'in_footer'     => false,
     2168                    ),
     2169                ),
     2170                'import_map'    => array(
     2171                    'j' => '/j.js?ver=1.0.0',
     2172                    'l' => '/l.js?ver=1.0.0',
     2173                    'k' => '/k.js?ver=1.0.0',
     2174                ),
     2175            ),
     2176            $actual,
     2177            "Snapshot:\n" . var_export( $actual, true )
     2178        );
     2179
     2180        $deregister( array( 'j', 'k', 'l', 'm' ) );
     2181
     2182        // Test dependency ordering when scripts modules are enqueued in both head and footer.
     2183        // Expected order: q, n, o, p, r.
     2184        $register_and_enqueue( 'n', '/n.js', array( 'q' ), '1.0.0' );
     2185        $register_and_enqueue( 'q', '/q.js', array(), '1.0.0' );
     2186        $register_and_enqueue( 'o', '/o.js', array( 'n' ), '1.0.0', array( 'in_footer' => true ) );
     2187        $register_and_enqueue( 'r', '/r.js', array( 'q', 'o', 'p' ), '1.0.0' );
     2188        $register_and_enqueue( 'p', '/p.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2189
     2190        $actual = array(
     2191            'preload_links' => $this->get_preloaded_script_modules(),
     2192            'script_tags'   => $this->get_enqueued_script_modules(),
     2193            'import_map'    => $this->get_import_map(),
     2194        );
     2195
     2196        $this->assertSame(
     2197            array(
     2198                'preload_links' => array(),
     2199                'script_tags'   => array(
     2200                    'q' => array(
     2201                        'url'           => '/q.js?ver=1.0.0',
     2202                        'fetchpriority' => 'auto',
     2203                        'in_footer'     => false,
     2204                    ),
     2205                    'n' => array(
     2206                        'url'           => '/n.js?ver=1.0.0',
     2207                        'fetchpriority' => 'auto',
     2208                        'in_footer'     => false,
     2209                    ),
     2210                    'o' => array(
     2211                        'url'           => '/o.js?ver=1.0.0',
     2212                        'fetchpriority' => 'auto',
     2213                        'in_footer'     => true,
     2214                    ),
     2215                    'p' => array(
     2216                        'url'           => '/p.js?ver=1.0.0',
     2217                        'fetchpriority' => 'auto',
     2218                        'in_footer'     => true,
     2219                    ),
     2220                    'r' => array(
     2221                        'url'           => '/r.js?ver=1.0.0',
     2222                        'fetchpriority' => 'auto',
     2223                        'in_footer'     => true,
     2224                    ),
     2225                ),
     2226                'import_map'    => array(
     2227                    'q' => '/q.js?ver=1.0.0',
     2228                    'n' => '/n.js?ver=1.0.0',
     2229                    'o' => '/o.js?ver=1.0.0',
     2230                    'p' => '/p.js?ver=1.0.0',
     2231                ),
     2232            ),
     2233            $actual,
     2234            "Snapshot:\n" . var_export( $actual, true )
     2235        );
     2236    }
     2237
     2238    /**
     2239     * Tests various ways of printing and dependency ordering of script modules.
     2240     *
     2241     * @ticket 63486
     2242     *
     2243     * @dataProvider data_test_register_and_enqueue_script_module
     2244     *
     2245     * @covers ::wp_register_script_module()
     2246     * @covers WP_Script_Modules::register()
     2247     * @covers ::wp_enqueue_script_module()
     2248     * @covers WP_Script_Modules::enqueue()
     2249     * @covers ::wp_dequeue_script_module()
     2250     * @covers WP_Script_Modules::dequeue()
     2251     * @covers ::wp_deregister_script_module()
     2252     * @covers WP_Script_Modules::deregister()
     2253     * @covers WP_Script_Modules::set_fetchpriority()
     2254     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     2255     * @covers WP_Script_Modules::print_enqueued_script_modules()
     2256     * @covers WP_Script_Modules::print_import_map()
     2257     * @covers WP_Script_Modules::print_script_module_preloads()
     2258     */
     2259    public function test_static_import_dependency_with_dynamic_imports_depending_on_static_import_dependency() {
     2260        $get_dependency = function ( string $id, string $import ): array {
     2261            return compact( 'id', 'import' );
     2262        };
     2263
     2264        wp_register_script_module( 'enqueued', '/enqueued.js', array( $get_dependency( 'static1', 'static' ) ), null );
     2265        wp_register_script_module( 'static1', '/static1.js', array( $get_dependency( 'dynamic1', 'dynamic' ) ), null );
     2266        wp_register_script_module( 'dynamic1', '/dynamic1.js', array( $get_dependency( 'static2', 'static' ) ), null );
     2267        wp_register_script_module( 'static2', '/static2.js', array(), null );
     2268
     2269        wp_enqueue_script_module( 'enqueued' );
     2270        $import_map     = $this->get_import_map();
     2271        $preload_links  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     2272        $script_modules = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     2273
     2274        $this->assertEquals(
     2275            array(
     2276                'static1'  => '/static1.js',
     2277                'dynamic1' => '/dynamic1.js',
     2278                'static2'  => '/static2.js',
     2279            ),
     2280            $import_map,
     2281            "Expected import map to match snapshot:\n" . var_export( $import_map, true )
     2282        );
     2283        $this->assertEqualHTML(
     2284            '
     2285                <link rel="modulepreload" href="/static1.js" id="static1-js-modulepreload">
     2286            ',
     2287            $preload_links,
     2288            '<body>',
     2289            "Expected preload links to match snapshot:\n$preload_links"
     2290        );
     2291        $this->assertEqualHTML(
     2292            '
     2293                <script type="module" src="/enqueued.js" id="enqueued-js-module"></script>
     2294            ',
     2295            $script_modules,
     2296            '<body>',
     2297            "Expected script modules to match snapshot:\n$script_modules"
     2298        );
     2299    }
    17332300}
Note: See TracChangeset for help on using the changeset viewer.