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.
*