diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 5b5524494d630..c574e5b2ef93a 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -175,11 +175,21 @@ function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { $block_version = isset( $metadata['version'] ) ? $metadata['version'] : false; $module_version = isset( $module_asset['version'] ) ? $module_asset['version'] : $block_version; + // Blocks using the Interactivity API are server-side rendered, so they are by design not in the critical rendering path and should be deprioritized. + $args = array(); + if ( + ( isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity'] ) || + ( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] ) + ) { + $args['fetchpriority'] = 'low'; + } + wp_register_script_module( $module_id, $module_uri, $module_dependencies, - $module_version + $module_version, + $args ); return $module_id; diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 056c061368f8b..08d08a5d1a65e 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -46,6 +46,7 @@ class WP_Script_Modules { * identifier has already been registered. * * @since 6.5.0 + * @since 6.9.0 Added the $args parameter. * * @param string $id The identifier of the script module. Should be unique. It will be used in the * final import map. @@ -71,13 +72,18 @@ class WP_Script_Modules { * It is added to the URL as a query string for cache busting purposes. If $version * is set to false, the version number is the currently installed WordPress version. * If $version is set to null, no version is added. + * @param array $args { + * Optional. An array of additional args. Default empty array. + * + * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. + * } */ - public function register( string $id, string $src, array $deps = array(), $version = false ) { + public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) { if ( ! isset( $this->registered[ $id ] ) ) { $dependencies = array(); foreach ( $deps as $dependency ) { if ( is_array( $dependency ) ) { - if ( ! isset( $dependency['id'] ) ) { + if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) { _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' ); continue; } @@ -95,13 +101,76 @@ public function register( string $id, string $src, array $deps = array(), $versi } } + $fetchpriority = 'auto'; + if ( isset( $args['fetchpriority'] ) ) { + if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) { + $fetchpriority = $args['fetchpriority']; + } else { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: $fetchpriority, 2: $id */ + __( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ), + is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ), + $id + ), + '6.9.0' + ); + } + } + $this->registered[ $id ] = array( - 'src' => $src, - 'version' => $version, - 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), - 'dependencies' => $dependencies, + 'src' => $src, + 'version' => $version, + 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), + 'dependencies' => $dependencies, + 'fetchpriority' => $fetchpriority, + ); + } + } + + /** + * Checks if the provided fetchpriority is valid. + * + * @since 6.9.0 + * + * @param string|mixed $priority Fetch priority. + * @return bool Whether valid fetchpriority. + */ + private function is_valid_fetchpriority( $priority ): bool { + return in_array( $priority, array( 'auto', 'low', 'high' ), true ); + } + + /** + * Sets the fetch priority for a script module. + * + * @since 6.9.0 + * + * @param string $id Script module identifier. + * @param 'auto'|'low'|'high' $priority Fetch priority for the script module. + * @return bool Whether setting the fetchpriority was successful. + */ + public function set_fetchpriority( string $id, string $priority ): bool { + if ( ! isset( $this->registered[ $id ] ) ) { + return false; + } + + if ( '' === $priority ) { + $priority = 'auto'; + } + + if ( ! $this->is_valid_fetchpriority( $priority ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Invalid fetchpriority. */ + sprintf( __( 'Invalid fetchpriority: %s' ), $priority ), + '6.9.0' ); + return false; } + + $this->registered[ $id ]['fetchpriority'] = $priority; + return true; } /** @@ -111,6 +180,7 @@ public function register( string $id, string $src, array $deps = array(), $versi * will be registered. * * @since 6.5.0 + * @since 6.9.0 Added the $args parameter. * * @param string $id The identifier of the script module. Should be unique. It will be used in the * final import map. @@ -136,12 +206,17 @@ public function register( string $id, string $src, array $deps = array(), $versi * It is added to the URL as a query string for cache busting purposes. If $version * is set to false, the version number is the currently installed WordPress version. * If $version is set to null, no version is added. + * @param array $args { + * Optional. An array of additional args. Default empty array. + * + * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. + * } */ - public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) { + public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) { if ( isset( $this->registered[ $id ] ) ) { $this->registered[ $id ]['enqueue'] = true; } elseif ( $src ) { - $this->register( $id, $src, $deps, $version ); + $this->register( $id, $src, $deps, $version, $args ); $this->registered[ $id ]['enqueue'] = true; } else { $this->enqueued_before_registered[ $id ] = true; @@ -208,13 +283,15 @@ public function add_hooks() { */ public function print_enqueued_script_modules() { foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) { - wp_print_script_tag( - array( - 'type' => 'module', - 'src' => $this->get_src( $id ), - 'id' => $id . '-js-module', - ) + $args = array( + 'type' => 'module', + 'src' => $this->get_src( $id ), + 'id' => $id . '-js-module', ); + if ( 'auto' !== $script_module['fetchpriority'] ) { + $args['fetchpriority'] = $script_module['fetchpriority']; + } + wp_print_script_tag( $args ); } } @@ -231,9 +308,10 @@ public function print_script_module_preloads() { // Don't preload if it's marked for enqueue. if ( true !== $script_module['enqueue'] ) { echo sprintf( - '', + '', esc_url( $this->get_src( $id ) ), - esc_attr( $id . '-js-modulepreload' ) + esc_attr( $id . '-js-modulepreload' ), + 'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : '' ); } } @@ -278,7 +356,7 @@ private function get_import_map(): array { * * @since 6.5.0 * - * @return array[] Script modules marked for enqueue, keyed by script module identifier. + * @return array Script modules marked for enqueue, keyed by script module identifier. */ private function get_marked_for_enqueue(): array { $enqueued = array(); diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index b1e4c76c73d43..328057aac0dd0 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -425,6 +425,9 @@ public function do_item( $handle, $group = false ) { if ( $intended_strategy ) { $attr['data-wp-strategy'] = $intended_strategy; } + if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) { + $attr['fetchpriority'] = $obj->extra['fetchpriority']; + } $tag = $translations . $ie_conditional_prefix . $before_script; $tag .= wp_get_script_tag( $attr ); $tag .= $after_script . $ie_conditional_suffix; @@ -831,6 +834,35 @@ public function add_data( $handle, $key, $value ) { ); return false; } + } elseif ( 'fetchpriority' === $key ) { + if ( empty( $value ) ) { + $value = 'auto'; + } + if ( ! $this->is_valid_fetchpriority( $value ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: $fetchpriority, 2: $handle */ + __( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ), + is_string( $value ) ? $value : gettype( $value ), + $handle + ), + '6.9.0' + ); + return false; + } elseif ( ! $this->registered[ $handle ]->src ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: $fetchpriority, 2: $handle */ + __( 'Cannot supply a fetchpriority `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ), + is_string( $value ) ? $value : gettype( $value ), + $handle + ), + '6.9.0' + ); + return false; + } } return parent::add_data( $handle, $key, $value ); } @@ -869,10 +901,10 @@ private function get_dependents( $handle ) { * * @since 6.3.0 * - * @param string $strategy The strategy to check. + * @param string|mixed $strategy The strategy to check. * @return bool True if $strategy is one of the delayed strategies, otherwise false. */ - private function is_delayed_strategy( $strategy ) { + private function is_delayed_strategy( $strategy ): bool { return in_array( $strategy, $this->delayed_strategies, @@ -880,6 +912,18 @@ private function is_delayed_strategy( $strategy ) { ); } + /** + * Checks if the provided fetchpriority is valid. + * + * @since 6.9.0 + * + * @param string|mixed $priority Fetch priority. + * @return bool Whether valid fetchpriority. + */ + private function is_valid_fetchpriority( $priority ): bool { + return in_array( $priority, array( 'auto', 'low', 'high' ), true ); + } + /** * Gets the best eligible loading strategy for a script. * diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 1be1822aa7c3d..69a1d96b1d198 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -158,6 +158,7 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * @since 2.1.0 * @since 4.3.0 A return value was added. * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. + * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * * @param string $handle Name of the script. Should be unique. * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. @@ -171,8 +172,9 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * Optional. An array of additional script loading strategies. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. * } * @return bool Whether the script has been registered. True on success, false on failure. */ @@ -193,6 +195,9 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args if ( ! empty( $args['strategy'] ) ) { $wp_scripts->add_data( $handle, 'strategy', $args['strategy'] ); } + if ( ! empty( $args['fetchpriority'] ) ) { + $wp_scripts->add_data( $handle, 'fetchpriority', $args['fetchpriority'] ); + } return $registered; } @@ -339,6 +344,7 @@ function wp_deregister_script( $handle ) { * * @since 2.1.0 * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. + * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * * @param string $handle Name of the script. Should be unique. * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. @@ -352,8 +358,9 @@ function wp_deregister_script( $handle ) { * Optional. An array of additional script loading strategies. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. * } */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { @@ -378,6 +385,9 @@ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $ if ( ! empty( $args['strategy'] ) ) { $wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] ); } + if ( ! empty( $args['fetchpriority'] ) ) { + $wp_scripts->add_data( $_handle[0], 'fetchpriority', $args['fetchpriority'] ); + } } $wp_scripts->enqueue( $handle ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index def31178880d5..7b9414fa12ed1 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1047,7 +1047,10 @@ function wp_default_scripts( $scripts ) { did_action( 'init' ) && $scripts->localize( 'wp-plupload', 'pluploadL10n', $uploader_l10n ); $scripts->add( 'comment-reply', "/wp-includes/js/comment-reply$suffix.js", array(), false, 1 ); - did_action( 'init' ) && $scripts->add_data( 'comment-reply', 'strategy', 'async' ); + if ( did_action( 'init' ) ) { + $scripts->add_data( 'comment-reply', 'strategy', 'async' ); + $scripts->add_data( 'comment-reply', 'fetchpriority', 'low' ); // In Chrome this is automatically low due to the async strategy, but in Firefox and Safari the priority is normal/medium. + } $scripts->add( 'json2', "/wp-includes/js/json2$suffix.js", array(), '2015-05-03' ); did_action( 'init' ) && $scripts->add_data( 'json2', 'conditional', 'lt IE 8' ); diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 31ee51b2a749f..0d284833bea09 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -35,6 +35,7 @@ function wp_script_modules(): WP_Script_Modules { * identifier has already been registered. * * @since 6.5.0 + * @since 6.9.0 Added the $args parameter. * * @param string $id The identifier of the script module. Should be unique. It will be used in the * final import map. @@ -60,9 +61,14 @@ function wp_script_modules(): WP_Script_Modules { * It is added to the URL as a query string for cache busting purposes. If $version * is set to false, the version number is the currently installed WordPress version. * If $version is set to null, no version is added. + * @param array $args { + * Optional. An array of additional args. Default empty array. + * + * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. + * } */ -function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) { - wp_script_modules()->register( $id, $src, $deps, $version ); +function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) { + wp_script_modules()->register( $id, $src, $deps, $version, $args ); } /** @@ -72,6 +78,7 @@ function wp_register_script_module( string $id, string $src, array $deps = array * will be registered. * * @since 6.5.0 + * @since 6.9.0 Added the $args parameter. * * @param string $id The identifier of the script module. Should be unique. It will be used in the * final import map. @@ -97,9 +104,14 @@ function wp_register_script_module( string $id, string $src, array $deps = array * It is added to the URL as a query string for cache busting purposes. If $version * is set to false, the version number is the currently installed WordPress version. * If $version is set to null, no version is added. + * @param array $args { + * Optional. An array of additional args. Default empty array. + * + * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. + * } */ -function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) { - wp_script_modules()->enqueue( $id, $src, $deps, $version ); +function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) { + wp_script_modules()->enqueue( $id, $src, $deps, $version, $args ); } /** @@ -169,7 +181,13 @@ function wp_default_script_modules() { break; } + // The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules should be loaded with low fetch priority since they should not be needed in the critical rendering path. + $args = array(); + if ( str_starts_with( $script_module_id, '@wordpress/interactivity' ) || str_starts_with( $script_module_id, '@wordpress/block-library' ) ) { + $args['fetchpriority'] = 'low'; + } + $path = includes_url( "js/dist/script-modules/{$file_name}" ); - wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] ); + wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'], $args ); } } diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 2cd51aad6ab70..18a41cb25a6c8 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -1127,27 +1127,160 @@ public function test_loading_strategy_with_all_defer_dependencies() { /** * Tests that dependents that are async but attached to a deferred main script, print with defer as opposed to async. * + * Also tests that fetchpriority attributes are added as expected. + * * @ticket 12009 + * @ticket 61734 * * @covers WP_Scripts::do_item * @covers WP_Scripts::get_eligible_loading_strategy + * @covers ::wp_register_script * @covers ::wp_enqueue_script */ public function test_defer_with_async_dependent() { // case with one async dependent. - wp_enqueue_script( 'main-script-d4', '/main-script-d4.js', array(), null, array( 'strategy' => 'defer' ) ); - wp_enqueue_script( 'dependent-script-d4-1', '/dependent-script-d4-1.js', array( 'main-script-d4' ), null, array( 'strategy' => 'defer' ) ); - wp_enqueue_script( 'dependent-script-d4-2', '/dependent-script-d4-2.js', array( 'dependent-script-d4-1' ), null, array( 'strategy' => 'async' ) ); - wp_enqueue_script( 'dependent-script-d4-3', '/dependent-script-d4-3.js', array( 'dependent-script-d4-2' ), null, array( 'strategy' => 'defer' ) ); + wp_register_script( 'main-script-d4', '/main-script-d4.js', array(), null, array( 'strategy' => 'defer' ) ); + wp_enqueue_script( + 'dependent-script-d4-1', + '/dependent-script-d4-1.js', + array( 'main-script-d4' ), + null, + array( + 'strategy' => 'defer', + 'fetchpriority' => 'auto', + ) + ); + wp_enqueue_script( + 'dependent-script-d4-2', + '/dependent-script-d4-2.js', + array( 'dependent-script-d4-1' ), + null, + array( + 'strategy' => 'async', + 'fetchpriority' => 'low', + ) + ); + wp_enqueue_script( + 'dependent-script-d4-3', + '/dependent-script-d4-3.js', + array( 'dependent-script-d4-2' ), + null, + array( + 'strategy' => 'defer', + 'fetchpriority' => 'high', + ) + ); $output = get_echo( 'wp_print_scripts' ); $expected = "\n"; $expected .= "\n"; - $expected .= "\n"; - $expected .= "\n"; + $expected .= "\n"; + $expected .= "\n"; $this->assertEqualHTML( $expected, $output, '', 'Scripts registered as defer but that have dependents that are async are expected to have said dependents deferred.' ); } + /** + * Data provider for test_fetchpriority_values. + * + * @return array + */ + public static function data_provider_fetchpriority_values(): array { + return array( + 'auto' => array( 'fetchpriority' => 'auto' ), + 'low' => array( 'fetchpriority' => 'low' ), + 'high' => array( 'fetchpriority' => 'high' ), + ); + } + + /** + * Tests that valid fetchpriority values are correctly added to script data. + * + * @ticket 61734 + * + * @covers ::wp_register_script + * @covers WP_Scripts::add_data + * @covers ::wp_script_add_data + * + * @dataProvider data_provider_fetchpriority_values + * + * @param string $fetchpriority The fetchpriority value to test. + */ + public function test_fetchpriority_values( string $fetchpriority ) { + wp_register_script( 'test-script', '/test-script.js', array(), null, array( 'fetchpriority' => $fetchpriority ) ); + $this->assertArrayHasKey( 'fetchpriority', wp_scripts()->registered['test-script']->extra ); + $this->assertSame( $fetchpriority, wp_scripts()->registered['test-script']->extra['fetchpriority'] ); + + wp_register_script( 'test-script-2', '/test-script-2.js' ); + $this->assertTrue( wp_script_add_data( 'test-script-2', 'fetchpriority', $fetchpriority ) ); + $this->assertArrayHasKey( 'fetchpriority', wp_scripts()->registered['test-script-2']->extra ); + $this->assertSame( $fetchpriority, wp_scripts()->registered['test-script-2']->extra['fetchpriority'] ); + } + + /** + * Tests that an empty fetchpriority is treated the same as auto. + * + * @ticket 61734 + * + * @covers ::wp_register_script + * @covers WP_Scripts::add_data + */ + public function test_empty_fetchpriority_value() { + wp_register_script( 'unset', '/joke.js', array(), null, array( 'fetchpriority' => 'low' ) ); + $this->assertSame( 'low', wp_scripts()->registered['unset']->extra['fetchpriority'] ); + $this->assertTrue( wp_script_add_data( 'unset', 'fetchpriority', null ) ); + $this->assertSame( 'auto', wp_scripts()->registered['unset']->extra['fetchpriority'] ); + } + + /** + * Tests that an invalid fetchpriority causes a _doing_it_wrong() warning. + * + * @ticket 61734 + * + * @covers ::wp_register_script + * @covers WP_Scripts::add_data + * + * @expectedIncorrectUsage WP_Scripts::add_data + */ + public function test_invalid_fetchpriority_value() { + wp_register_script( 'joke', '/joke.js', array(), null, array( 'fetchpriority' => 'silly' ) ); + $this->assertArrayNotHasKey( 'fetchpriority', wp_scripts()->registered['joke']->extra ); + $this->assertArrayHasKey( 'WP_Scripts::add_data', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `silly`', $this->caught_doing_it_wrong['WP_Scripts::add_data'] ); + } + + /** + * Tests that an invalid fetchpriority causes a _doing_it_wrong() warning. + * + * @ticket 61734 + * + * @covers ::wp_register_script + * @covers WP_Scripts::add_data + * + * @expectedIncorrectUsage WP_Scripts::add_data + */ + public function test_invalid_fetchpriority_value_type() { + wp_register_script( 'bad', '/bad.js' ); + $this->assertFalse( wp_script_add_data( 'bad', 'fetchpriority', array( 'THIS IS SO WRONG!!!' ) ) ); + $this->assertArrayNotHasKey( 'fetchpriority', wp_scripts()->registered['bad']->extra ); + $this->assertArrayHasKey( 'WP_Scripts::add_data', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `array`', $this->caught_doing_it_wrong['WP_Scripts::add_data'] ); + } + + /** + * Tests that adding fetchpriority causes a _doing_it_wrong() warning on a script alias. + * + * @ticket 61734 + * + * @covers ::wp_register_script + * @covers WP_Scripts::add_data + * + * @expectedIncorrectUsage WP_Scripts::add_data + */ + public function test_invalid_fetchpriority_on_alias() { + wp_register_script( 'alias', false, array(), null, array( 'fetchpriority' => 'low' ) ); + $this->assertArrayNotHasKey( 'fetchpriority', wp_scripts()->registered['alias']->extra ); + } + /** * Tests that scripts registered as defer become blocking when their dependents chain are all blocking. * diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 85f9599f0dac3..aa2b5b7806844 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -8,11 +8,19 @@ * @since 6.5.0 * * @group script-modules - * - * @coversDefaultClass WP_Script_Modules */ class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase { + /** + * @var WP_Script_Modules + */ + protected $original_script_modules; + + /** + * @var string + */ + protected $original_wp_version; + /** * Instance of WP_Script_Modules. * @@ -24,9 +32,22 @@ class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase { * Set up. */ public function set_up() { + global $wp_script_modules, $wp_version; parent::set_up(); - // Set up the WP_Script_Modules instance. - $this->script_modules = new WP_Script_Modules(); + $this->original_script_modules = $wp_script_modules; + $this->original_wp_version = $wp_version; + $wp_script_modules = null; + $this->script_modules = wp_script_modules(); + } + + /** + * Tear down. + */ + public function tear_down() { + global $wp_script_modules, $wp_version; + parent::tear_down(); + $wp_script_modules = $this->original_script_modules; + $wp_version = $this->original_wp_version; } /** @@ -34,30 +55,44 @@ public function set_up() { * * @return array Enqueued script module URLs, keyed by script module identifier. */ - public function get_enqueued_script_modules() { - $script_modules_markup = get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ); - $p = new WP_HTML_Tag_Processor( $script_modules_markup ); - $enqueued_script_modules = array(); + public function get_enqueued_script_modules(): array { + $modules = array(); + $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ) ); while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) { - if ( 'module' === $p->get_attribute( 'type' ) ) { - $id = preg_replace( '/-js-module$/', '', $p->get_attribute( 'id' ) ); - $enqueued_script_modules[ $id ] = $p->get_attribute( 'src' ); - } + $this->assertSame( 'module', $p->get_attribute( 'type' ) ); + $this->assertIsString( $p->get_attribute( 'id' ) ); + $this->assertIsString( $p->get_attribute( 'src' ) ); + $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) ); + + $id = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) ); + $fetchpriority = $p->get_attribute( 'fetchpriority' ); + $modules[ $id ] = array( + 'url' => $p->get_attribute( 'src' ), + 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto', + ); } - return $enqueued_script_modules; + return $modules; } /** * Gets the script modules listed in the import map. * - * @return array Import map entry URLs, keyed by script module identifier. + * @return array Import map entry URLs, keyed by script module identifier. */ - public function get_import_map() { - $import_map_markup = get_echo( array( $this->script_modules, 'print_import_map' ) ); - preg_match( '/', '\u003C/script\u003E', 'iso-8859-1' ), - 'Entity-encoded malicious script closer' => array( '</script>', '</script>', 'iso-8859-1' ), + 'Flag of england non-utf8' => array( '🏴󠁧󠁢󠁥󠁮󠁧󠁿', "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f", 'iso-8859-1' ), + 'Malicious script closer non-utf8' => array( '', '\u003C/script\u003E', 'iso-8859-1' ), + 'Entity-encoded malicious script closer non-utf8' => array( '</script>', '</script>', 'iso-8859-1' ), ); } @@ -893,6 +1253,107 @@ function ( $_ ) use ( $data ) { $this->assertSame( '', $actual ); } + /** + * Data provider for test_fetchpriority_values. + * + * @return array + */ + public static function data_provider_fetchpriority_values(): array { + return array( + 'auto' => array( 'fetchpriority' => 'auto' ), + 'low' => array( 'fetchpriority' => 'low' ), + 'high' => array( 'fetchpriority' => 'high' ), + ); + } + + /** + * Tests that valid fetchpriority values are correctly added to the registered module. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @covers WP_Script_Modules::set_fetchpriority + * + * @dataProvider data_provider_fetchpriority_values + * + * @param string $fetchpriority The fetchpriority value to test. + */ + public function test_fetchpriority_values( string $fetchpriority ) { + $this->script_modules->register( 'test-script', '/test-script.js', array(), null, array( 'fetchpriority' => $fetchpriority ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( $fetchpriority, $registered_modules['test-script']['fetchpriority'] ); + + $this->script_modules->register( 'test-script-2', '/test-script-2.js' ); + $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', $fetchpriority ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( $fetchpriority, $registered_modules['test-script-2']['fetchpriority'] ); + + $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', '' ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['test-script-2']['fetchpriority'] ); + } + + /** + * Tests that a script module with an invalid fetchpriority value gets a value of auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @expectedIncorrectUsage WP_Script_Modules::register + */ + public function test_register_script_module_having_fetchpriority_with_invalid_value() { + $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => 'silly' ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `silly`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] ); + } + + /** + * Tests that a script module with an invalid fetchpriority value type gets a value of auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @expectedIncorrectUsage WP_Script_Modules::register + */ + public function test_register_script_module_having_fetchpriority_with_invalid_value_type() { + $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => array( 'WHY AM I NOT A STRING???' ) ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `array`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] ); + } + + /** + * Tests that a setting the fetchpriority for script module with an invalid value is ignored so that it remains auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @covers WP_Script_Modules::set_fetchpriority + * @expectedIncorrectUsage WP_Script_Modules::set_fetchpriority + */ + public function test_set_fetchpriority_with_invalid_value() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->set_fetchpriority( 'foo', 'silly' ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + } + + /** + * Gets registered script modules. + * + * @param WP_Script_Modules $script_modules + * @return array Registered modules. + */ + private function get_registered_script_modules( WP_Script_Modules $script_modules ): array { + $reflection_class = new ReflectionClass( $script_modules ); + $registered_property = $reflection_class->getProperty( 'registered' ); + $registered_property->setAccessible( true ); + return $registered_property->getValue( $script_modules ); + } + /** * Data provider. *