From 613b0e7dc91f9f94c640717df01b636d0cfea539 Mon Sep 17 00:00:00 2001
From: Joe McGill
Date: Mon, 26 Jun 2023 13:40:31 +0000
Subject: [PATCH] Script Loader: Add support for HTML 5 "async" and "defer"
attributes.
This allows developers to register scripts with an intended loading strategy by changing the `$in_footer` parameter of `wp_register_script` and `wp_enqueue_script` to an array that accepts both an `in_footer` and `strategy` argument. If present, the loading strategy attribute will be added to the script tag when that script is printed to the page as long as it is not a dependency of any blocking scripts, including any inline scripts attached to the script or any of its dependents.
Props 10upsimon, thekt12, westonruter, costdev, flixos90, spacedmonkey, adamsilverstein, azaozz, mukeshpanchal27, mor10, scep, wpnook, vanaf1979, Otto42.
Fixes #12009.
git-svn-id: https://develop.svn.wordpress.org/trunk@56033 602fd350-edb4-49c9-b593-d223f7449a82
---
.jshintrc | 4 +-
phpcs.xml.dist | 2 +
src/wp-includes/class-wp-scripts.php | 345 +++-
src/wp-includes/functions.wp-scripts.php | 48 +-
tests/phpunit/tests/dependencies/scripts.php | 1687 ++++++++++++++++--
5 files changed, 1943 insertions(+), 143 deletions(-)
diff --git a/.jshintrc b/.jshintrc
index 20dadcd1fa..f0e6b02645 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -19,11 +19,13 @@
"globals": {
"_": false,
"Backbone": false,
+ "console": false,
"jQuery": false,
"JSON": false,
"wp": false,
"export": false,
"module": false,
- "require": false
+ "require": false,
+ "Set": false
}
}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 453ef5c794..02e1b657e2 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -68,7 +68,9 @@
+
+
diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php
index ddaa270c6d..857f1e948b 100644
--- a/src/wp-includes/class-wp-scripts.php
+++ b/src/wp-includes/class-wp-scripts.php
@@ -133,6 +133,24 @@ class WP_Scripts extends WP_Dependencies {
*/
private $type_attr = '';
+ /**
+ * Holds a mapping of dependents (as handles) for a given script handle.
+ * Used to optimize recursive dependency tree checks.
+ *
+ * @since 6.3.0
+ * @var array
+ */
+ private $dependents_map = array();
+
+ /**
+ * Holds a reference to the delayed (non-blocking) script loading strategies.
+ * Used by methods that validate loading strategies.
+ *
+ * @since 6.3.0
+ * @var string[]
+ */
+ private $delayed_strategies = array( 'defer', 'async' );
+
/**
* Constructor.
*
@@ -284,29 +302,27 @@ class WP_Scripts extends WP_Dependencies {
$ver = $ver ? $ver . '&' . $this->args[ $handle ] : $this->args[ $handle ];
}
- $src = $obj->src;
- $cond_before = '';
- $cond_after = '';
- $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : '';
+ $src = $obj->src;
+ $strategy = $this->get_eligible_loading_strategy( $handle );
+ $intended_strategy = (string) $this->get_data( $handle, 'strategy' );
+ $cond_before = '';
+ $cond_after = '';
+ $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : '';
+
+ if ( ! $this->is_delayed_strategy( $intended_strategy ) ) {
+ $intended_strategy = '';
+ }
if ( $conditional ) {
$cond_before = "\n";
}
- $before_handle = $this->print_inline_script( $handle, 'before', false );
- $after_handle = $this->print_inline_script( $handle, 'after', false );
+ $before_script = $this->get_inline_script_tag( $handle, 'before' );
+ $after_script = $this->get_inline_script_tag( $handle, 'after' );
- if ( $before_handle ) {
- $before_handle = sprintf( "\n", $this->type_attr, esc_attr( $handle ), $before_handle );
- }
-
- if ( $after_handle ) {
- $after_handle = sprintf( "\n", $this->type_attr, esc_attr( $handle ), $after_handle );
- }
-
- if ( $before_handle || $after_handle ) {
- $inline_script_tag = $cond_before . $before_handle . $after_handle . $cond_after;
+ if ( $before_script || $after_script ) {
+ $inline_script_tag = $cond_before . $before_script . $after_script . $cond_after;
} else {
$inline_script_tag = '';
}
@@ -333,7 +349,10 @@ class WP_Scripts extends WP_Dependencies {
*/
$srce = apply_filters( 'script_loader_src', $src, $handle );
- if ( $this->in_default_dir( $srce ) && ( $before_handle || $after_handle || $translations_stop_concat ) ) {
+ if (
+ $this->in_default_dir( $srce )
+ && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) )
+ ) {
$this->do_concat = false;
// Have to print the so-far concatenated scripts right away to maintain the right order.
@@ -390,9 +409,16 @@ class WP_Scripts extends WP_Dependencies {
return true;
}
- $tag = $translations . $cond_before . $before_handle;
- $tag .= sprintf( "\n", $this->type_attr, $src, esc_attr( $handle ) );
- $tag .= $after_handle . $cond_after;
+ $tag = $translations . $cond_before . $before_script;
+ $tag .= sprintf(
+ "\n",
+ $this->type_attr,
+ $src, // Value is escaped above.
+ esc_attr( $handle ),
+ $strategy ? " {$strategy}" : '',
+ $intended_strategy ? " data-wp-strategy='{$intended_strategy}'" : ''
+ );
+ $tag .= $after_script . $cond_after;
/**
* Filters the HTML script tag of an enqueued script.
@@ -445,29 +471,97 @@ class WP_Scripts extends WP_Dependencies {
* Prints inline scripts registered for a specific handle.
*
* @since 4.5.0
+ * @deprecated 6.3.0 Use methods get_inline_script_tag() or get_inline_script_data() instead.
*
- * @param string $handle Name of the script to add the inline script to.
+ * @param string $handle Name of the script to print inline scripts for.
* Must be lowercase.
* @param string $position Optional. Whether to add the inline script
* before the handle or after. Default 'after'.
- * @param bool $display Optional. Whether to print the script
- * instead of just returning it. Default true.
- * @return string|false Script on success, false otherwise.
+ * @param bool $display Optional. Whether to print the script tag
+ * instead of just returning the script data. Default true.
+ * @return string|false Script data on success, false otherwise.
*/
public function print_inline_script( $handle, $position = 'after', $display = true ) {
- $output = $this->get_data( $handle, $position );
+ _deprecated_function( __METHOD__, '6.3.0', 'WP_Scripts::get_inline_script_data() or WP_Scripts::get_inline_script_tag()' );
+ $output = $this->get_inline_script_data( $handle, $position );
if ( empty( $output ) ) {
return false;
}
- $output = trim( implode( "\n", $output ), "\n" );
-
if ( $display ) {
- printf( "\n", $this->type_attr, esc_attr( $handle ), esc_attr( $position ), $output );
+ echo $this->get_inline_script_tag( $handle, $position );
+ }
+ return $output;
+ }
+
+ /**
+ * Gets data for inline scripts registered for a specific handle.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle Name of the script to get data for.
+ * Must be lowercase.
+ * @param string $position Optional. Whether to add the inline script
+ * before the handle or after. Default 'after'.
+ * @return string Inline script, which may be empty string.
+ */
+ public function get_inline_script_data( $handle, $position = 'after' ) {
+ $data = $this->get_data( $handle, $position );
+ if ( empty( $data ) || ! is_array( $data ) ) {
+ return '';
}
- return $output;
+ return trim( implode( "\n", $data ), "\n" );
+ }
+
+ /**
+ * Gets unaliased dependencies.
+ *
+ * An alias is a dependency whose src is false. It is used as a way to bundle multiple dependencies in a single
+ * handle. This in effect flattens an alias dependency tree.
+ *
+ * @since 6.3.0
+ *
+ * @param string[] $deps Dependency handles.
+ * @return string[] Unaliased handles.
+ */
+ private function get_unaliased_deps( array $deps ) {
+ $flattened = array();
+ foreach ( $deps as $dep ) {
+ if ( ! isset( $this->registered[ $dep ] ) ) {
+ continue;
+ }
+
+ if ( $this->registered[ $dep ]->src ) {
+ $flattened[] = $dep;
+ } elseif ( $this->registered[ $dep ]->deps ) {
+ array_push( $flattened, ...$this->get_unaliased_deps( $this->registered[ $dep ]->deps ) );
+ }
+ }
+ return $flattened;
+ }
+
+ /**
+ * Gets tags for inline scripts registered for a specific handle.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle Name of the script to get associated inline script tag for.
+ * Must be lowercase.
+ * @param string $position Optional. Whether to get tag for inline
+ * scripts in the before or after position. Default 'after'.
+ * @return string Inline script, which may be empty string.
+ */
+ public function get_inline_script_tag( $handle, $position = 'after' ) {
+ $js = $this->get_inline_script_data( $handle, $position );
+ if ( empty( $js ) ) {
+ return '';
+ }
+
+ $id = "{$handle}-js-{$position}";
+
+ return wp_get_inline_script_tag( $js, compact( 'id' ) );
}
/**
@@ -714,6 +808,199 @@ JS;
return false;
}
+ /**
+ * This overrides the add_data method from WP_Dependencies, to support normalizing of $args.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle Name of the item. Should be unique.
+ * @param string $key The data key.
+ * @param mixed $value The data value.
+ * @return bool True on success, false on failure.
+ */
+ public function add_data( $handle, $key, $value ) {
+ if ( ! isset( $this->registered[ $handle ] ) ) {
+ return false;
+ }
+
+ if ( 'strategy' === $key ) {
+ if ( ! empty( $value ) && ! $this->is_delayed_strategy( $value ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: 1: $strategy, 2: $handle */
+ __( 'Invalid strategy `%1$s` defined for `%2$s` during script registration.' ),
+ $value,
+ $handle
+ ),
+ '6.3.0'
+ );
+ return false;
+ } elseif ( ! $this->registered[ $handle ]->src && $this->is_delayed_strategy( $value ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: 1: $strategy, 2: $handle */
+ __( 'Cannot supply a strategy `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ),
+ $value,
+ $handle
+ ),
+ '6.3.0'
+ );
+ return false;
+ }
+ }
+ return parent::add_data( $handle, $key, $value );
+ }
+
+ /**
+ * Gets all dependents of a script.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle The script handle.
+ * @return string[] Script handles.
+ */
+ private function get_dependents( $handle ) {
+ // Check if dependents map for the handle in question is present. If so, use it.
+ if ( isset( $this->dependents_map[ $handle ] ) ) {
+ return $this->dependents_map[ $handle ];
+ }
+
+ $dependents = array();
+
+ // Iterate over all registered scripts, finding dependents of the script passed to this method.
+ foreach ( $this->registered as $registered_handle => $args ) {
+ if ( in_array( $handle, $args->deps, true ) ) {
+ $dependents[] = $registered_handle;
+ }
+ }
+
+ // Add the handles dependents to the map to ease future lookups.
+ $this->dependents_map[ $handle ] = $dependents;
+
+ return $dependents;
+ }
+
+ /**
+ * Checks if the strategy passed is a valid delayed (non-blocking) strategy.
+ *
+ * @since 6.3.0
+ *
+ * @param string $strategy The strategy to check.
+ * @return bool True if $strategy is one of the delayed strategies, otherwise false.
+ */
+ private function is_delayed_strategy( $strategy ) {
+ return in_array(
+ $strategy,
+ $this->delayed_strategies,
+ true
+ );
+ }
+
+ /**
+ * Gets the best eligible loading strategy for a script.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle The script handle.
+ * @return string The best eligible loading strategy.
+ */
+ private function get_eligible_loading_strategy( $handle ) {
+ $eligible = $this->filter_eligible_strategies( $handle );
+
+ // Bail early once we know the eligible strategy is blocking.
+ if ( empty( $eligible ) ) {
+ return '';
+ }
+
+ return in_array( 'async', $eligible, true ) ? 'async' : 'defer';
+ }
+
+ /**
+ * Filter the list of eligible loading strategies for a script.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle The script handle.
+ * @param string[]|null $eligible Optional. The list of strategies to filter. Default null.
+ * @param array $checked Optional. An array of already checked script handles, used to avoid recursive loops.
+ * @return string[] A list of eligible loading strategies that could be used.
+ */
+ private function filter_eligible_strategies( $handle, $eligible = null, $checked = array() ) {
+ // If no strategies are being passed, all strategies are eligible.
+ if ( null === $eligible ) {
+ $eligible = $this->delayed_strategies;
+ }
+
+ // If this handle was already checked, return early.
+ if ( isset( $checked[ $handle ] ) ) {
+ return $eligible;
+ }
+
+ // Mark this handle as checked.
+ $checked[ $handle ] = true;
+
+ // If this handle isn't registered, don't filter anything and return.
+ if ( ! isset( $this->registered[ $handle ] ) ) {
+ return $eligible;
+ }
+
+ // If the handle is not enqueued, don't filter anything and return.
+ if ( ! $this->query( $handle, 'enqueued' ) ) {
+ return $eligible;
+ }
+
+ $is_alias = (bool) ! $this->registered[ $handle ]->src;
+ $intended_strategy = $this->get_data( $handle, 'strategy' );
+
+ // For non-alias handles, an empty intended strategy filters all strategies.
+ if ( ! $is_alias && empty( $intended_strategy ) ) {
+ return array();
+ }
+
+ // Handles with inline scripts attached in the 'after' position cannot be delayed.
+ if ( $this->has_inline_script( $handle, 'after' ) ) {
+ return array();
+ }
+
+ // If the intended strategy is 'defer', filter out 'async'.
+ if ( 'defer' === $intended_strategy ) {
+ $eligible = array( 'defer' );
+ }
+
+ $dependents = $this->get_dependents( $handle );
+
+ // Recursively filter eligible strategies for dependents.
+ foreach ( $dependents as $dependent ) {
+ // Bail early once we know the eligible strategy is blocking.
+ if ( empty( $eligible ) ) {
+ return array();
+ }
+
+ $eligible = $this->filter_eligible_strategies( $dependent, $eligible, $checked );
+ }
+
+ return $eligible;
+ }
+
+ /**
+ * Gets data for inline scripts registered for a specific handle.
+ *
+ * @since 6.3.0
+ *
+ * @param string $handle Name of the script to get data for. Must be lowercase.
+ * @param string $position The position of the inline script.
+ * @return bool Whether the handle has an inline script (either before or after).
+ */
+ private function has_inline_script( $handle, $position = null ) {
+ if ( $position && in_array( $position, array( 'before', 'after' ), true ) ) {
+ return (bool) $this->get_data( $handle, $position );
+ }
+
+ return (bool) ( $this->get_data( $handle, 'before' ) || $this->get_data( $handle, 'after' ) );
+ }
+
/**
* Resets class properties.
*
diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php
index 64b9368344..aab583d6e3 100644
--- a/src/wp-includes/functions.wp-scripts.php
+++ b/src/wp-includes/functions.wp-scripts.php
@@ -157,6 +157,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.
*
* @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.
@@ -166,20 +167,32 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) {
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
- * @param bool $in_footer Optional. Whether to enqueue the script before `
` instead of in the `
`.
- * Default 'false'.
+ * @param array|bool $args {
+ * 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'.
+ * }
* @return bool Whether the script has been registered. True on success, false on failure.
*/
-function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
+function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args = array() ) {
+ if ( ! is_array( $args ) ) {
+ $args = array(
+ 'in_footer' => (bool) $args,
+ );
+ }
_wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle );
$wp_scripts = wp_scripts();
$registered = $wp_scripts->add( $handle, $src, $deps, $ver );
- if ( $in_footer ) {
+ if ( ! empty( $args['in_footer'] ) ) {
$wp_scripts->add_data( $handle, 'group', 1 );
}
-
+ if ( ! empty( $args['strategy'] ) ) {
+ $wp_scripts->add_data( $handle, 'strategy', $args['strategy'] );
+ }
return $registered;
}
@@ -331,6 +344,7 @@ function wp_deregister_script( $handle ) {
* @see WP_Dependencies::enqueue()
*
* @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.
*
* @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.
@@ -340,24 +354,36 @@ function wp_deregister_script( $handle ) {
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
- * @param bool $in_footer Optional. Whether to enqueue the script before `` instead of in the `
`.
- * Default 'false'.
+ * @param array|bool $args {
+ * 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'.
+ * }
*/
-function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $in_footer = false ) {
+function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) {
_wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle );
$wp_scripts = wp_scripts();
- if ( $src || $in_footer ) {
+ if ( $src || ! empty( $args ) ) {
$_handle = explode( '?', $handle );
+ if ( ! is_array( $args ) ) {
+ $args = array(
+ 'in_footer' => (bool) $args,
+ );
+ }
if ( $src ) {
$wp_scripts->add( $_handle[0], $src, $deps, $ver );
}
-
- if ( $in_footer ) {
+ if ( ! empty( $args['in_footer'] ) ) {
$wp_scripts->add_data( $_handle[0], 'group', 1 );
}
+ if ( ! empty( $args['strategy'] ) ) {
+ $wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] );
+ }
}
$wp_scripts->enqueue( $handle );
diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php
index 78642c1178..6395f18f8d 100644
--- a/tests/phpunit/tests/dependencies/scripts.php
+++ b/tests/phpunit/tests/dependencies/scripts.php
@@ -10,17 +10,35 @@
* @covers ::wp_set_script_translations
*/
class Tests_Dependencies_Scripts extends WP_UnitTestCase {
+
+ /**
+ * @var WP_Scripts
+ */
protected $old_wp_scripts;
+ /**
+ * @var WP_Styles
+ */
+ protected $old_wp_styles;
+
protected $wp_scripts_print_translations_output;
+ /**
+ * Stores a string reference to a default scripts directory name, utilised by certain tests.
+ *
+ * @var string
+ */
+ protected $default_scripts_dir = '/directory/';
+
public function set_up() {
parent::set_up();
$this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
+ $this->old_wp_styles = isset( $GLOBALS['wp_styles'] ) ? $GLOBALS['wp_styles'] : null;
remove_action( 'wp_default_scripts', 'wp_default_scripts' );
remove_action( 'wp_default_scripts', 'wp_default_packages' );
$GLOBALS['wp_scripts'] = new WP_Scripts();
$GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
+ $GLOBALS['wp_styles'] = new WP_Styles();
$this->wp_scripts_print_translations_output = <<
@@ -36,6 +54,7 @@ JS;
public function tear_down() {
$GLOBALS['wp_scripts'] = $this->old_wp_scripts;
+ $GLOBALS['wp_styles'] = $this->old_wp_styles;
add_action( 'wp_default_scripts', 'wp_default_scripts' );
parent::tear_down();
}
@@ -46,14 +65,15 @@ JS;
* @ticket 11315
*/
public function test_wp_enqueue_script() {
+ global $wp_version;
+
wp_enqueue_script( 'no-deps-no-version', 'example.com', array() );
wp_enqueue_script( 'empty-deps-no-version', 'example.com' );
wp_enqueue_script( 'empty-deps-version', 'example.com', array(), 1.2 );
wp_enqueue_script( 'empty-deps-null-version', 'example.com', array(), null );
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
- $expected .= "\n";
+ $expected = "\n";
+ $expected .= "\n";
$expected .= "\n";
$expected .= "\n";
@@ -63,10 +83,1335 @@ JS;
$this->assertSame( '', get_echo( 'wp_print_scripts' ) );
}
+ /**
+ * Gets delayed strategies as a data provider.
+ *
+ * @return array[] Delayed strategies.
+ */
+ public function data_provider_delayed_strategies() {
+ return array(
+ 'defer' => array( 'defer' ),
+ 'async' => array( 'async' ),
+ );
+ }
+
+ /**
+ * Tests that inline scripts in the `after` position, attached to delayed main scripts, remain unaffected.
+ *
+ * If the main script with delayed loading strategy has an `after` inline script,
+ * the inline script should not be affected.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_inline_script_tag
+ * @covers ::wp_add_inline_script
+ * @covers ::wp_enqueue_script
+ *
+ * @dataProvider data_provider_delayed_strategies
+ *
+ * @param string $strategy Strategy.
+ */
+ public function test_after_inline_script_with_delayed_main_script( $strategy ) {
+ wp_enqueue_script( 'ms-isa-1', 'http://example.org/ms-isa-1.js', array(), null, compact( 'strategy' ) );
+ wp_add_inline_script( 'ms-isa-1', 'console.log("after one");', 'after' );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $expected .= wp_get_inline_script_tag(
+ "console.log(\"after one\");\n",
+ array(
+ 'id' => 'ms-isa-1-js-after',
+ )
+ );
+ $this->assertSame( $expected, $output, 'Inline scripts in the "after" position, that are attached to a deferred main script, are failing to print/execute.' );
+ }
+
+ /**
+ * Tests that inline scripts in the `after` position, attached to a blocking main script, are rendered as javascript.
+ *
+ * If a main script with a `blocking` strategy has an `after` inline script,
+ * the inline script should be rendered as type='text/javascript'.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_inline_script_tag
+ * @covers ::wp_add_inline_script
+ * @covers ::wp_enqueue_script
+ */
+ public function test_after_inline_script_with_blocking_main_script() {
+ wp_enqueue_script( 'ms-insa-3', 'http://example.org/ms-insa-3.js', array(), null );
+ wp_add_inline_script( 'ms-insa-3', 'console.log("after one");', 'after' );
+ $output = get_echo( 'wp_print_scripts' );
+
+ $expected = "\n";
+ $expected .= wp_get_inline_script_tag(
+ "console.log(\"after one\");\n",
+ array(
+ 'id' => 'ms-insa-3-js-after',
+ )
+ );
+
+ $this->assertSame( $expected, $output, 'Inline scripts in the "after" position, that are attached to a blocking main script, are failing to print/execute.' );
+ }
+
+ /**
+ * Tests that inline scripts in the `before` position, attached to a delayed inline main script, results in all
+ * dependents being delayed.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_inline_script_tag
+ * @covers ::wp_add_inline_script
+ * @covers ::wp_enqueue_script
+ *
+ * @dataProvider data_provider_delayed_strategies
+ *
+ * @param string $strategy
+ */
+ public function test_before_inline_scripts_with_delayed_main_script( $strategy ) {
+ wp_enqueue_script( 'ds-i1-1', 'http://example.org/ds-i1-1.js', array(), null, compact( 'strategy' ) );
+ wp_add_inline_script( 'ds-i1-1', 'console.log("before first");', 'before' );
+ wp_enqueue_script( 'ds-i1-2', 'http://example.org/ds-i1-2.js', array(), null, compact( 'strategy' ) );
+ wp_enqueue_script( 'ds-i1-3', 'http://example.org/ds-i1-3.js', array(), null, compact( 'strategy' ) );
+ wp_enqueue_script( 'ms-i1-1', 'http://example.org/ms-i1-1.js', array( 'ds-i1-1', 'ds-i1-2', 'ds-i1-3' ), null, compact( 'strategy' ) );
+ wp_add_inline_script( 'ms-i1-1', 'console.log("before last");', 'before' );
+ $output = get_echo( 'wp_print_scripts' );
+
+ $expected = wp_get_inline_script_tag(
+ "console.log(\"before first\");\n",
+ array(
+ 'id' => 'ds-i1-1-js-before',
+ )
+ );
+ $expected .= "\n";
+ $expected .= "\n";
+ $expected .= "\n";
+ $expected .= wp_get_inline_script_tag(
+ "console.log(\"before last\");\n",
+ array(
+ 'id' => 'ms-i1-1-js-before',
+ 'type' => 'text/javascript',
+ )
+ );
+ $expected .= "\n";
+
+ $this->assertSame( $expected, $output, 'Inline scripts in the "before" position, that are attached to a deferred main script, are failing to print/execute.' );
+ }
+
+ /**
+ * Tests that scripts registered with an async strategy print with the async attribute.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_valid_async_registration() {
+ // No dependents, No dependencies then async.
+ wp_enqueue_script( 'main-script-a1', '/main-script-a1.js', array(), null, array( 'strategy' => 'async' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertSame( $expected, $output, 'Scripts enqueued with an async loading strategy are failing to have the async attribute applied to the script handle when being printed.' );
+ }
+
+ /**
+ * Tests that dependents of a blocking dependency script are free to contain any strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ *
+ * @dataProvider data_provider_delayed_strategies
+ *
+ * @param string $strategy Strategy.
+ */
+ public function test_delayed_dependent_with_blocking_dependency( $strategy ) {
+ wp_enqueue_script( 'dependency-script-a2', '/dependency-script-a2.js', array(), null );
+ wp_enqueue_script( 'main-script-a2', '/main-script-a2.js', array( 'dependency-script-a2' ), null, compact( 'strategy' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "";
+ $this->assertStringContainsString( $expected, $output, 'Dependents of a blocking dependency are free to have any strategy.' );
+ }
+
+ /**
+ * Tests that blocking dependents force delayed dependencies to become blocking.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ *
+ * @dataProvider data_provider_delayed_strategies
+ * @param string $strategy Strategy.
+ */
+ public function test_blocking_dependent_with_delayed_dependency( $strategy ) {
+ wp_enqueue_script( 'main-script-a3', '/main-script-a3.js', array(), null, compact( 'strategy' ) );
+ wp_enqueue_script( 'dependent-script-a3', '/dependent-script-a3.js', array( 'main-script-a3' ), null );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "";
+ $this->assertStringContainsString( $expected, $output, 'Blocking dependents must force delayed dependencies to become blocking.' );
+ }
+
+ /**
+ * Tests that only enqueued dependents effect the eligible loading strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ *
+ * @dataProvider data_provider_delayed_strategies
+ * @param string $strategy Strategy.
+ */
+ public function test_delayed_dependent_with_blocking_dependency_not_enqueued( $strategy ) {
+ wp_enqueue_script( 'main-script-a4', '/main-script-a4.js', array(), null, compact( 'strategy' ) );
+ // This dependent is registered but not enqueued, so it should not factor into the eligible loading strategy.
+ wp_register_script( 'dependent-script-a4', '/dependent-script-a4.js', array( 'main-script-a4' ), null );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "";
+ $this->assertStringContainsString( $expected, $output, 'Only enqueued dependents should affect the eligible strategy.' );
+ }
+
+ /**
+ * Data provider for test_filter_eligible_strategies.
+ *
+ * @return array
+ */
+ public function get_data_to_filter_eligible_strategies() {
+ return array(
+ 'no_dependents' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'one_delayed_dependent' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'one_blocking_dependent' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null );
+ return 'foo';
+ },
+ 'expected' => array(),
+ ),
+ 'one_blocking_dependent_not_enqueued' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_register_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ), // Because bar was not enqueued, only foo was.
+ ),
+ 'two_delayed_dependents' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'baz', 'https://example.com/baz.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'recursion_not_delayed' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'foo' ), null );
+ return 'foo';
+ },
+ 'expected' => array(),
+ ),
+ 'recursion_yes_delayed' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'recursion_triple_level' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'baz' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'baz', 'https://example.com/bar.js', array( 'bar' ), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'async_only_with_async_dependency' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null, array( 'strategy' => 'async' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer', 'async' ),
+ ),
+ 'async_only_with_defer_dependency' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null, array( 'strategy' => 'defer' ) );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'async_only_with_blocking_dependency' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_enqueue_script( 'bar', 'https://example.com/bar.js', array( 'foo' ), null );
+ return 'foo';
+ },
+ 'expected' => array(),
+ ),
+ 'defer_with_inline_after_script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_add_inline_script( 'foo', 'console.log("foo")', 'after' );
+ return 'foo';
+ },
+ 'expected' => array(),
+ ),
+ 'defer_with_inline_before_script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_add_inline_script( 'foo', 'console.log("foo")', 'before' );
+ return 'foo';
+ },
+ 'expected' => array( 'defer' ),
+ ),
+ 'async_with_inline_after_script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_add_inline_script( 'foo', 'console.log("foo")', 'after' );
+ return 'foo';
+ },
+ 'expected' => array(),
+ ),
+ 'async_with_inline_before_script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_add_inline_script( 'foo', 'console.log("foo")', 'before' );
+ return 'foo';
+ },
+ 'expected' => array( 'defer', 'async' ),
+ ),
+ );
+ }
+
+ /**
+ * Tests that the filter_eligible_strategies method works as expected and returns the correct value.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::filter_eligible_strategies
+ *
+ * @dataProvider get_data_to_filter_eligible_strategies
+ *
+ * @param callable $set_up Set up.
+ * @param bool $async_only Async only.
+ * @param bool $expected Expected return value.
+ */
+ public function test_filter_eligible_strategies( $set_up, $expected ) {
+ $handle = $set_up();
+
+ $wp_scripts_reflection = new ReflectionClass( WP_Scripts::class );
+ $filter_eligible_strategies = $wp_scripts_reflection->getMethod( 'filter_eligible_strategies' );
+ $filter_eligible_strategies->setAccessible( true );
+ $this->assertSame( $expected, $filter_eligible_strategies->invokeArgs( wp_scripts(), array( $handle ) ), 'Expected return value of WP_Scripts::filter_eligible_strategies to match.' );
+ }
+
+ /**
+ * Register test script.
+ *
+ * @param string $handle Dependency handle to enqueue.
+ * @param string $strategy Strategy to use for dependency.
+ * @param string[] $deps Dependencies for the script.
+ * @param bool $in_footer Whether to print the script in the footer.
+ */
+ protected function register_test_script( $handle, $strategy, $deps = array(), $in_footer = false ) {
+ wp_register_script(
+ $handle,
+ add_query_arg(
+ array(
+ 'script_event_log' => "$handle: script",
+ ),
+ 'https://example.com/external.js'
+ ),
+ $deps,
+ null
+ );
+ if ( 'blocking' !== $strategy ) {
+ wp_script_add_data( $handle, 'strategy', $strategy );
+ }
+ }
+
+ /**
+ * Enqueue test script.
+ *
+ * @param string $handle Dependency handle to enqueue.
+ * @param string $strategy Strategy to use for dependency.
+ * @param string[] $deps Dependencies for the script.
+ * @param bool $in_footer Whether to print the script in the footer.
+ */
+ protected function enqueue_test_script( $handle, $strategy, $deps = array(), $in_footer = false ) {
+ $this->register_test_script( $handle, $strategy, $deps, $in_footer );
+ wp_enqueue_script( $handle );
+ }
+
+ /**
+ * Adds test inline script.
+ *
+ * @param string $handle Dependency handle to enqueue.
+ * @param string $position Position.
+ */
+ protected function add_test_inline_script( $handle, $position ) {
+ wp_add_inline_script( $handle, sprintf( 'scriptEventLog.push( %s )', wp_json_encode( "{$handle}: {$position} inline" ) ), $position );
+ }
+
+ /**
+ * Data provider to test various strategy dependency chains.
+ *
+ * @return array[]
+ */
+ public function data_provider_to_test_various_strategy_dependency_chains() {
+ return array(
+ 'async-dependent-with-one-blocking-dependency' => array(
+ 'set_up' => function () {
+ $handle1 = 'blocking-not-async-without-dependency';
+ $handle2 = 'async-with-blocking-dependency';
+ $this->enqueue_test_script( $handle1, 'blocking', array() );
+ $this->enqueue_test_script( $handle2, 'async', array( $handle1 ) );
+ foreach ( array( $handle1, $handle2 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "blocking-not-async-without-dependency: before inline" )
+
+
+
+
+
+
+HTML
+ ,
+ /*
+ * Note: The above comma must be on its own line in PHP<7.3 and not after the `HTML` identifier
+ * terminating the heredoc. Otherwise, a syntax error is raised with the line number being wildly wrong:
+ *
+ * PHP Parse error: syntax error, unexpected '' (T_ENCAPSED_AND_WHITESPACE), expecting '-' or identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING)
+ */
+ ),
+ 'async-with-async-dependencies' => array(
+ 'set_up' => function () {
+ $handle1 = 'async-no-dependency';
+ $handle2 = 'async-one-async-dependency';
+ $handle3 = 'async-two-async-dependencies';
+ $this->enqueue_test_script( $handle1, 'async', array() );
+ $this->enqueue_test_script( $handle2, 'async', array( $handle1 ) );
+ $this->enqueue_test_script( $handle3, 'async', array( $handle1, $handle2 ) );
+ foreach ( array( $handle1, $handle2, $handle3 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "async-no-dependency: before inline" )
+
+
+
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'async-with-blocking-dependency' => array(
+ 'set_up' => function () {
+ $handle1 = 'async-with-blocking-dependent';
+ $handle2 = 'blocking-dependent-of-async';
+ $this->enqueue_test_script( $handle1, 'async', array() );
+ $this->enqueue_test_script( $handle2, 'blocking', array( $handle1 ) );
+ foreach ( array( $handle1, $handle2 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "async-with-blocking-dependent: before inline" )
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'defer-with-async-dependency' => array(
+ 'set_up' => function () {
+ $handle1 = 'async-with-defer-dependent';
+ $handle2 = 'defer-dependent-of-async';
+ $this->enqueue_test_script( $handle1, 'async', array() );
+ $this->enqueue_test_script( $handle2, 'defer', array( $handle1 ) );
+ foreach ( array( $handle1, $handle2 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "async-with-defer-dependent: before inline" )
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'blocking-bundle-of-none-with-inline-scripts-and-defer-dependent' => array(
+ 'set_up' => function () {
+ $handle1 = 'blocking-bundle-of-none';
+ $handle2 = 'defer-dependent-of-blocking-bundle-of-none';
+
+ wp_register_script( $handle1, false, array(), null );
+ $this->add_test_inline_script( $handle1, 'before' );
+ $this->add_test_inline_script( $handle1, 'after' );
+
+ // Note: the before script for this will be blocking because the dependency is blocking.
+ $this->enqueue_test_script( $handle2, 'defer', array( $handle1 ) );
+ $this->add_test_inline_script( $handle2, 'before' );
+ $this->add_test_inline_script( $handle2, 'after' );
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "blocking-bundle-of-none: before inline" )
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'blocking-bundle-of-two-with-defer-dependent' => array(
+ 'set_up' => function () {
+ $handle1 = 'blocking-bundle-of-two';
+ $handle2 = 'blocking-bundle-member-one';
+ $handle3 = 'blocking-bundle-member-two';
+ $handle4 = 'defer-dependent-of-blocking-bundle-of-two';
+
+ wp_register_script( $handle1, false, array( $handle2, $handle3 ), null );
+ $this->enqueue_test_script( $handle2, 'blocking' );
+ $this->enqueue_test_script( $handle3, 'blocking' );
+ $this->enqueue_test_script( $handle4, 'defer', array( $handle1 ) );
+
+ foreach ( array( $handle2, $handle3, $handle4 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "blocking-bundle-member-one: before inline" )
+
+
+
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'defer-bundle-of-none-with-inline-scripts-and-defer-dependents' => array(
+ 'set_up' => function () {
+ $handle1 = 'defer-bundle-of-none';
+ $handle2 = 'defer-dependent-of-defer-bundle-of-none';
+
+ // The eligible loading strategy for this will be forced to be blocking when rendered since $src = false.
+ wp_register_script( $handle1, false, array(), null );
+ wp_scripts()->registered[ $handle1 ]->extra['strategy'] = 'defer'; // Bypass wp_script_add_data() which should no-op with _doing_it_wrong() because of $src=false.
+ $this->add_test_inline_script( $handle1, 'before' );
+ $this->add_test_inline_script( $handle1, 'after' );
+
+ // Note: the before script for this will be blocking because the dependency is blocking.
+ $this->enqueue_test_script( $handle2, 'defer', array( $handle1 ) );
+ $this->add_test_inline_script( $handle2, 'before' );
+ $this->add_test_inline_script( $handle2, 'after' );
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "defer-bundle-of-none: before inline" )
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'defer-dependent-with-blocking-and-defer-dependencies' => array(
+ 'set_up' => function () {
+ $handle1 = 'blocking-dependency-with-defer-following-dependency';
+ $handle2 = 'defer-dependency-with-blocking-preceding-dependency';
+ $handle3 = 'defer-dependent-of-blocking-and-defer-dependencies';
+ $this->enqueue_test_script( $handle1, 'blocking', array() );
+ $this->enqueue_test_script( $handle2, 'defer', array() );
+ $this->enqueue_test_script( $handle3, 'defer', array( $handle1, $handle2 ) );
+
+ foreach ( array( $handle1, $handle2, $handle3 ) as $dep ) {
+ $this->add_test_inline_script( $dep, 'before' );
+ $this->add_test_inline_script( $dep, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "blocking-dependency-with-defer-following-dependency: before inline" )
+
+
+
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'defer-dependent-with-defer-and-blocking-dependencies' => array(
+ 'set_up' => function () {
+ $handle1 = 'defer-dependency-with-blocking-following-dependency';
+ $handle2 = 'blocking-dependency-with-defer-preceding-dependency';
+ $handle3 = 'defer-dependent-of-defer-and-blocking-dependencies';
+ $this->enqueue_test_script( $handle1, 'defer', array() );
+ $this->enqueue_test_script( $handle2, 'blocking', array() );
+ $this->enqueue_test_script( $handle3, 'defer', array( $handle1, $handle2 ) );
+
+ foreach ( array( $handle1, $handle2, $handle3 ) as $dep ) {
+ $this->add_test_inline_script( $dep, 'before' );
+ $this->add_test_inline_script( $dep, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "defer-dependency-with-blocking-following-dependency: before inline" )
+
+
+
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'async-with-defer-dependency' => array(
+ 'set_up' => function () {
+ $handle1 = 'defer-with-async-dependent';
+ $handle2 = 'async-dependent-of-defer';
+ $this->enqueue_test_script( $handle1, 'defer', array() );
+ $this->enqueue_test_script( $handle2, 'async', array( $handle1 ) );
+ foreach ( array( $handle1, $handle2 ) as $handle ) {
+ $this->add_test_inline_script( $handle, 'before' );
+ $this->add_test_inline_script( $handle, 'after' );
+ }
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "defer-with-async-dependent: before inline" )
+
+
+
+
+
+
+HTML
+ ,
+ ),
+ 'defer-with-before-inline-script' => array(
+ 'set_up' => function () {
+ // Note this should NOT result in no delayed-inline-script-loader script being added.
+ $handle = 'defer-with-before-inline';
+ $this->enqueue_test_script( $handle, 'defer', array() );
+ $this->add_test_inline_script( $handle, 'before' );
+ },
+ 'expected_markup' => <<
+scriptEventLog.push( "defer-with-before-inline: before inline" )
+
+
+HTML
+ ,
+ ),
+ 'defer-with-after-inline-script' => array(
+ 'set_up' => function () {
+ // Note this SHOULD result in delayed-inline-script-loader script being added.
+ $handle = 'defer-with-after-inline';
+ $this->enqueue_test_script( $handle, 'defer', array() );
+ $this->add_test_inline_script( $handle, 'after' );
+ },
+ 'expected_markup' => <<
+
+HTML
+ ,
+ ),
+ 'jquery-deferred' => array(
+ 'set_up' => function () {
+ $wp_scripts = wp_scripts();
+ wp_default_scripts( $wp_scripts );
+ foreach ( $wp_scripts->registered['jquery']->deps as $jquery_dep ) {
+ $wp_scripts->registered[ $jquery_dep ]->add_data( 'strategy', 'defer' );
+ $wp_scripts->registered[ $jquery_dep ]->ver = null; // Just to avoid markup changes in the test when jQuery is upgraded.
+ }
+ wp_enqueue_script( 'theme-functions', 'https://example.com/theme-functions.js', array( 'jquery' ), null, array( 'strategy' => 'defer' ) );
+ },
+ 'expected_markup' => <<
+
+
+HTML
+ ,
+ ),
+ 'nested-aliases' => array(
+ 'set_up' => function () {
+ $outer_alias_handle = 'outer-bundle-of-two';
+ $inner_alias_handle = 'inner-bundle-of-two';
+
+ // The outer alias contains a blocking member, as well as a nested alias that contains defer scripts.
+ wp_register_script( $outer_alias_handle, false, array( $inner_alias_handle, 'outer-bundle-leaf-member' ), null );
+ $this->register_test_script( 'outer-bundle-leaf-member', 'blocking', array() );
+
+ // Inner alias only contains delay scripts.
+ wp_register_script( $inner_alias_handle, false, array( 'inner-bundle-member-one', 'inner-bundle-member-two' ), null );
+ $this->register_test_script( 'inner-bundle-member-one', 'defer', array() );
+ $this->register_test_script( 'inner-bundle-member-two', 'defer', array() );
+
+ $this->enqueue_test_script( 'defer-dependent-of-nested-aliases', 'defer', array( $outer_alias_handle ) );
+ $this->add_test_inline_script( 'defer-dependent-of-nested-aliases', 'before' );
+ $this->add_test_inline_script( 'defer-dependent-of-nested-aliases', 'after' );
+ },
+ 'expected_markup' => <<
+
+
+
+
+
+HTML
+ ,
+ ),
+
+ 'async-alias-members-with-defer-dependency' => array(
+ 'set_up' => function () {
+ $alias_handle = 'async-alias';
+ $async_handle1 = 'async1';
+ $async_handle2 = 'async2';
+
+ wp_register_script( $alias_handle, false, array( $async_handle1, $async_handle2 ), null );
+ $this->register_test_script( $async_handle1, 'async', array() );
+ $this->register_test_script( $async_handle2, 'async', array() );
+
+ $this->enqueue_test_script( 'defer-dependent-of-async-aliases', 'defer', array( $alias_handle ) );
+ },
+ 'expected_markup' => <<
+
+
+HTML
+ ,
+ ),
+ );
+ }
+
+ /**
+ * Tests that various loading strategy dependency chains function as expected.
+ *
+ * @covers ::wp_enqueue_script()
+ * @covers ::wp_add_inline_script()
+ * @covers ::wp_print_scripts()
+ * @covers WP_Scripts::get_inline_script_tag
+ *
+ * @dataProvider data_provider_to_test_various_strategy_dependency_chains
+ *
+ * @param callable $set_up Set up.
+ * @param string $expected_markup Expected markup.
+ */
+ public function test_various_strategy_dependency_chains( $set_up, $expected_markup ) {
+ $set_up();
+ $actual_markup = get_echo( 'wp_print_scripts' );
+ $this->assertEqualMarkup( trim( $expected_markup ), trim( $actual_markup ), "Actual markup:\n{$actual_markup}" );
+ }
+
+ /**
+ * Tests that defer is the final strategy when registering a script using defer, that has no dependents/dependencies.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_defer_having_no_dependents_nor_dependencies() {
+ wp_enqueue_script( 'main-script-d1', 'http://example.com/main-script-d1.js', array(), null, array( 'strategy' => 'defer' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertStringContainsString( $expected, $output, 'Expected defer, as there is no dependent or dependency' );
+ }
+
+ /**
+ * Tests that a script registered with defer remains deferred when all dependencies are either deferred or blocking.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_defer_dependent_and_varied_dependencies() {
+ wp_enqueue_script( 'dependency-script-d2-1', 'http://example.com/dependency-script-d2-1.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'dependency-script-d2-2', 'http://example.com/dependency-script-d2-2.js', array(), null );
+ wp_enqueue_script( 'dependency-script-d2-3', 'http://example.com/dependency-script-d2-3.js', array( 'dependency-script-d2-2' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'main-script-d2', 'http://example.com/main-script-d2.js', array( 'dependency-script-d2-1', 'dependency-script-d2-3' ), null, array( 'strategy' => 'defer' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertStringContainsString( $expected, $output, 'Expected defer, as all dependencies are either deferred or blocking' );
+ }
+
+ /**
+ * Tests that scripts registered with defer remain deferred when all dependents are also deferred.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_all_defer_dependencies() {
+ wp_enqueue_script( 'main-script-d3', 'http://example.com/main-script-d3.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'dependent-script-d3-1', 'http://example.com/dependent-script-d3-1.js', array( 'main-script-d3' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'dependent-script-d3-2', 'http://example.com/dependent-script-d3-2.js', array( 'dependent-script-d3-1' ), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'dependent-script-d3-3', 'http://example.com/dependent-script-d3-3.js', array( 'dependent-script-d3-2' ), null, array( 'strategy' => 'defer' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertStringContainsString( $expected, $output, 'Expected defer, as all dependents have defer loading strategy' );
+ }
+
+ /**
+ * Tests that dependents that are async but attached to a deferred main script, print with defer as opposed to async.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @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' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $expected .= "\n";
+ $expected .= "\n";
+ $expected .= "\n";
+
+ $this->assertSame( $expected, $output, 'Scripts registered as defer but that have dependents that are async are expected to have said dependents deferred.' );
+ }
+
+ /**
+ * Tests that scripts registered as defer become blocking when their dependents chain are all blocking.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_invalid_defer_registration() {
+ // Main script is defer and all dependent are not defer. Then main script will have blocking(or no) strategy.
+ 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 );
+ wp_enqueue_script( 'dependent-script-d4-3', '/dependent-script-d4-3.js', array( 'dependent-script-d4-2' ), null, array( 'strategy' => 'defer' ) );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertStringContainsString( $expected, $output, 'Scripts registered as defer but that have all dependents with no strategy, should become blocking (no strategy).' );
+ }
+
+ /**
+ * Tests that scripts registered as default/blocking remain as such when they have no dependencies.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::get_eligible_loading_strategy
+ * @covers WP_Scripts::filter_eligible_strategies
+ * @covers ::wp_enqueue_script
+ */
+ public function test_loading_strategy_with_valid_blocking_registration() {
+ wp_enqueue_script( 'main-script-b1', '/main-script-b1.js', array(), null );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertSame( $expected, $output, 'Scripts registered with a "blocking" strategy, and who have no dependencies, should have no loading strategy attributes printed.' );
+
+ // strategy args not set.
+ wp_enqueue_script( 'main-script-b2', '/main-script-b2.js', array(), null, array() );
+ $output = get_echo( 'wp_print_scripts' );
+ $expected = "\n";
+ $this->assertSame( $expected, $output, 'Scripts registered with no strategy assigned, and who have no dependencies, should have no loading strategy attributes printed.' );
+ }
+
+ /**
+ * Tests that scripts registered for the head do indeed end up there.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_register_script
+ */
+ public function test_scripts_targeting_head() {
+ wp_register_script( 'header-old', '/header-old.js', array(), null, false );
+ wp_register_script( 'header-new', '/header-new.js', array( 'header-old' ), null, array( 'in_footer' => false ) );
+ wp_enqueue_script( 'enqueue-header-old', '/enqueue-header-old.js', array( 'header-new' ), null, false );
+ wp_enqueue_script( 'enqueue-header-new', '/enqueue-header-new.js', array( 'enqueue-header-old' ), null, array( 'in_footer' => false ) );
+
+ $actual_header = get_echo( 'wp_print_head_scripts' );
+ $actual_footer = get_echo( 'wp_print_scripts' );
+
+ $expected_header = "\n";
+ $expected_header .= "\n";
+ $expected_header .= "\n";
+ $expected_header .= "\n";
+
+ $this->assertSame( $expected_header, $actual_header, 'Scripts registered/enqueued using the older $in_footer parameter or the newer $args parameter should have the same outcome.' );
+ $this->assertEmpty( $actual_footer, 'Expected footer to be empty since all scripts were for head.' );
+ }
+
+ /**
+ * Test that scripts registered for the footer do indeed end up there.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_register_script
+ */
+ public function test_scripts_targeting_footer() {
+ wp_register_script( 'footer-old', '/footer-old.js', array(), null, true );
+ wp_register_script( 'footer-new', '/footer-new.js', array( 'footer-old' ), null, array( 'in_footer' => true ) );
+ wp_enqueue_script( 'enqueue-footer-old', '/enqueue-footer-old.js', array( 'footer-new' ), null, true );
+ wp_enqueue_script( 'enqueue-footer-new', '/enqueue-footer-new.js', array( 'enqueue-footer-old' ), null, array( 'in_footer' => true ) );
+
+ $actual_header = get_echo( 'wp_print_head_scripts' );
+ $actual_footer = get_echo( 'wp_print_scripts' );
+
+ $expected_footer = "\n";
+ $expected_footer .= "\n";
+ $expected_footer .= "\n";
+ $expected_footer .= "\n";
+
+ $this->assertEmpty( $actual_header, 'Expected header to be empty since all scripts targeted footer.' );
+ $this->assertSame( $expected_footer, $actual_footer, 'Scripts registered/enqueued using the older $in_footer parameter or the newer $args parameter should have the same outcome.' );
+ }
+
+ /**
+ * Data provider for test_setting_in_footer_and_strategy.
+ *
+ * @return array[]
+ */
+ public function get_data_for_test_setting_in_footer_and_strategy() {
+ return array(
+ // Passing in_footer and strategy via args array.
+ 'async_footer_in_args_array' => array(
+ 'set_up' => static function ( $handle ) {
+ $args = array(
+ 'in_footer' => true,
+ 'strategy' => 'async',
+ );
+ wp_enqueue_script( $handle, '/footer-async.js', array(), null, $args );
+ },
+ 'group' => 1,
+ 'strategy' => 'async',
+ ),
+
+ // Passing in_footer=true but no strategy.
+ 'blocking_footer_in_args_array' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_register_script( $handle, '/defaults.js', array(), null, array( 'in_footer' => true ) );
+ },
+ 'group' => 1,
+ 'strategy' => false,
+ ),
+
+ // Passing async strategy in script args array.
+ 'async_in_args_array' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_register_script( $handle, '/defaults.js', array(), null, array( 'strategy' => 'async' ) );
+ },
+ 'group' => false,
+ 'strategy' => 'async',
+ ),
+
+ // Passing empty array as 5th arg.
+ 'empty_args_array' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_register_script( $handle, '/defaults.js', array(), null, array() );
+ },
+ 'group' => false,
+ 'strategy' => false,
+ ),
+
+ // Passing no value as 5th arg.
+ 'undefined_args_param' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_register_script( $handle, '/defaults.js', array(), null );
+ },
+ 'group' => false,
+ 'strategy' => false,
+ ),
+
+ // Test backward compatibility, passing $in_footer=true as 5th arg.
+ 'passing_bool_as_args_param' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_enqueue_script( $handle, '/footer-async.js', array(), null, true );
+ },
+ 'group' => 1,
+ 'strategy' => false,
+ ),
+
+ // Test backward compatibility, passing $in_footer=true as 5th arg and setting strategy via wp_script_add_data().
+ 'bool_as_args_and_add_data' => array(
+ 'set_up' => static function ( $handle ) {
+ wp_register_script( $handle, '/footer-async.js', array(), null, true );
+ wp_script_add_data( $handle, 'strategy', 'defer' );
+ },
+ 'group' => 1,
+ 'strategy' => 'defer',
+ ),
+ );
+ }
+
+ /**
+ * Tests that scripts print in the correct group (head/footer) when using in_footer and assigning a strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers ::wp_register_script
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_script_add_data
+ *
+ * @dataProvider get_data_for_test_setting_in_footer_and_strategy
+ *
+ * @param callable $set_up Set up.
+ * @param int|false $expected_group Expected group.
+ * @param string|false $expected_strategy Expected strategy.
+ */
+ public function test_setting_in_footer_and_strategy( $set_up, $expected_group, $expected_strategy ) {
+ $handle = 'foo';
+ $set_up( $handle );
+ $this->assertSame( $expected_group, wp_scripts()->get_data( $handle, 'group' ) );
+ $this->assertSame( $expected_strategy, wp_scripts()->get_data( $handle, 'strategy' ) );
+ }
+
+ /**
+ * Tests that scripts print with no strategy when an incorrect strategy is passed during wp_register_script.
+ *
+ * For an invalid strategy defined during script registration, default to a blocking strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::add_data
+ * @covers ::wp_register_script
+ * @covers ::wp_enqueue_script
+ *
+ * @expectedIncorrectUsage WP_Scripts::add_data
+ */
+ public function test_script_strategy_doing_it_wrong_via_register() {
+ wp_register_script( 'invalid-strategy', '/defaults.js', array(), null, array( 'strategy' => 'random-strategy' ) );
+ wp_enqueue_script( 'invalid-strategy' );
+
+ $this->assertSame(
+ "\n",
+ get_echo( 'wp_print_scripts' )
+ );
+ }
+
+ /**
+ * Tests that scripts print with no strategy when an incorrect strategy is passed via wp_script_add_data().
+ *
+ * For an invalid strategy defined during script registration, default to a blocking strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::add_data
+ * @covers ::wp_script_add_data
+ * @covers ::wp_register_script
+ * @covers ::wp_enqueue_script
+ *
+ * @expectedIncorrectUsage WP_Scripts::add_data
+ */
+ public function test_script_strategy_doing_it_wrong_via_add_data() {
+ wp_register_script( 'invalid-strategy', '/defaults.js', array(), null );
+ wp_script_add_data( 'invalid-strategy', 'strategy', 'random-strategy' );
+ wp_enqueue_script( 'invalid-strategy' );
+
+ $this->assertSame(
+ "\n",
+ get_echo( 'wp_print_scripts' )
+ );
+ }
+
+ /**
+ * Tests that scripts print with no strategy when an incorrect strategy is passed during wp_enqueue_script.
+ *
+ * For an invalid strategy defined during script registration, default to a blocking strategy.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::add_data
+ * @covers ::wp_enqueue_script
+ *
+ * @expectedIncorrectUsage WP_Scripts::add_data
+ */
+ public function test_script_strategy_doing_it_wrong_via_enqueue() {
+ wp_enqueue_script( 'invalid-strategy', '/defaults.js', array(), null, array( 'strategy' => 'random-strategy' ) );
+
+ $this->assertSame(
+ "\n",
+ get_echo( 'wp_print_scripts' )
+ );
+ }
+
+ /**
+ * Tests that scripts registered with a deferred strategy are not included in the script concat loading query.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_register_script
+ */
+ public function test_concatenate_with_defer_strategy() {
+ global $wp_scripts, $concatenate_scripts, $wp_version;
+
+ $old_value = $concatenate_scripts;
+ $concatenate_scripts = true;
+
+ $wp_scripts->do_concat = true;
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
+
+ wp_register_script( 'one-concat-dep', $this->default_scripts_dir . 'script.js' );
+ wp_register_script( 'two-concat-dep', $this->default_scripts_dir . 'script.js' );
+ wp_register_script( 'three-concat-dep', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'main-defer-script', '/main-script.js', array( 'one-concat-dep', 'two-concat-dep', 'three-concat-dep' ), null, array( 'strategy' => 'defer' ) );
+
+ wp_print_scripts();
+ $print_scripts = get_echo( '_print_scripts' );
+
+ // Reset global before asserting.
+ $concatenate_scripts = $old_value;
+
+ $expected = "\n";
+ $expected .= "\n";
+
+ $this->assertSame( $expected, $print_scripts, 'Scripts are being incorrectly concatenated when a main script is registered with a "defer" loading strategy. Deferred scripts should not be part of the script concat loading query.' );
+ }
+
+ /**
+ * Test script concatenation with `async` main script.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_register_script
+ */
+ public function test_concatenate_with_async_strategy() {
+ global $wp_scripts, $concatenate_scripts, $wp_version;
+
+ $old_value = $concatenate_scripts;
+ $concatenate_scripts = true;
+
+ $wp_scripts->do_concat = true;
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
+
+ wp_enqueue_script( 'one-concat-dep-1', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'two-concat-dep-1', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'three-concat-dep-1', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'main-async-script-1', '/main-script.js', array(), null, array( 'strategy' => 'async' ) );
+
+ wp_print_scripts();
+ $print_scripts = get_echo( '_print_scripts' );
+
+ // Reset global before asserting.
+ $concatenate_scripts = $old_value;
+
+ $expected = "\n";
+ $expected .= "\n";
+
+ $this->assertSame( $expected, $print_scripts, 'Scripts are being incorrectly concatenated when a main script is registered with an "async" loading strategy. Async scripts should not be part of the script concat loading query.' );
+ }
+
+ /**
+ * Tests that script concatenation remains correct when a main script is registered as deferred after other blocking
+ * scripts are registered.
+ *
+ * @ticket 12009
+ *
+ * @covers WP_Scripts::do_item
+ * @covers ::wp_enqueue_script
+ * @covers ::wp_register_script
+ */
+ public function test_concatenate_with_blocking_script_before_and_after_script_with_defer_strategy() {
+ global $wp_scripts, $concatenate_scripts, $wp_version;
+
+ $old_value = $concatenate_scripts;
+ $concatenate_scripts = true;
+
+ $wp_scripts->do_concat = true;
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
+
+ wp_enqueue_script( 'one-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'two-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'three-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'deferred-script-2', '/main-script.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'four-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'five-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'six-concat-dep-2', $this->default_scripts_dir . 'script.js' );
+
+ wp_print_scripts();
+ $print_scripts = get_echo( '_print_scripts' );
+
+ // Reset global before asserting.
+ $concatenate_scripts = $old_value;
+
+ $expected = "\n";
+ $expected .= "\n";
+
+ $this->assertSame( $expected, $print_scripts, 'Scripts are being incorrectly concatenated when a main script is registered as deferred after other blocking scripts are registered. Deferred scripts should not be part of the script concat loader query string. ' );
+ }
+
/**
* @ticket 42804
*/
public function test_wp_enqueue_script_with_html5_support_does_not_contain_type_attribute() {
+ global $wp_version;
add_theme_support( 'html5', array( 'script' ) );
$GLOBALS['wp_scripts'] = new WP_Scripts();
@@ -74,8 +1419,7 @@ JS;
wp_enqueue_script( 'empty-deps-no-version', 'example.com' );
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
+ $expected = "\n";
$this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
}
@@ -83,37 +1427,37 @@ JS;
/**
* Test the different protocol references in wp_enqueue_script
*
- * @global WP_Scripts $wp_scripts
* @ticket 16560
+ *
+ * @global WP_Scripts $wp_scripts
*/
public function test_protocols() {
// Init.
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
$base_url_backup = $wp_scripts->base_url;
$wp_scripts->base_url = 'http://example.com/wordpress';
$expected = '';
- $ver = get_bloginfo( 'version' );
// Try with an HTTP reference.
wp_enqueue_script( 'jquery-http', 'http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js' );
- $expected .= "\n";
+ $expected .= "\n";
// Try with an HTTPS reference.
wp_enqueue_script( 'jquery-https', 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js' );
- $expected .= "\n";
+ $expected .= "\n";
// Try with an automatic protocol reference (//).
wp_enqueue_script( 'jquery-doubleslash', '//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js' );
- $expected .= "\n";
+ $expected .= "\n";
// Try with a local resource and an automatic protocol reference (//).
$url = '//my_plugin/script.js';
wp_enqueue_script( 'plugin-script', $url );
- $expected .= "\n";
+ $expected .= "\n";
// Try with a bad protocol.
wp_enqueue_script( 'jquery-ftp', 'ftp://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js' );
- $expected .= "\n";
+ $expected .= "\n";
// Go!
$this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
@@ -129,20 +1473,19 @@ JS;
* Test script concatenation.
*/
public function test_script_concatenation() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
$wp_scripts->do_concat = true;
- $wp_scripts->default_dirs = array( '/directory/' );
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
- wp_enqueue_script( 'one', '/directory/script.js' );
- wp_enqueue_script( 'two', '/directory/script.js' );
- wp_enqueue_script( 'three', '/directory/script.js' );
+ wp_enqueue_script( 'one', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'two', $this->default_scripts_dir . 'script.js' );
+ wp_enqueue_script( 'three', $this->default_scripts_dir . 'script.js' );
wp_print_scripts();
$print_scripts = get_echo( '_print_scripts' );
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
+ $expected = "\n";
$this->assertSame( $expected, $print_scripts );
}
@@ -205,7 +1548,7 @@ JS;
}
/**
- * Testing `wp_script_add_data` with an anvalid key.
+ * Testing `wp_script_add_data` with an invalid key.
*
* @ticket 16024
*/
@@ -333,8 +1676,8 @@ JS;
$expected_header .= "\n";
$expected_footer = "\n";
- $this->assertSame( $expected_header, $header );
- $this->assertSame( $expected_footer, $footer );
+ $this->assertSame( $expected_header, $header, 'Expected same header markup.' );
+ $this->assertSame( $expected_footer, $footer, 'Expected same footer markup.' );
}
/**
@@ -354,8 +1697,8 @@ JS;
$expected_footer = "\n";
$expected_footer .= "\n";
- $this->assertSame( $expected_header, $header );
- $this->assertSame( $expected_footer, $footer );
+ $this->assertSame( $expected_header, $header, 'Expected same header markup.' );
+ $this->assertSame( $expected_footer, $footer, 'Expected same footer markup.' );
}
/**
@@ -385,8 +1728,8 @@ JS;
$expected_footer .= "\n";
$expected_footer .= "\n";
- $this->assertSame( $expected_header, $header );
- $this->assertSame( $expected_footer, $footer );
+ $this->assertSame( $expected_header, $header, 'Expected same header markup.' );
+ $this->assertSame( $expected_footer, $footer, 'Expected same footer markup.' );
}
/**
@@ -416,7 +1759,7 @@ JS;
$expected = "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -429,7 +1772,7 @@ JS;
$expected = "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -444,7 +1787,7 @@ JS;
$expected .= "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -457,7 +1800,7 @@ JS;
$expected = "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -470,7 +1813,7 @@ JS;
$expected = "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -485,7 +1828,7 @@ JS;
$expected = "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -502,7 +1845,7 @@ JS;
$expected .= "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -519,85 +1862,82 @@ JS;
$expected .= "\n";
$expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
* @ticket 14853
*/
public function test_wp_add_inline_script_before_with_concat() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
$wp_scripts->do_concat = true;
- $wp_scripts->default_dirs = array( '/directory/' );
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
- wp_enqueue_script( 'one', '/directory/one.js' );
- wp_enqueue_script( 'two', '/directory/two.js' );
- wp_enqueue_script( 'three', '/directory/three.js' );
+ wp_enqueue_script( 'one', $this->default_scripts_dir . 'one.js' );
+ wp_enqueue_script( 'two', $this->default_scripts_dir . 'two.js' );
+ wp_enqueue_script( 'three', $this->default_scripts_dir . 'three.js' );
wp_add_inline_script( 'one', 'console.log("before one");', 'before' );
wp_add_inline_script( 'two', 'console.log("before two");', 'before' );
- $ver = get_bloginfo( 'version' );
$expected = "\n";
- $expected .= "\n";
+ $expected .= "\n";
$expected .= "\n";
- $expected .= "\n";
- $expected .= "\n";
+ $expected .= "\n";
+ $expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
* @ticket 14853
*/
public function test_wp_add_inline_script_before_with_concat2() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
$wp_scripts->do_concat = true;
- $wp_scripts->default_dirs = array( '/directory/' );
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
- wp_enqueue_script( 'one', '/directory/one.js' );
- wp_enqueue_script( 'two', '/directory/two.js' );
- wp_enqueue_script( 'three', '/directory/three.js' );
+ wp_enqueue_script( 'one', $this->default_scripts_dir . 'one.js' );
+ wp_enqueue_script( 'two', $this->default_scripts_dir . 'two.js' );
+ wp_enqueue_script( 'three', $this->default_scripts_dir . 'three.js' );
wp_add_inline_script( 'one', 'console.log("before one");', 'before' );
- $ver = get_bloginfo( 'version' );
$expected = "\n";
- $expected .= "\n";
- $expected .= "\n";
- $expected .= "\n";
+ $expected .= "\n";
+ $expected .= "\n";
+ $expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
* @ticket 14853
*/
public function test_wp_add_inline_script_after_with_concat() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
$wp_scripts->do_concat = true;
- $wp_scripts->default_dirs = array( '/directory/' );
+ $wp_scripts->default_dirs = array( $this->default_scripts_dir );
- wp_enqueue_script( 'one', '/directory/one.js' );
- wp_enqueue_script( 'two', '/directory/two.js' );
- wp_enqueue_script( 'three', '/directory/three.js' );
- wp_enqueue_script( 'four', '/directory/four.js' );
+ wp_enqueue_script( 'one', $this->default_scripts_dir . 'one.js' );
+ wp_enqueue_script( 'two', $this->default_scripts_dir . 'two.js' );
+ wp_enqueue_script( 'three', $this->default_scripts_dir . 'three.js' );
+ wp_enqueue_script( 'four', $this->default_scripts_dir . 'four.js' );
wp_add_inline_script( 'two', 'console.log("after two");' );
wp_add_inline_script( 'three', 'console.log("after three");' );
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
- $expected .= "\n";
+ $expected = "\n";
+ $expected .= "\n";
$expected .= "\n";
- $expected .= "\n";
+ $expected .= "\n";
$expected .= "\n";
- $expected .= "\n";
+ $expected .= "\n";
- $this->assertSame( $expected, get_echo( 'wp_print_scripts' ) );
+ $this->assertEqualMarkup( $expected, get_echo( 'wp_print_scripts' ) );
}
/**
@@ -626,7 +1966,7 @@ JS;
wp_script_add_data( 'test-example', 'conditional', 'gte IE 9' );
$this->assertSame( $expected_localized, get_echo( 'wp_print_scripts' ) );
- $this->assertSame( $expected, $wp_scripts->print_html );
+ $this->assertEqualMarkup( $expected, $wp_scripts->print_html );
$this->assertTrue( $wp_scripts->do_concat );
}
@@ -634,15 +1974,14 @@ JS;
* @ticket 36392
*/
public function test_wp_add_inline_script_after_with_concat_and_core_dependency() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
wp_default_scripts( $wp_scripts );
$wp_scripts->base_url = '';
$wp_scripts->do_concat = true;
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
+ $expected = "\n";
$expected .= "\n";
$expected .= "\n";
@@ -652,22 +1991,21 @@ JS;
wp_print_scripts();
$print_scripts = get_echo( '_print_scripts' );
- $this->assertSame( $expected, $print_scripts );
+ $this->assertEqualMarkup( $expected, $print_scripts );
}
/**
* @ticket 36392
*/
public function test_wp_add_inline_script_after_with_concat_and_conditional_and_core_dependency() {
- global $wp_scripts;
+ global $wp_scripts, $wp_version;
wp_default_scripts( $wp_scripts );
$wp_scripts->base_url = '';
$wp_scripts->do_concat = true;
- $ver = get_bloginfo( 'version' );
- $expected = "\n";
+ $expected = "\n";
$expected .= "