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%s\n\n", $this->type_attr, esc_attr( $handle ), $before_handle ); - } - - if ( $after_handle ) { - $after_handle = sprintf( "\n%s\n\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%s\n\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 .= "