Make WordPress Core


Ignore:
Timestamp:
09/03/2025 10:15:31 PM (6 months ago)
Author:
westonruter
Message:

Script Loader: Introduce fetchpriority for Scripts and Script Modules.

  • Allow scripts and script modules to be registered with a fetchpriority of auto (default), high, low:
    • When registering a script, add a fetchpriority arg to go alongside the strategy arg which was added for loading scripts with the defer and async loading strategies. See #12009.
    • For script modules, introduce an $args array parameter with a fetchpriority key to the wp_register_script_module(), and wp_enqueue_script_module() functions (and their respective underlying WP_Script_Modules::register() and WP_Script_Modules::enqueue() methods). This $args parameter corresponds with the same parameter used when registering non-module scripts.
    • Also for script modules, introduce WP_Script_Modules::set_fetchpriority() to override the fetchpriority for what was previously registered.
    • Emit a _doing_it_wrong() warning when an invalid fetchpriority value is used, and when fetchpriority is added to a script alias.
    • Include fetchpriority as an attribute on printed SCRIPT tags as well as on preload LINK tags for static script module dependencies.
  • Use a fetchpriority of low by default for:
    • Script modules used with the Interactivity API. For overriding this default in blocks, see Gutenberg#71366.
    • The comment-reply script.
  • Improve type checks and type hints.

Developed in GitHub PR, with companion for Gutenberg.

Props westonruter, jonsurrell, swissspidy, luisherranz, kraftbj, audrasjb, dennysdionigi.
Fixes #61734.

File:
1 edited

Legend:

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

    r58952 r60704  
    99 *
    1010 * @group script-modules
    11  *
    12  * @coversDefaultClass WP_Script_Modules
    1311 */
    1412class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase {
    1513
    1614    /**
     15     * @var WP_Script_Modules
     16     */
     17    protected $original_script_modules;
     18
     19    /**
     20     * @var string
     21     */
     22    protected $original_wp_version;
     23
     24    /**
    1725     * Instance of WP_Script_Modules.
    1826     *
     
    2533     */
    2634    public function set_up() {
     35        global $wp_script_modules, $wp_version;
    2736        parent::set_up();
    28         // Set up the WP_Script_Modules instance.
    29         $this->script_modules = new WP_Script_Modules();
     37        $this->original_script_modules = $wp_script_modules;
     38        $this->original_wp_version     = $wp_version;
     39        $wp_script_modules             = null;
     40        $this->script_modules          = wp_script_modules();
     41    }
     42
     43    /**
     44     * Tear down.
     45     */
     46    public function tear_down() {
     47        global $wp_script_modules, $wp_version;
     48        parent::tear_down();
     49        $wp_script_modules = $this->original_script_modules;
     50        $wp_version        = $this->original_wp_version;
    3051    }
    3152
     
    3556     * @return array Enqueued script module URLs, keyed by script module identifier.
    3657     */
    37     public function get_enqueued_script_modules() {
    38         $script_modules_markup   = get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) );
    39         $p                       = new WP_HTML_Tag_Processor( $script_modules_markup );
    40         $enqueued_script_modules = array();
    41 
     58    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' ) ) );
    4262        while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
    43             if ( 'module' === $p->get_attribute( 'type' ) ) {
    44                 $id                             = preg_replace( '/-js-module$/', '', $p->get_attribute( 'id' ) );
    45                 $enqueued_script_modules[ $id ] = $p->get_attribute( 'src' );
     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(
     71                'url'           => $p->get_attribute( 'src' ),
     72                'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     73            );
     74        }
     75
     76        return $modules;
     77    }
     78
     79    /**
     80     * Gets the script modules listed in the import map.
     81     *
     82     * @return array<string, string> Import map entry URLs, keyed by script module identifier.
     83     */
     84    public function get_import_map(): array {
     85        $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_import_map' ) ) );
     86        if ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
     87            $this->assertSame( 'importmap', $p->get_attribute( 'type' ) );
     88            $this->assertSame( 'wp-importmap', $p->get_attribute( 'id' ) );
     89            $data = json_decode( $p->get_modifiable_text(), true );
     90            $this->assertIsArray( $data );
     91            $this->assertArrayHasKey( 'imports', $data );
     92            return $data['imports'];
     93        } else {
     94            return array();
     95        }
     96    }
     97
     98    /**
     99     * Gets a list of preloaded script modules.
     100     *
     101     * @return array Preloaded script module URLs, keyed by script module identifier.
     102     */
     103    public function get_preloaded_script_modules(): array {
     104        $preloads = array();
     105
     106        $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_script_module_preloads' ) ) );
     107        while ( $p->next_tag( array( 'tag' => 'LINK' ) ) ) {
     108            $this->assertSame( 'modulepreload', $p->get_attribute( 'rel' ) );
     109            $this->assertIsString( $p->get_attribute( 'id' ) );
     110            $this->assertIsString( $p->get_attribute( 'href' ) );
     111            $this->assertStringEndsWith( '-js-modulepreload', $p->get_attribute( 'id' ) );
     112
     113            $id              = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) );
     114            $fetchpriority   = $p->get_attribute( 'fetchpriority' );
     115            $preloads[ $id ] = array(
     116                'url'           => $p->get_attribute( 'href' ),
     117                'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     118            );
     119        }
     120
     121        return $preloads;
     122    }
     123
     124    /**
     125     * Test wp_script_modules().
     126     *
     127     * @covers ::wp_script_modules()
     128     */
     129    public function test_wp_script_modules() {
     130        $this->assertSame( $this->script_modules, wp_script_modules() );
     131    }
     132
     133    /**
     134     * Tests various ways of registering, enqueueing, dequeuing, and deregistering a script module.
     135     *
     136     * This ensures that the global function aliases pass all the same parameters as the class methods.
     137     *
     138     * @ticket 56313
     139     *
     140     * @dataProvider data_test_register_and_enqueue_script_module
     141     *
     142     * @covers ::wp_register_script_module()
     143     * @covers WP_Script_Modules::register()
     144     * @covers ::wp_enqueue_script_module()
     145     * @covers WP_Script_Modules::enqueue()
     146     * @covers ::wp_dequeue_script_module()
     147     * @covers WP_Script_Modules::dequeue()
     148     * @covers ::wp_deregister_script_module()
     149     * @covers WP_Script_Modules::deregister()
     150     * @covers WP_Script_Modules::set_fetchpriority()
     151     * @covers WP_Script_Modules::print_enqueued_script_modules()
     152     * @covers WP_Script_Modules::print_import_map()
     153     * @covers WP_Script_Modules::print_script_module_preloads()
     154     */
     155    public function test_comprehensive_methods( bool $use_global_function, bool $only_enqueue ) {
     156        global $wp_version;
     157        $wp_version = '99.9.9';
     158
     159        $register = static function ( ...$args ) use ( $use_global_function ) {
     160            if ( $use_global_function ) {
     161                wp_register_script_module( ...$args );
     162            } else {
     163                wp_script_modules()->register( ...$args );
     164            }
     165        };
     166
     167        $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) {
     168            if ( $use_global_function ) {
     169                if ( $only_enqueue ) {
     170                    wp_enqueue_script_module( ...$args );
     171                } else {
     172                    wp_register_script_module( ...$args );
     173                    wp_enqueue_script_module( $args[0] );
     174                }
     175            } else {
     176                if ( $only_enqueue ) {
     177                    wp_script_modules()->enqueue( ...$args );
     178                } else {
     179                    wp_script_modules()->register( ...$args );
     180                    wp_script_modules()->enqueue( $args[0] );
     181                }
     182            }
     183        };
     184
     185        // Minimal args.
     186        $register_and_enqueue( 'a', '/a.js' );
     187
     188        // One Dependency.
     189        $register( 'b-dep', '/b-dep.js' );
     190        $register_and_enqueue( 'b', '/b.js', array( 'b-dep' ) );
     191        $this->assertTrue( wp_script_modules()->set_fetchpriority( 'b', 'low' ) );
     192
     193        // Two dependencies with different formats and a false version.
     194        $register( 'c-dep', '/c-static.js', array(), false, array( 'fetchpriority' => 'low' ) );
     195        $register( 'c-static-dep', '/c-static-dep.js', array(), false, array( 'fetchpriority' => 'high' ) );
     196        $register_and_enqueue(
     197            'c',
     198            '/c.js',
     199            array(
     200                'c-dep',
     201                array(
     202                    'id'     => 'c-static-dep',
     203                    'import' => 'static',
     204                ),
     205            ),
     206            false
     207        );
     208
     209        // Two dependencies, one imported statically and the other dynamically, with a null version.
     210        $register( 'd-static-dep', '/d-static-dep.js', array(), false, array( 'fetchpriority' => 'auto' ) );
     211        $register( 'd-dynamic-dep', '/d-dynamic-dep.js', array(), false, array( 'fetchpriority' => 'high' ) ); // Because this is a dynamic import dependency, the fetchpriority will not be reflected in the markup since no SCRIPT tag and no preload LINK are printed, and the importmap SCRIPT does not support designating a priority.
     212        $register_and_enqueue(
     213            'd',
     214            '/d.js',
     215            array(
     216                array(
     217                    'id'     => 'd-static-dep',
     218                    'import' => 'static',
     219                ),
     220                array(
     221                    'id'     => 'd-dynamic-dep',
     222                    'import' => 'dynamic',
     223                ),
     224            ),
     225            null
     226        );
     227
     228        // No dependencies, with a string version version.
     229        $register_and_enqueue(
     230            'e',
     231            '/e.js',
     232            array(),
     233            '1.0.0'
     234        );
     235
     236        // No dependencies, with a string version and fetch priority.
     237        $register_and_enqueue(
     238            'f',
     239            '/f.js',
     240            array(),
     241            '2.0.0',
     242            array( 'fetchpriority' => 'auto' )
     243        );
     244
     245        // No dependencies, with a string version and fetch priority of low.
     246        $register_and_enqueue(
     247            'g',
     248            '/g.js',
     249            array(),
     250            '2.0.0',
     251            array( 'fetchpriority' => 'low' )
     252        );
     253
     254        // No dependencies, with a string version and fetch priority of high.
     255        $register_and_enqueue(
     256            'h',
     257            '/h.js',
     258            array(),
     259            '3.0.0',
     260            array( 'fetchpriority' => 'high' )
     261        );
     262
     263        $actual = array(
     264            'preload_links' => $this->get_preloaded_script_modules(),
     265            'script_tags'   => $this->get_enqueued_script_modules(),
     266            'import_map'    => $this->get_import_map(),
     267        );
     268
     269        $this->assertSame(
     270            array(
     271                'preload_links' => array(
     272                    'b-dep'        => array(
     273                        'url'           => '/b-dep.js?ver=99.9.9',
     274                        'fetchpriority' => 'auto',
     275                    ),
     276                    'c-dep'        => array(
     277                        'url'           => '/c-static.js?ver=99.9.9',
     278                        'fetchpriority' => 'low',
     279                    ),
     280                    'c-static-dep' => array(
     281                        'url'           => '/c-static-dep.js?ver=99.9.9',
     282                        'fetchpriority' => 'high',
     283                    ),
     284                    'd-static-dep' => array(
     285                        'url'           => '/d-static-dep.js?ver=99.9.9',
     286                        'fetchpriority' => 'auto',
     287                    ),
     288                ),
     289                'script_tags'   => array(
     290                    'a' => array(
     291                        'url'           => '/a.js?ver=99.9.9',
     292                        'fetchpriority' => 'auto',
     293                    ),
     294                    'b' => array(
     295                        'url'           => '/b.js?ver=99.9.9',
     296                        'fetchpriority' => 'low',
     297                    ),
     298                    'c' => array(
     299                        'url'           => '/c.js?ver=99.9.9',
     300                        'fetchpriority' => 'auto',
     301                    ),
     302                    'd' => array(
     303                        'url'           => '/d.js',
     304                        'fetchpriority' => 'auto',
     305                    ),
     306                    'e' => array(
     307                        'url'           => '/e.js?ver=1.0.0',
     308                        'fetchpriority' => 'auto',
     309                    ),
     310                    'f' => array(
     311                        'url'           => '/f.js?ver=2.0.0',
     312                        'fetchpriority' => 'auto',
     313                    ),
     314                    'g' => array(
     315                        'url'           => '/g.js?ver=2.0.0',
     316                        'fetchpriority' => 'low',
     317                    ),
     318                    'h' => array(
     319                        'url'           => '/h.js?ver=3.0.0',
     320                        'fetchpriority' => 'high',
     321                    ),
     322                ),
     323                'import_map'    => array(
     324                    'b-dep'         => '/b-dep.js?ver=99.9.9',
     325                    'c-dep'         => '/c-static.js?ver=99.9.9',
     326                    'c-static-dep'  => '/c-static-dep.js?ver=99.9.9',
     327                    'd-static-dep'  => '/d-static-dep.js?ver=99.9.9',
     328                    'd-dynamic-dep' => '/d-dynamic-dep.js?ver=99.9.9',
     329                ),
     330            ),
     331            $actual,
     332            "Snapshot:\n" . var_export( $actual, true )
     333        );
     334
     335        // Dequeue the first half of the scripts.
     336        foreach ( array( 'a', 'b', 'c', 'd' ) as $id ) {
     337            if ( $use_global_function ) {
     338                wp_dequeue_script_module( $id );
     339            } else {
     340                wp_script_modules()->dequeue( $id );
    46341            }
    47342        }
    48343
    49         return $enqueued_script_modules;
    50     }
    51 
    52     /**
    53      * Gets the script modules listed in the import map.
    54      *
    55      * @return array Import map entry URLs, keyed by script module identifier.
    56      */
    57     public function get_import_map() {
    58         $import_map_markup = get_echo( array( $this->script_modules, 'print_import_map' ) );
    59         preg_match( '/<script type="importmap" id="wp-importmap">.*?(\{.*\}).*?<\/script>/s', $import_map_markup, $import_map_string );
    60         return json_decode( $import_map_string[1], true )['imports'];
    61     }
    62 
    63     /**
    64      * Gets a list of preloaded script modules.
    65      *
    66      * @return array Preloaded script module URLs, keyed by script module identifier.
    67      */
    68     public function get_preloaded_script_modules() {
    69         $preloaded_markup         = get_echo( array( $this->script_modules, 'print_script_module_preloads' ) );
    70         $p                        = new WP_HTML_Tag_Processor( $preloaded_markup );
    71         $preloaded_script_modules = array();
    72 
    73         while ( $p->next_tag( array( 'tag' => 'LINK' ) ) ) {
    74             if ( 'modulepreload' === $p->get_attribute( 'rel' ) ) {
    75                 $id                              = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) );
    76                 $preloaded_script_modules[ $id ] = $p->get_attribute( 'href' );
     344        $actual = array(
     345            'preload_links' => $this->get_preloaded_script_modules(),
     346            'script_tags'   => $this->get_enqueued_script_modules(),
     347            'import_map'    => $this->get_import_map(),
     348        );
     349        $this->assertSame(
     350            array(
     351                'preload_links' => array(),
     352                'script_tags'   => array(
     353                    'e' => array(
     354                        'url'           => '/e.js?ver=1.0.0',
     355                        'fetchpriority' => 'auto',
     356                    ),
     357                    'f' => array(
     358                        'url'           => '/f.js?ver=2.0.0',
     359                        'fetchpriority' => 'auto',
     360                    ),
     361                    'g' => array(
     362                        'url'           => '/g.js?ver=2.0.0',
     363                        'fetchpriority' => 'low',
     364                    ),
     365                    'h' => array(
     366                        'url'           => '/h.js?ver=3.0.0',
     367                        'fetchpriority' => 'high',
     368                    ),
     369                ),
     370                'import_map'    => array(),
     371            ),
     372            $actual,
     373            "Snapshot:\n" . var_export( $actual, true )
     374        );
     375
     376        // Unregister the remaining scripts.
     377        foreach ( array( 'e', 'f', 'g', 'h' ) as $id ) {
     378            if ( $use_global_function ) {
     379                wp_dequeue_script_module( $id );
     380            } else {
     381                wp_script_modules()->dequeue( $id );
    77382            }
    78383        }
    79384
    80         return $preloaded_script_modules;
     385        $actual = array(
     386            'preload_links' => $this->get_preloaded_script_modules(),
     387            'script_tags'   => $this->get_enqueued_script_modules(),
     388            'import_map'    => $this->get_import_map(),
     389        );
     390        $this->assertSame(
     391            array(
     392                'preload_links' => array(),
     393                'script_tags'   => array(),
     394                'import_map'    => array(),
     395            ),
     396            $actual,
     397            "Snapshot:\n" . var_export( $actual, true )
     398        );
     399    }
     400
     401    /**
     402     * Data provider.
     403     *
     404     * @return array<string, array{ use_global_function: bool, only_enqueue: bool }>
     405     */
     406    public function data_test_register_and_enqueue_script_module(): array {
     407        $data = array();
     408
     409        foreach ( array( true, false ) as $use_global_function ) {
     410            foreach ( array( true, false ) as $only_enqueue ) {
     411                $test_case = compact( 'use_global_function', 'only_enqueue' );
     412                $key_parts = array();
     413                foreach ( $test_case as $param_name => $param_value ) {
     414                    $key_parts[] = sprintf( '%s_%s', $param_name, json_encode( $param_value ) );
     415                }
     416                $data[ join( '_', $key_parts ) ] = $test_case;
     417            }
     418        }
     419
     420        return $data;
    81421    }
    82422
     
    86426     * @ticket 56313
    87427     *
    88      * @covers ::register()
    89      * @covers ::enqueue()
    90      * @covers ::print_enqueued_script_modules()
     428     * @covers WP_Script_Modules::register()
     429     * @covers WP_Script_Modules::enqueue()
     430     * @covers WP_Script_Modules::print_enqueued_script_modules()
     431     * @covers WP_Script_Modules::set_fetchpriority()
    91432     */
    92433    public function test_wp_enqueue_script_module() {
    93434        $this->script_modules->register( 'foo', '/foo.js' );
    94         $this->script_modules->register( 'bar', '/bar.js' );
     435        $this->script_modules->register( 'bar', '/bar.js', array(), false, array( 'fetchpriority' => 'high' ) );
     436        $this->script_modules->register( 'baz', '/baz.js' );
     437        $this->assertTrue( $this->script_modules->set_fetchpriority( 'baz', 'low' ) );
    95438        $this->script_modules->enqueue( 'foo' );
    96439        $this->script_modules->enqueue( 'bar' );
     440        $this->script_modules->enqueue( 'baz' );
    97441
    98442        $enqueued_script_modules = $this->get_enqueued_script_modules();
    99443
    100         $this->assertCount( 2, $enqueued_script_modules );
    101         $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo'] );
    102         $this->assertStringStartsWith( '/bar.js', $enqueued_script_modules['bar'] );
     444        $this->assertCount( 3, $enqueued_script_modules );
     445        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
     446        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
     447        $this->assertStringStartsWith( '/bar.js', $enqueued_script_modules['bar']['url'] );
     448        $this->assertSame( 'high', $enqueued_script_modules['bar']['fetchpriority'] );
     449        $this->assertStringStartsWith( '/baz.js', $enqueued_script_modules['baz']['url'] );
     450        $this->assertSame( 'low', $enqueued_script_modules['baz']['fetchpriority'] );
    103451    }
    104452
     
    108456    * @ticket 56313
    109457    *
    110     * @covers ::register()
    111     * @covers ::enqueue()
    112     * @covers ::dequeue()
    113     * @covers ::print_enqueued_script_modules()
     458    * @covers WP_Script_Modules::register()
     459    * @covers WP_Script_Modules::enqueue()
     460    * @covers WP_Script_Modules::dequeue()
     461    * @covers WP_Script_Modules::print_enqueued_script_modules()
    114462    */
    115463    public function test_wp_dequeue_script_module() {
     
    135483     * @ticket 60463
    136484     *
    137      * @covers ::register()
    138      * @covers ::enqueue()
    139      * @covers ::deregister()
    140      * @covers ::get_enqueued_script_modules()
     485     * @covers WP_Script_Modules::register()
     486     * @covers WP_Script_Modules::enqueue()
     487     * @covers WP_Script_Modules::deregister()
     488     * @covers WP_Script_Modules::get_enqueued_script_modules()
    141489     */
    142490    public function test_wp_deregister_script_module() {
     
    161509     * @ticket 60463
    162510     *
    163      * @covers ::deregister()
    164      * @covers ::get_enqueued_script_modules()
     511     * @covers WP_Script_Modules::deregister()
     512     * @covers WP_Script_Modules::get_enqueued_script_modules()
    165513     */
    166514    public function test_wp_deregister_unexistent_script_module() {
     
    179527     * @ticket 60463
    180528     *
    181      * @covers ::get_enqueued_script_modules()
    182      * @covers ::register()
    183      * @covers ::deregister()
    184      * @covers ::enqueue()
     529     * @covers WP_Script_Modules::get_enqueued_script_modules()
     530     * @covers WP_Script_Modules::register()
     531     * @covers WP_Script_Modules::deregister()
     532     * @covers WP_Script_Modules::enqueue()
    185533     */
    186534    public function test_wp_deregister_already_deregistered_script_module() {
     
    206554    * @ticket 56313
    207555    *
    208     * @covers ::register()
    209     * @covers ::enqueue()
    210     * @covers ::print_enqueued_script_modules()
     556    * @covers WP_Script_Modules::register()
     557    * @covers WP_Script_Modules::enqueue()
     558    * @covers WP_Script_Modules::print_enqueued_script_modules()
    211559    */
    212560    public function test_wp_enqueue_script_module_works_before_register() {
     
    218566
    219567        $this->assertCount( 1, $enqueued_script_modules );
    220         $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo'] );
     568        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
     569        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
    221570        $this->assertArrayNotHasKey( 'bar', $enqueued_script_modules );
    222571    }
     
    228577     * @ticket 56313
    229578     *
    230      * @covers ::register()
    231      * @covers ::enqueue()
    232      * @covers ::dequeue()
    233      * @covers ::print_enqueued_script_modules()
     579     * @covers WP_Script_Modules::register()
     580     * @covers WP_Script_Modules::enqueue()
     581     * @covers WP_Script_Modules::dequeue()
     582     * @covers WP_Script_Modules::print_enqueued_script_modules()
    234583     */
    235584    public function test_wp_dequeue_script_module_works_before_register() {
     
    253602     * @ticket 56313
    254603     *
    255      * @covers ::register()
    256      * @covers ::enqueue()
    257      * @covers ::print_import_map()
     604     * @covers WP_Script_Modules::register()
     605     * @covers WP_Script_Modules::enqueue()
     606     * @covers WP_Script_Modules::print_import_map()
    258607     */
    259608    public function test_wp_import_map_dependencies() {
     
    276625     * @ticket 56313
    277626     *
    278      * @covers ::register()
    279      * @covers ::enqueue()
    280      * @covers ::print_import_map()
     627     * @covers WP_Script_Modules::register()
     628     * @covers WP_Script_Modules::enqueue()
     629     * @covers WP_Script_Modules::print_import_map()
    281630     */
    282631    public function test_wp_import_map_no_duplicate_dependencies() {
     
    299648     * @ticket 56313
    300649     *
    301      * @covers ::register()
    302      * @covers ::enqueue()
    303      * @covers ::print_import_map()
     650     * @covers WP_Script_Modules::register()
     651     * @covers WP_Script_Modules::enqueue()
     652     * @covers WP_Script_Modules::print_import_map()
    304653     */
    305654    public function test_wp_import_map_recursive_dependencies() {
     
    350699     * @ticket 56313
    351700     *
    352      * @covers ::register()
    353      * @covers ::enqueue()
    354      * @covers ::print_import_map()
     701     * @covers WP_Script_Modules::register()
     702     * @covers WP_Script_Modules::enqueue()
     703     * @covers WP_Script_Modules::print_import_map()
    355704     */
    356705    public function test_wp_import_map_doesnt_print_if_no_dependencies() {
     
    369718     * @ticket 56313
    370719     *
    371      * @covers ::register()
    372      * @covers ::enqueue()
    373      * @covers ::print_script_module_preloads()
     720     * @covers WP_Script_Modules::register()
     721     * @covers WP_Script_Modules::enqueue()
     722     * @covers WP_Script_Modules::print_script_module_preloads()
    374723     */
    375724    public function test_wp_enqueue_preloaded_static_dependencies() {
     
    397746                    'import' => 'dynamic',
    398747                ),
    399             )
     748            ),
     749            false,
     750            array( 'fetchpriority' => 'high' )
    400751        );
    401752        $this->script_modules->register( 'dynamic-dep', '/dynamic-dep.js' );
     
    408759
    409760        $this->assertCount( 2, $preloaded_script_modules );
    410         $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep'] );
    411         $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep'] );
     761        $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
     762        $this->assertSame( 'high', $preloaded_script_modules['static-dep']['fetchpriority'] );
     763        $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] );
     764        $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] );
    412765        $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
    413766        $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
     
    420773     * @ticket 56313
    421774     *
    422      * @covers ::register()
    423      * @covers ::enqueue()
    424      * @covers ::print_script_module_preloads()
     775     * @covers WP_Script_Modules::register()
     776     * @covers WP_Script_Modules::enqueue()
     777     * @covers WP_Script_Modules::print_script_module_preloads()
    425778     */
    426779    public function test_wp_dont_preload_static_dependencies_of_dynamic_dependencies() {
     
    445798
    446799        $this->assertCount( 1, $preloaded_script_modules );
    447         $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep'] );
     800        $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
     801        $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] );
    448802        $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
    449803        $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
     
    456810     * @ticket 56313
    457811     *
    458      * @covers ::register()
    459      * @covers ::enqueue()
    460      * @covers ::print_script_module_preloads()
     812     * @covers WP_Script_Modules::register()
     813     * @covers WP_Script_Modules::enqueue()
     814     * @covers WP_Script_Modules::print_script_module_preloads()
    461815     */
    462816    public function test_wp_preloaded_dependencies_filter_enqueued_script_modules() {
     
    487841     * @ticket 56313
    488842     *
    489      * @covers ::register()
    490      * @covers ::enqueue()
    491      * @covers ::print_import_map()
     843     * @covers WP_Script_Modules::register()
     844     * @covers WP_Script_Modules::enqueue()
     845     * @covers WP_Script_Modules::print_import_map()
    492846     */
    493847    public function test_wp_enqueued_script_modules_with_dependants_add_import_map() {
     
    518872     * @ticket 56313
    519873     *
    520      * @covers ::get_src()
     874     * @covers WP_Script_Modules::get_src()
    521875     */
    522876    public function test_get_src() {
     
    587941     * @ticket 56313
    588942     *
    589      * @covers ::register()
    590      * @covers ::enqueue()
    591      * @covers ::print_enqueued_script_modules()
    592      * @covers ::print_import_map()
    593      * @covers ::print_script_module_preloads()
    594      * @covers ::get_version_query_string()
     943     * @covers WP_Script_Modules::register()
     944     * @covers WP_Script_Modules::enqueue()
     945     * @covers WP_Script_Modules::print_enqueued_script_modules()
     946     * @covers WP_Script_Modules::print_import_map()
     947     * @covers WP_Script_Modules::print_script_module_preloads()
     948     * @covers WP_Script_Modules::get_version_query_string()
    595949     */
    596950    public function test_version_is_propagated_correctly() {
     
    601955                'dep',
    602956            ),
    603             '1.0'
    604         );
    605         $this->script_modules->register( 'dep', '/dep.js', array(), '2.0' );
     957            '1.0',
     958            array( 'fetchpriority' => 'auto' )
     959        );
     960        $this->script_modules->register( 'dep', '/dep.js', array(), '2.0', array( 'fetchpriority' => 'high' ) );
    606961        $this->script_modules->enqueue( 'foo' );
    607962
    608963        $enqueued_script_modules = $this->get_enqueued_script_modules();
    609         $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo'] );
     964        $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo']['url'] );
     965        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
    610966
    611967        $import_map = $this->get_import_map();
     
    613969
    614970        $preloaded_script_modules = $this->get_preloaded_script_modules();
    615         $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep'] );
     971        $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] );
     972        $this->assertSame( 'high', $preloaded_script_modules['dep']['fetchpriority'] );
    616973    }
    617974
     
    622979     * @ticket 56313
    623980     *
    624      * @covers ::enqueue()
    625      * @covers ::print_enqueued_script_modules()
     981     * @covers WP_Script_Modules::enqueue()
     982     * @covers WP_Script_Modules::print_enqueued_script_modules()
    626983     */
    627984    public function test_wp_enqueue_script_module_doesnt_register_without_a_valid_src() {
     
    640997     * @ticket 56313
    641998     *
    642      * @covers ::enqueue()
    643      * @covers ::print_enqueued_script_modules()
     999     * @covers WP_Script_Modules::enqueue()
     1000     * @covers WP_Script_Modules::print_enqueued_script_modules()
    6441001     */
    6451002    public function test_wp_enqueue_script_module_registers_with_valid_src() {
     
    6491006
    6501007        $this->assertCount( 1, $enqueued_script_modules );
    651         $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo'] );
     1008        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
     1009        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
    6521010    }
    6531011
     
    6581016     * @ticket 56313
    6591017     *
    660      * @covers ::enqueue()
    661      * @covers ::print_enqueued_script_modules()
     1018     * @covers WP_Script_Modules::enqueue()
     1019     * @covers WP_Script_Modules::print_enqueued_script_modules()
    6621020     */
    6631021    public function test_wp_enqueue_script_module_registers_with_valid_src_the_second_time() {
     
    6741032
    6751033        $this->assertCount( 1, $enqueued_script_modules );
    676         $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo'] );
     1034        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
     1035        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
    6771036    }
    6781037
     
    6831042     * @ticket 56313
    6841043     *
    685      * @covers ::register()
    686      * @covers ::enqueue()
    687      * @covers ::print_enqueued_script_modules()
    688      * @covers ::print_import_map()
     1044     * @covers WP_Script_Modules::register()
     1045     * @covers WP_Script_Modules::enqueue()
     1046     * @covers WP_Script_Modules::print_enqueued_script_modules()
     1047     * @covers WP_Script_Modules::print_import_map()
    6891048     */
    6901049    public function test_wp_enqueue_script_module_registers_all_params() {
    691         $this->script_modules->enqueue( 'foo', '/foo.js', array( 'dep' ), '1.0' );
     1050        $this->script_modules->enqueue( 'foo', '/foo.js', array( 'dep' ), '1.0', array( 'fetchpriority' => 'low' ) );
    6921051        $this->script_modules->register( 'dep', '/dep.js' );
    6931052
     
    6961055
    6971056        $this->assertCount( 1, $enqueued_script_modules );
    698         $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo'] );
     1057        $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo']['url'] );
     1058        $this->assertSame( 'low', $enqueued_script_modules['foo']['fetchpriority'] );
    6991059        $this->assertCount( 1, $import_map );
    7001060        $this->assertStringStartsWith( '/dep.js', $import_map['dep'] );
     
    8531213
    8541214            // Non UTF-8
    855             'Solidus'                                => array( '/', '/', 'iso-8859-1' ),
    856             'Less than'                              => array( '<', '\u003C', 'iso-8859-1' ),
    857             'Greater than'                           => array( '>', '\u003E', 'iso-8859-1' ),
    858             'Ampersand'                              => array( '&', '&', 'iso-8859-1' ),
    859             'Newline'                                => array( "\n", "\\n", 'iso-8859-1' ),
    860             'Tab'                                    => array( "\t", "\\t", 'iso-8859-1' ),
    861             'Form feed'                              => array( "\f", "\\f", 'iso-8859-1' ),
    862             'Carriage return'                        => array( "\r", "\\r", 'iso-8859-1' ),
    863             'Line separator'                         => array( "\u{2028}", "\u2028", 'iso-8859-1' ),
    864             'Paragraph separator'                    => array( "\u{2029}", "\u2029", 'iso-8859-1' ),
     1215            'Solidus non-utf8'                       => array( '/', '/', 'iso-8859-1' ),
     1216            'Less than non-utf8'                     => array( '<', '\u003C', 'iso-8859-1' ),
     1217            'Greater than non-utf8'                  => array( '>', '\u003E', 'iso-8859-1' ),
     1218            'Ampersand non-utf8'                     => array( '&', '&', 'iso-8859-1' ),
     1219            'Newline non-utf8'                       => array( "\n", "\\n", 'iso-8859-1' ),
     1220            'Tab non-utf8'                           => array( "\t", "\\t", 'iso-8859-1' ),
     1221            'Form feed non-utf8'                     => array( "\f", "\\f", 'iso-8859-1' ),
     1222            'Carriage return non-utf8'               => array( "\r", "\\r", 'iso-8859-1' ),
     1223            'Line separator non-utf8'                => array( "\u{2028}", "\u2028", 'iso-8859-1' ),
     1224            'Paragraph separator non-utf8'           => array( "\u{2029}", "\u2029", 'iso-8859-1' ),
    8651225            /*
    8661226             * The following is the Flag of England emoji
    8671227             * PHP: "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}"
    8681228             */
    869             'Flag of england'                        => array( '🏴󠁧󠁢󠁥󠁮󠁧󠁿', "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f", 'iso-8859-1' ),
    870             'Malicious script closer'                => array( '</script>', '\u003C/script\u003E', 'iso-8859-1' ),
    871             'Entity-encoded malicious script closer' => array( '&lt;/script&gt;', '&lt;/script&gt;', 'iso-8859-1' ),
     1229            'Flag of england non-utf8'               => array( '🏴󠁧󠁢󠁥󠁮󠁧󠁿', "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f", 'iso-8859-1' ),
     1230            'Malicious script closer non-utf8'       => array( '</script>', '\u003C/script\u003E', 'iso-8859-1' ),
     1231            'Entity-encoded malicious script closer non-utf8' => array( '&lt;/script&gt;', '&lt;/script&gt;', 'iso-8859-1' ),
    8721232
    8731233        );
     
    8951255
    8961256    /**
     1257     * Data provider for test_fetchpriority_values.
     1258     *
     1259     * @return array<string, array{fetchpriority: string}>
     1260     */
     1261    public function data_provider_fetchpriority_values(): array {
     1262        return array(
     1263            'auto' => array( 'fetchpriority' => 'auto' ),
     1264            'low'  => array( 'fetchpriority' => 'low' ),
     1265            'high' => array( 'fetchpriority' => 'high' ),
     1266        );
     1267    }
     1268
     1269    /**
     1270     * Tests that valid fetchpriority values are correctly added to the registered module.
     1271     *
     1272     * @ticket 61734
     1273     *
     1274     * @covers WP_Script_Modules::register
     1275     * @covers WP_Script_Modules::set_fetchpriority
     1276     *
     1277     * @dataProvider data_provider_fetchpriority_values
     1278     *
     1279     * @param string $fetchpriority The fetchpriority value to test.
     1280     */
     1281    public function test_fetchpriority_values( string $fetchpriority ) {
     1282        $this->script_modules->register( 'test-script', '/test-script.js', array(), null, array( 'fetchpriority' => $fetchpriority ) );
     1283        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1284        $this->assertSame( $fetchpriority, $registered_modules['test-script']['fetchpriority'] );
     1285
     1286        $this->script_modules->register( 'test-script-2', '/test-script-2.js' );
     1287        $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', $fetchpriority ) );
     1288        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1289        $this->assertSame( $fetchpriority, $registered_modules['test-script-2']['fetchpriority'] );
     1290
     1291        $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', '' ) );
     1292        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1293        $this->assertSame( 'auto', $registered_modules['test-script-2']['fetchpriority'] );
     1294    }
     1295
     1296    /**
     1297     * Tests that a script module with an invalid fetchpriority value gets a value of auto.
     1298     *
     1299     * @ticket 61734
     1300     *
     1301     * @covers WP_Script_Modules::register
     1302     * @expectedIncorrectUsage WP_Script_Modules::register
     1303     */
     1304    public function test_register_script_module_having_fetchpriority_with_invalid_value() {
     1305        $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => 'silly' ) );
     1306        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1307        $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] );
     1308        $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong );
     1309        $this->assertStringContainsString( 'Invalid fetchpriority `silly`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] );
     1310    }
     1311
     1312    /**
     1313     * Tests that a script module with an invalid fetchpriority value type gets a value of auto.
     1314     *
     1315     * @ticket 61734
     1316     *
     1317     * @covers WP_Script_Modules::register
     1318     * @expectedIncorrectUsage WP_Script_Modules::register
     1319     */
     1320    public function test_register_script_module_having_fetchpriority_with_invalid_value_type() {
     1321        $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => array( 'WHY AM I NOT A STRING???' ) ) );
     1322        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1323        $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] );
     1324        $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong );
     1325        $this->assertStringContainsString( 'Invalid fetchpriority `array`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] );
     1326    }
     1327
     1328    /**
     1329     * Tests that a setting the fetchpriority for script module with an invalid value is ignored so that it remains auto.
     1330     *
     1331     * @ticket 61734
     1332     *
     1333     * @covers WP_Script_Modules::register
     1334     * @covers WP_Script_Modules::set_fetchpriority
     1335     * @expectedIncorrectUsage WP_Script_Modules::set_fetchpriority
     1336     */
     1337    public function test_set_fetchpriority_with_invalid_value() {
     1338        $this->script_modules->register( 'foo', '/foo.js' );
     1339        $this->script_modules->set_fetchpriority( 'foo', 'silly' );
     1340        $registered_modules = $this->get_registered_script_modules( $this->script_modules );
     1341        $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] );
     1342    }
     1343
     1344    /**
     1345     * Gets registered script modules.
     1346     *
     1347     * @param WP_Script_Modules $script_modules
     1348     * @return array<string, array> Registered modules.
     1349     */
     1350    private function get_registered_script_modules( WP_Script_Modules $script_modules ): array {
     1351        $reflection_class    = new ReflectionClass( $script_modules );
     1352        $registered_property = $reflection_class->getProperty( 'registered' );
     1353        $registered_property->setAccessible( true );
     1354        return $registered_property->getValue( $script_modules );
     1355    }
     1356
     1357    /**
    8971358     * Data provider.
    8981359     *
Note: See TracChangeset for help on using the changeset viewer.