Make WordPress Core

Changeset 60999


Ignore:
Timestamp:
10/21/2025 12:43:51 AM (5 weeks ago)
Author:
westonruter
Message:

Script Loader: Add support for printing script modules at wp_footer.

This brings API parity with WP_Scripts by implementing opt-in support for printing in the footer via an in_footer argument. This argument can be supplied via the $args array passed to wp_enqueue_script_module() or wp_register_script_module(), alongside the existing fetchpriority key introduced in #61734. It can also be set for previously-registered script modules via WP_Script_Modules::set_in_footer(). This is not applicable to classic themes since modules are enqueued while blocks are rendered after wp_head has completed, so all script modules are printed in the footer anyway; the importmap script must be printed after all script modules have been enqueued.

Script modules used for interactive blocks (with the Interactivity API) are automatically printed in the footer. Such script modules should be deprioritized because they are not in the critical rendering path due to interactive blocks using server-side rendering. Script modules remain printed at wp_head by default, although this default should be revisited since they have deferred execution (and they are printed in the footer for classic themes already, as previously noted). Moving a script module to the footer ensures that its loading does not contend with the loading of critical resources, such as the LCP element's image resource, and LCP is improved as a result.

This also improves specificity of some PHP types, it ensures that script modules can't be registered with an empty ID, and it prevents printing script modules with empty src URLs.

Developed in https://github.com/WordPress/wordpress-develop/pull/9867

Follow-up to [60704].

Props b1ink0, westonruter, jonsurrell, peterwilsoncc, vipulpatil, mindctrl.
See #61734.
Fixes #63486.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/blocks.php

    r60939 r60999  
    183183    ) {
    184184        $args['fetchpriority'] = 'low';
     185        $args['in_footer']     = true;
    185186    }
    186187
  • trunk/src/wp-includes/class-wp-script-modules.php

    r60951 r60999  
    1919     *
    2020     * @since 6.5.0
    21      * @var array[]
     21     * @var array<string, array<string, mixed>>
    2222     */
    2323    private $registered = array();
     
    3030     */
    3131    private $queue = array();
     32
     33    /**
     34     * Holds the script module identifiers that have been printed.
     35     *
     36     * @since 6.9.0
     37     * @var string[]
     38     */
     39    private $done = array();
    3240
    3341    /**
     
    5058     */
    5159    private $dependents_map = array();
     60
     61    /**
     62     * Holds the valid values for fetchpriority.
     63     *
     64     * @since 6.9.0
     65     * @var string[]
     66     */
     67    private $priorities = array(
     68        'low',
     69        'auto',
     70        'high',
     71    );
    5272
    5373    /**
     
    85105     *     Optional. An array of additional args. Default empty array.
    86106     *
     107     *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
    87108     *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
    88109     * }
    89110     */
    90111    public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
     112        if ( '' === $id ) {
     113            _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
     114            return;
     115        }
     116
    91117        if ( ! isset( $this->registered[ $id ] ) ) {
    92118            $dependencies = array();
     
    111137            }
    112138
     139            $in_footer = isset( $args['in_footer'] ) && (bool) $args['in_footer'];
     140
    113141            $fetchpriority = 'auto';
    114142            if ( isset( $args['fetchpriority'] ) ) {
     
    133161                'version'       => $version,
    134162                'dependencies'  => $dependencies,
     163                'in_footer'     => $in_footer,
    135164                'fetchpriority' => $fetchpriority,
    136165            );
     
    158187     */
    159188    private function is_valid_fetchpriority( $priority ): bool {
    160         return in_array( $priority, array( 'auto', 'low', 'high' ), true );
     189        return in_array( $priority, $this->priorities, true );
    161190    }
    162191
     
    190219
    191220        $this->registered[ $id ]['fetchpriority'] = $priority;
     221        return true;
     222    }
     223
     224    /**
     225     * Sets whether a script module should be printed in the footer.
     226     *
     227     * This is only relevant in block themes.
     228     *
     229     * @since 6.9.0
     230     *
     231     * @param string           $id        Script module identifier.
     232     * @param bool             $in_footer Whether to print in the footer.
     233     * @return bool Whether setting the printing location was successful.
     234     */
     235    public function set_in_footer( string $id, bool $in_footer ): bool {
     236        if ( ! isset( $this->registered[ $id ] ) ) {
     237            return false;
     238        }
     239        $this->registered[ $id ]['in_footer'] = $in_footer;
    192240        return true;
    193241    }
     
    229277     *     Optional. An array of additional args. Default empty array.
    230278     *
     279     *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
    231280     *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
    232281     * }
    233282     */
    234283    public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
     284        if ( '' === $id ) {
     285            _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
     286            return;
     287        }
     288
    235289        if ( ! in_array( $id, $this->queue, true ) ) {
    236290            $this->queue[] = $id;
     
    275329     */
    276330    public function add_hooks() {
    277         $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer';
     331        $is_block_theme = wp_is_block_theme();
     332        $position       = $is_block_theme ? 'wp_head' : 'wp_footer';
    278333        add_action( $position, array( $this, 'print_import_map' ) );
    279         add_action( $position, array( $this, 'print_enqueued_script_modules' ) );
     334        if ( $is_block_theme ) {
     335            /*
     336             * Modules can only be printed in the head for block themes because only with
     337             * block themes will import map be fully populated by modules discovered by
     338             * rendering the block template. In classic themes, modules are enqueued during
     339             * template rendering, thus the import map must be printed in the footer,
     340             * followed by all enqueued modules.
     341             */
     342            add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) );
     343        }
     344        add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) );
    280345        add_action( $position, array( $this, 'print_script_module_preloads' ) );
    281346
     
    296361     *
    297362     * @param string[] $ids Script module IDs.
    298      * @return string Highest fetch priority for the provided script module IDs.
     363     * @return 'auto'|'low'|'high' Highest fetch priority for the provided script module IDs.
    299364     */
    300365    private function get_highest_fetchpriority( array $ids ): string {
    301         static $priorities   = array(
    302             'low',
    303             'auto',
    304             'high',
    305         );
    306         $high_priority_index = count( $priorities ) - 1;
     366        static $high_priority_index = null;
     367        if ( null === $high_priority_index ) {
     368            $high_priority_index = count( $this->priorities ) - 1;
     369        }
    307370
    308371        $highest_priority_index = 0;
    309372        foreach ( $ids as $id ) {
    310373            if ( isset( $this->registered[ $id ] ) ) {
    311                 $highest_priority_index = max(
     374                $highest_priority_index = (int) max(
    312375                    $highest_priority_index,
    313                     array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true )
     376                    (int) array_search( $this->registered[ $id ]['fetchpriority'], $this->priorities, true )
    314377                );
    315378                if ( $high_priority_index === $highest_priority_index ) {
     
    319382        }
    320383
    321         return $priorities[ $highest_priority_index ];
    322     }
    323 
    324     /**
    325      * Prints the enqueued script modules using script tags with type="module"
     384        return $this->priorities[ $highest_priority_index ];
     385    }
     386
     387    /**
     388     * Prints the enqueued script modules in head.
     389     *
     390     * This is only used in block themes.
     391     *
     392     * @since 6.9.0
     393     */
     394    public function print_head_enqueued_script_modules() {
     395        foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
     396            if (
     397                isset( $this->registered[ $id ] ) &&
     398                ! $this->registered[ $id ]['in_footer']
     399            ) {
     400                // If any dependency is set to be printed in footer, skip printing this module in head.
     401                $dependencies = $this->get_dependencies( array( $id ) );
     402                foreach ( $dependencies as $dependency_id ) {
     403                    if (
     404                        in_array( $dependency_id, $this->queue, true ) &&
     405                        isset( $this->registered[ $dependency_id ] ) &&
     406                        $this->registered[ $dependency_id ]['in_footer']
     407                    ) {
     408                        continue 2;
     409                    }
     410                }
     411                $this->print_script_module( $id );
     412            }
     413        }
     414    }
     415
     416    /**
     417     * Prints the enqueued script modules in footer.
     418     *
     419     * @since 6.5.0
     420     */
     421    public function print_enqueued_script_modules() {
     422        foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
     423            $this->print_script_module( $id );
     424        }
     425    }
     426
     427    /**
     428     * Prints the enqueued script module using script tags with type="module"
    326429     * attributes.
    327430     *
    328      * @since 6.5.0
    329      */
    330     public function print_enqueued_script_modules() {
    331         foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
    332             $args = array(
    333                 'type' => 'module',
    334                 'src'  => $this->get_src( $id ),
    335                 'id'   => $id . '-js-module',
    336             );
    337 
    338             $dependents    = $this->get_recursive_dependents( $id );
    339             $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
    340             if ( 'auto' !== $fetchpriority ) {
    341                 $args['fetchpriority'] = $fetchpriority;
    342             }
    343             if ( $fetchpriority !== $script_module['fetchpriority'] ) {
    344                 $args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
    345             }
    346             wp_print_script_tag( $args );
    347         }
     431     * @since 6.9.0
     432     *
     433     * @param string $id The script module identifier.
     434     */
     435    private function print_script_module( string $id ) {
     436        if ( in_array( $id, $this->done, true ) || ! in_array( $id, $this->queue, true ) ) {
     437            return;
     438        }
     439
     440        $this->done[] = $id;
     441
     442        $src = $this->get_src( $id );
     443        if ( '' === $src ) {
     444            return;
     445        }
     446
     447        $attributes = array(
     448            'type' => 'module',
     449            'src'  => $src,
     450            'id'   => $id . '-js-module',
     451        );
     452
     453        $script_module = $this->registered[ $id ];
     454        $dependents    = $this->get_recursive_dependents( $id );
     455        $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
     456        if ( 'auto' !== $fetchpriority ) {
     457            $attributes['fetchpriority'] = $fetchpriority;
     458        }
     459        if ( $fetchpriority !== $script_module['fetchpriority'] ) {
     460            $attributes['data-wp-fetchpriority'] = $script_module['fetchpriority'];
     461        }
     462        wp_print_script_tag( $attributes );
    348463    }
    349464
     
    357472     */
    358473    public function print_script_module_preloads() {
    359         foreach ( $this->get_dependencies( array_unique( $this->queue ), array( 'static' ) ) as $id => $script_module ) {
     474        $dependency_ids = $this->get_sorted_dependencies( $this->queue, array( 'static' ) );
     475        foreach ( $dependency_ids as $id ) {
    360476            // Don't preload if it's marked for enqueue.
    361             if ( ! in_array( $id, $this->queue, true ) ) {
    362                 $enqueued_dependents   = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
    363                 $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
    364                 printf(
    365                     '<link rel="modulepreload" href="%s" id="%s"',
    366                     esc_url( $this->get_src( $id ) ),
    367                     esc_attr( $id . '-js-modulepreload' )
    368                 );
    369                 if ( 'auto' !== $highest_fetchpriority ) {
    370                     printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
    371                 }
    372                 if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) {
    373                     printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) );
    374                 }
    375                 echo ">\n";
    376             }
     477            if ( in_array( $id, $this->queue, true ) ) {
     478                continue;
     479            }
     480
     481            $src = $this->get_src( $id );
     482            if ( '' === $src ) {
     483                continue;
     484            }
     485
     486            $enqueued_dependents   = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
     487            $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
     488            printf(
     489                '<link rel="modulepreload" href="%s" id="%s"',
     490                esc_url( $src ),
     491                esc_attr( $id . '-js-modulepreload' )
     492            );
     493            if ( 'auto' !== $highest_fetchpriority ) {
     494                printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
     495            }
     496            if ( $highest_fetchpriority !== $this->registered[ $id ]['fetchpriority'] && 'auto' !== $this->registered[ $id ]['fetchpriority'] ) {
     497                printf( ' data-wp-fetchpriority="%s"', esc_attr( $this->registered[ $id ]['fetchpriority'] ) );
     498            }
     499            echo ">\n";
    377500        }
    378501    }
     
    387510        if ( ! empty( $import_map['imports'] ) ) {
    388511            wp_print_inline_script_tag(
    389                 wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
     512                (string) wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
    390513                array(
    391514                    'type' => 'importmap',
     
    406529    private function get_import_map(): array {
    407530        $imports = array();
    408         foreach ( $this->get_dependencies( array_unique( $this->queue ) ) as $id => $script_module ) {
    409             $imports[ $id ] = $this->get_src( $id );
     531        foreach ( $this->get_dependencies( $this->queue ) as $id ) {
     532            $src = $this->get_src( $id );
     533            if ( '' !== $src ) {
     534                $imports[ $id ] = $src;
     535            }
    410536        }
    411537        return array( 'imports' => $imports );
     
    414540    /**
    415541     * Retrieves the list of script modules marked for enqueue.
     542     *
     543     * Even though this is a private method and is unused in core, there are ecosystem plugins accessing it via the
     544     * Reflection API. The ecosystem should rather use {@see self::get_queue()}.
    416545     *
    417546     * @since 6.5.0
     
    427556
    428557    /**
    429      * Retrieves all the dependencies for the given script module identifiers,
    430      * filtered by import types.
     558     * Retrieves all the dependencies for the given script module identifiers, filtered by import types.
    431559     *
    432560     * It will consolidate an array containing a set of unique dependencies based
     
    438566     * @param string[] $ids          The identifiers of the script modules for which to gather dependencies.
    439567     * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
    440      *                               Default is both.
    441      * @return array[] List of dependencies, keyed by script module identifier.
     568     *                                         Default is both.
     569     * @return string[] List of IDs for script module dependencies.
    442570     */
    443571    private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
    444         return array_reduce(
    445             $ids,
    446             function ( $dependency_script_modules, $id ) use ( $import_types ) {
    447                 $dependencies = array();
    448                 if ( isset( $this->registered[ $id ] ) ) {
    449                     foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
    450                         if (
    451                             in_array( $dependency['import'], $import_types, true ) &&
    452                             isset( $this->registered[ $dependency['id'] ] ) &&
    453                             ! isset( $dependency_script_modules[ $dependency['id'] ] )
    454                         ) {
    455                             $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
    456                         }
    457                     }
     572        $all_dependencies = array();
     573        $id_queue         = $ids;
     574
     575        while ( ! empty( $id_queue ) ) {
     576            $id = array_shift( $id_queue );
     577            if ( ! isset( $this->registered[ $id ] ) ) {
     578                continue;
     579            }
     580
     581            foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
     582                if (
     583                    ! isset( $all_dependencies[ $dependency['id'] ] ) &&
     584                    in_array( $dependency['import'], $import_types, true ) &&
     585                    isset( $this->registered[ $dependency['id'] ] )
     586                ) {
     587                    $all_dependencies[ $dependency['id'] ] = true;
     588
     589                    // Add this dependency to the list to get dependencies for.
     590                    $id_queue[] = $dependency['id'];
    458591                }
    459                 return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) );
    460             },
    461             array()
    462         );
     592            }
     593        }
     594
     595        return array_keys( $all_dependencies );
    463596    }
    464597
     
    507640     */
    508641    private function get_recursive_dependents( string $id ): array {
    509         $get = function ( string $id, array $checked = array() ) use ( &$get ): array {
    510 
    511             // If by chance an unregistered script module is checked or there is a recursive dependency, return early.
    512             if ( ! isset( $this->registered[ $id ] ) || isset( $checked[ $id ] ) ) {
    513                 return array();
    514             }
    515 
    516             // Mark this script module as checked to guard against infinite recursion.
    517             $checked[ $id ] = true;
    518 
    519             $dependents = array();
    520             foreach ( $this->get_dependents( $id ) as $dependent ) {
    521                 $dependents = array_merge(
    522                     $dependents,
    523                     array( $dependent ),
    524                     $get( $dependent, $checked )
    525                 );
    526             }
    527 
    528             return $dependents;
    529         };
    530 
    531         return array_unique( $get( $id ) );
     642        $dependents = array();
     643        $id_queue   = array( $id );
     644        $processed  = array();
     645
     646        while ( ! empty( $id_queue ) ) {
     647            $current_id = array_shift( $id_queue );
     648
     649            // Skip unregistered or already-processed script modules.
     650            if ( ! isset( $this->registered[ $current_id ] ) || isset( $processed[ $current_id ] ) ) {
     651                continue;
     652            }
     653
     654            // Mark as processed to guard against infinite loops from circular dependencies.
     655            $processed[ $current_id ] = true;
     656
     657            // Find the direct dependents of the current script.
     658            foreach ( $this->get_dependents( $current_id ) as $dependent_id ) {
     659                // Only add the dependent if we haven't found it before.
     660                if ( ! isset( $dependents[ $dependent_id ] ) ) {
     661                    $dependents[ $dependent_id ] = true;
     662
     663                    // Add dependency to the queue.
     664                    $id_queue[] = $dependent_id;
     665                }
     666            }
     667        }
     668
     669        return array_keys( $dependents );
     670    }
     671
     672    /**
     673     * Sorts the given script module identifiers based on their dependencies.
     674     *
     675     * It will return a list of script module identifiers sorted in the order
     676     * they should be printed, so that dependencies are printed before the script
     677     * modules that depend on them.
     678     *
     679     * @since 6.9.0
     680     *
     681     * @param string[] $ids          The identifiers of the script modules to sort.
     682     * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
     683     *                                         Default is both.
     684     * @return string[] Sorted list of script module identifiers.
     685     */
     686    private function get_sorted_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
     687        $sorted = array();
     688
     689        foreach ( $ids as $id ) {
     690            $this->sort_item_dependencies( $id, $import_types, $sorted );
     691        }
     692
     693        return array_unique( $sorted );
     694    }
     695
     696    /**
     697     * Recursively sorts the dependencies for a single script module identifier.
     698     *
     699     * @since 6.9.0
     700     *
     701     * @param string   $id           The identifier of the script module to sort.
     702     * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
     703     * @param string[] &$sorted      The array of sorted identifiers, passed by reference.
     704     * @return bool True on success, false on failure (e.g., missing dependency).
     705     */
     706    private function sort_item_dependencies( string $id, array $import_types, array &$sorted ): bool {
     707        // If already processed, don't do it again.
     708        if ( in_array( $id, $sorted, true ) ) {
     709            return true;
     710        }
     711
     712        // If the item doesn't exist, fail.
     713        if ( ! isset( $this->registered[ $id ] ) ) {
     714            return false;
     715        }
     716
     717        $dependency_ids = array();
     718        foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
     719            if ( in_array( $dependency['import'], $import_types, true ) ) {
     720                $dependency_ids[] = $dependency['id'];
     721            }
     722        }
     723
     724        // If the item requires dependencies that do not exist, fail.
     725        if ( count( array_diff( $dependency_ids, array_keys( $this->registered ) ) ) > 0 ) {
     726            return false;
     727        }
     728
     729        // Recursively process dependencies.
     730        foreach ( $dependency_ids as $dependency_id ) {
     731            if ( ! $this->sort_item_dependencies( $dependency_id, $import_types, $sorted ) ) {
     732                // A dependency failed to resolve, so this branch fails.
     733                return false;
     734            }
     735        }
     736
     737        // All dependencies are sorted, so we can now add the current item.
     738        $sorted[] = $id;
     739
     740        return true;
    532741    }
    533742
     
    552761        $src           = $script_module['src'];
    553762
    554         if ( false === $script_module['version'] ) {
    555             $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
    556         } elseif ( null !== $script_module['version'] ) {
    557             $src = add_query_arg( 'ver', $script_module['version'], $src );
     763        if ( '' !== $src ) {
     764            if ( false === $script_module['version'] ) {
     765                $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
     766            } elseif ( null !== $script_module['version'] ) {
     767                $src = add_query_arg( 'ver', $script_module['version'], $src );
     768            }
    558769        }
    559770
     
    567778         */
    568779        $src = apply_filters( 'script_module_loader_src', $src, $id );
     780        if ( ! is_string( $src ) ) {
     781            $src = '';
     782        }
    569783
    570784        return $src;
     
    682896
    683897                wp_print_inline_script_tag(
    684                     wp_json_encode(
     898                    (string) wp_json_encode(
    685899                        $data,
    686900                        $json_encode_flags
  • trunk/src/wp-includes/script-modules.php

    r60931 r60999  
    6565 *     Optional. An array of additional args. Default empty array.
    6666 *
     67 *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
    6768 *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
    6869 * }
     
    108109 *     Optional. An array of additional args. Default empty array.
    109110 *
     111 *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
    110112 *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
    111113 * }
     
    184186        /*
    185187         * The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules
    186          * should be loaded with low fetchpriority since they should not be needed in the critical rendering path.
    187          * Also, the @wordpress/a11y script module is intended to be used as a dynamic import dependency, in which case
    188          * the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
     188         * should be loaded with low fetchpriority and printed in the footer since they should not be needed in the
     189         * critical rendering path. Also, the @wordpress/a11y script module is intended to be used as a dynamic import
     190         * dependency, in which case the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
    189191         * However, in case it is added as a static import dependency, the fetchpriority is explicitly set to be 'low'
    190192         * since the module should not be involved in the critical rendering path, and if it is, its fetchpriority will
     
    198200        ) {
    199201            $args['fetchpriority'] = 'low';
     202            $args['in_footer']     = true;
    200203        }
    201204
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60951 r60999  
    5757     */
    5858    public function get_enqueued_script_modules(): array {
    59         $modules = array();
    60 
    61         $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ) );
    62         while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
    63             $this->assertSame( 'module', $p->get_attribute( 'type' ) );
    64             $this->assertIsString( $p->get_attribute( 'id' ) );
    65             $this->assertIsString( $p->get_attribute( 'src' ) );
    66             $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) );
    67 
    68             $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
    69             $fetchpriority  = $p->get_attribute( 'fetchpriority' );
    70             $modules[ $id ] = array_merge(
    71                 array(
    72                     'url'           => $p->get_attribute( 'src' ),
    73                     'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
    74                 ),
    75                 ...array_map(
    76                     static function ( $attribute_name ) use ( $p ) {
    77                         return array( $attribute_name => $p->get_attribute( $attribute_name ) );
    78                     },
    79                     $p->get_attribute_names_with_prefix( 'data-' )
    80                 )
    81             );
    82         }
     59        $get_modules = function ( string $html, bool $in_footer ): array {
     60            $modules = array();
     61            $p       = new WP_HTML_Tag_Processor( $html );
     62            while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) {
     63                $this->assertSame( 'module', $p->get_attribute( 'type' ) );
     64                $this->assertIsString( $p->get_attribute( 'id' ) );
     65                $this->assertIsString( $p->get_attribute( 'src' ) );
     66                $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) );
     67
     68                $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
     69                $fetchpriority  = $p->get_attribute( 'fetchpriority' );
     70                $modules[ $id ] = array_merge(
     71                    array(
     72                        'url'           => $p->get_attribute( 'src' ),
     73                        'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     74                        'in_footer'     => $in_footer,
     75                    ),
     76                    ...array_map(
     77                        static function ( $attribute_name ) use ( $p ) {
     78                            return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     79                        },
     80                        $p->get_attribute_names_with_prefix( 'data-' )
     81                    )
     82                );
     83            }
     84            return $modules;
     85        };
     86
     87        $modules = array_merge(
     88            $get_modules( get_echo( array( $this->script_modules, 'print_head_enqueued_script_modules' ) ), false ),
     89            $get_modules( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ), true )
     90        );
    8391
    8492        return $modules;
     
    148156
    149157    /**
     158     * Test wp_register_script_module() with empty ID.
     159     *
     160     * @ticket 63486
     161     *
     162     * @expectedIncorrectUsage WP_Script_Modules::register
     163     *
     164     * @covers ::wp_register_script_module
     165     * @covers WP_Script_Modules::register
     166     */
     167    public function test_register_with_empty_id() {
     168        wp_register_script_module( '', '/null-and-void.js' );
     169        $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) );
     170    }
     171
     172    /**
     173     * Test wp_enqueue_script_module() with empty ID.
     174     *
     175     * @ticket 63486
     176     *
     177     * @expectedIncorrectUsage WP_Script_Modules::enqueue
     178     *
     179     * @covers ::wp_enqueue_script_module
     180     * @covers WP_Script_Modules::enqueue
     181     */
     182    public function test_enqueue_with_empty_id() {
     183        wp_enqueue_script_module( '', '/null-and-void.js' );
     184        $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) );
     185        $this->assertNotContains( '', wp_script_modules()->get_queue() );
     186    }
     187
     188    /**
    150189     * Tests various ways of registering, enqueueing, dequeuing, and deregistering a script module.
    151190     *
     
    153192     *
    154193     * @ticket 56313
     194     * @ticket 63486
    155195     *
    156196     * @dataProvider data_test_register_and_enqueue_script_module
     
    164204     * @covers ::wp_deregister_script_module()
    165205     * @covers WP_Script_Modules::deregister()
     206     * @covers WP_Script_Modules::get_queue()
     207     * @covers WP_Script_Modules::get_marked_for_enqueue()
    166208     * @covers WP_Script_Modules::set_fetchpriority()
     209     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    167210     * @covers WP_Script_Modules::print_enqueued_script_modules()
    168211     * @covers WP_Script_Modules::print_import_map()
     
    180223            }
    181224        };
     225
     226        $reflection_class       = new ReflectionClass( wp_script_modules() );
     227        $get_marked_for_enqueue = $reflection_class->getMethod( 'get_marked_for_enqueue' );
     228        if ( PHP_VERSION_ID < 80100 ) {
     229            $get_marked_for_enqueue->setAccessible( true );
     230        }
    182231
    183232        $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) {
     
    201250        // Minimal args.
    202251        $register_and_enqueue( 'a', '/a.js' );
     252        $this->assertSame( array( 'a' ), wp_script_modules()->get_queue(), 'Expected queue to match.' );
     253        $marked_for_enqueue = $get_marked_for_enqueue->invoke( wp_script_modules() );
     254        $this->assertSame( wp_script_modules()->get_queue(), array_keys( $marked_for_enqueue ), 'Expected get_queue() to match keys returned by get_marked_for_enqueue().' );
     255        $this->assertIsArray( $marked_for_enqueue['a'], 'Expected script module "a" to have an array entry.' );
     256        $this->assertSame( '/a.js', $marked_for_enqueue['a']['src'], 'Expected script module "a" to have the given src.' );
    203257
    204258        // One Dependency.
     
    206260        $register_and_enqueue( 'b', '/b.js', array( 'b-dep' ) );
    207261        $this->assertTrue( wp_script_modules()->set_fetchpriority( 'b', 'low' ) );
     262        $this->assertSame( array( 'a', 'b' ), wp_script_modules()->get_queue() );
    208263
    209264        // Two dependencies with different formats and a false version.
     
    222277            false
    223278        );
     279        $this->assertSame( array( 'a', 'b', 'c' ), wp_script_modules()->get_queue() );
    224280
    225281        // Two dependencies, one imported statically and the other dynamically, with a null version.
     
    241297            null
    242298        );
     299        $this->assertSame( array( 'a', 'b', 'c', 'd' ), wp_script_modules()->get_queue() );
    243300
    244301        // No dependencies, with a string version version.
     
    249306            '1.0.0'
    250307        );
     308        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e' ), wp_script_modules()->get_queue() );
    251309
    252310        // No dependencies, with a string version and fetch priority.
     
    258316            array( 'fetchpriority' => 'auto' )
    259317        );
     318        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f' ), wp_script_modules()->get_queue() );
    260319
    261320        // No dependencies, with a string version and fetch priority of low.
     
    267326            array( 'fetchpriority' => 'low' )
    268327        );
     328        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g' ), wp_script_modules()->get_queue() );
    269329
    270330        // No dependencies, with a string version and fetch priority of high.
     
    276336            array( 'fetchpriority' => 'high' )
    277337        );
     338        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() );
     339
     340        // Register and enqueue something which we'll dequeue right away.
     341        $register_and_enqueue(
     342            'i',
     343            '/i.js',
     344            array(),
     345            '3.0.0'
     346        );
     347        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() );
     348
     349        // Register and enqueue something which we'll deregister right away.
     350        $register_and_enqueue(
     351            'j',
     352            '/j.js',
     353            array(),
     354            '3.0.0'
     355        );
     356        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' ), wp_script_modules()->get_queue() );
     357
     358        // Make sure unregister functions work.
     359        $deregister_id = 'j';
     360        $this->assertArrayHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) );
     361        if ( $use_global_function ) {
     362            wp_deregister_script_module( $deregister_id );
     363        } else {
     364            wp_script_modules()->deregister( $deregister_id );
     365        }
     366        $this->assertArrayNotHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) );
     367        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() );
     368
     369        // Make sure dequeue functions work.
     370        $dequeue_id = 'i';
     371        $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) );
     372        if ( $use_global_function ) {
     373            wp_dequeue_script_module( $dequeue_id );
     374        } else {
     375            wp_script_modules()->dequeue( $dequeue_id );
     376        }
     377        $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) );
     378        $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() );
    278379
    279380        $actual = array(
     
    309410                        'url'           => '/a.js?ver=99.9.9',
    310411                        'fetchpriority' => 'auto',
     412                        'in_footer'     => false,
    311413                    ),
    312414                    'b' => array(
    313415                        'url'           => '/b.js?ver=99.9.9',
    314416                        'fetchpriority' => 'low',
     417                        'in_footer'     => false,
    315418                    ),
    316419                    'c' => array(
    317420                        'url'           => '/c.js?ver=99.9.9',
    318421                        'fetchpriority' => 'auto',
     422                        'in_footer'     => false,
    319423                    ),
    320424                    'd' => array(
    321425                        'url'           => '/d.js',
    322426                        'fetchpriority' => 'auto',
     427                        'in_footer'     => false,
    323428                    ),
    324429                    'e' => array(
    325430                        'url'           => '/e.js?ver=1.0.0',
    326431                        'fetchpriority' => 'auto',
     432                        'in_footer'     => false,
    327433                    ),
    328434                    'f' => array(
    329435                        'url'           => '/f.js?ver=2.0.0',
    330436                        'fetchpriority' => 'auto',
     437                        'in_footer'     => false,
    331438                    ),
    332439                    'g' => array(
    333440                        'url'           => '/g.js?ver=2.0.0',
    334441                        'fetchpriority' => 'low',
     442                        'in_footer'     => false,
    335443                    ),
    336444                    'h' => array(
    337445                        'url'           => '/h.js?ver=3.0.0',
    338446                        'fetchpriority' => 'high',
     447                        'in_footer'     => false,
    339448                    ),
    340449                ),
     
    346455                    'd-dynamic-dep' => '/d-dynamic-dep.js?ver=99.9.9',
    347456                ),
    348             ),
    349             $actual,
    350             "Snapshot:\n" . var_export( $actual, true )
    351         );
    352 
    353         // Dequeue the first half of the scripts.
    354         foreach ( array( 'a', 'b', 'c', 'd' ) as $id ) {
    355             if ( $use_global_function ) {
    356                 wp_dequeue_script_module( $id );
    357             } else {
    358                 wp_script_modules()->dequeue( $id );
    359             }
    360         }
    361 
    362         $actual = array(
    363             'preload_links' => $this->get_preloaded_script_modules(),
    364             'script_tags'   => $this->get_enqueued_script_modules(),
    365             'import_map'    => $this->get_import_map(),
    366         );
    367         $this->assertSame(
    368             array(
    369                 'preload_links' => array(),
    370                 'script_tags'   => array(
    371                     'e' => array(
    372                         'url'           => '/e.js?ver=1.0.0',
    373                         'fetchpriority' => 'auto',
    374                     ),
    375                     'f' => array(
    376                         'url'           => '/f.js?ver=2.0.0',
    377                         'fetchpriority' => 'auto',
    378                     ),
    379                     'g' => array(
    380                         'url'           => '/g.js?ver=2.0.0',
    381                         'fetchpriority' => 'low',
    382                     ),
    383                     'h' => array(
    384                         'url'           => '/h.js?ver=3.0.0',
    385                         'fetchpriority' => 'high',
    386                     ),
    387                 ),
    388                 'import_map'    => array(),
    389             ),
    390             $actual,
    391             "Snapshot:\n" . var_export( $actual, true )
    392         );
    393 
    394         // Unregister the remaining scripts.
    395         foreach ( array( 'e', 'f', 'g', 'h' ) as $id ) {
    396             if ( $use_global_function ) {
    397                 wp_dequeue_script_module( $id );
    398             } else {
    399                 wp_script_modules()->dequeue( $id );
    400             }
    401         }
    402 
    403         $actual = array(
    404             'preload_links' => $this->get_preloaded_script_modules(),
    405             'script_tags'   => $this->get_enqueued_script_modules(),
    406             'import_map'    => $this->get_import_map(),
    407         );
    408         $this->assertSame(
    409             array(
    410                 'preload_links' => array(),
    411                 'script_tags'   => array(),
    412                 'import_map'    => array(),
    413457            ),
    414458            $actual,
     
    443487     *
    444488     * @ticket 56313
     489     * @ticket 63486
    445490     *
    446491     * @covers WP_Script_Modules::register()
    447492     * @covers WP_Script_Modules::enqueue()
     493     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    448494     * @covers WP_Script_Modules::print_enqueued_script_modules()
    449495     * @covers WP_Script_Modules::set_fetchpriority()
     496     * @covers WP_Script_Modules::set_in_footer()
    450497     */
    451498    public function test_wp_enqueue_script_module() {
    452499        $this->script_modules->register( 'foo', '/foo.js' );
    453         $this->script_modules->register( 'bar', '/bar.js', array(), false, array( 'fetchpriority' => 'high' ) );
     500        $this->script_modules->register(
     501            'bar',
     502            '/bar.js',
     503            array(),
     504            false,
     505            array(
     506                'fetchpriority' => 'high',
     507                'in_footer'     => true,
     508            )
     509        );
    454510        $this->script_modules->register( 'baz', '/baz.js' );
    455511        $this->assertTrue( $this->script_modules->set_fetchpriority( 'baz', 'low' ) );
     512        $this->assertTrue( $this->script_modules->set_in_footer( 'baz', true ) );
    456513        $this->script_modules->enqueue( 'foo' );
    457514        $this->script_modules->enqueue( 'bar' );
     
    463520        $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] );
    464521        $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] );
     522        $this->assertFalse( $enqueued_script_modules['foo']['in_footer'] );
    465523        $this->assertStringStartsWith( '/bar.js', $enqueued_script_modules['bar']['url'] );
    466524        $this->assertSame( 'high', $enqueued_script_modules['bar']['fetchpriority'] );
     525        $this->assertTrue( $enqueued_script_modules['bar']['in_footer'] );
    467526        $this->assertStringStartsWith( '/baz.js', $enqueued_script_modules['baz']['url'] );
    468527        $this->assertSame( 'low', $enqueued_script_modules['baz']['fetchpriority'] );
     528        $this->assertTrue( $enqueued_script_modules['baz']['in_footer'] );
     529    }
     530
     531    /**
     532     * Tests that no script is printed for a script without a src.
     533     *
     534     * @ticket 63486
     535     *
     536     * @covers WP_Script_Modules::register()
     537     * @covers WP_Script_Modules::enqueue()
     538     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     539     * @covers WP_Script_Modules::print_enqueued_script_modules()
     540     * @covers WP_Script_Modules::get_src()
     541     */
     542    public function test_wp_enqueue_script_module_with_empty_src() {
     543        wp_enqueue_script_module( 'with-src', '/src.js' );
     544        wp_register_script_module( 'without-src', '' );
     545        wp_register_script_module( 'without-src-but-filtered', '' );
     546        wp_enqueue_script_module( 'without-src' );
     547        wp_enqueue_script_module( 'without-src-but-filtered' );
     548        $this->assertSame( array( 'with-src', 'without-src', 'without-src-but-filtered' ), wp_script_modules()->get_queue() );
     549        add_filter(
     550            'script_module_loader_src',
     551            static function ( $src, $id ) {
     552                if ( 'without-src-but-filtered' === $id ) {
     553                    $src = '/was-empty-but-added-via-filter.js';
     554                }
     555                return $src;
     556            },
     557            10,
     558            2
     559        );
     560        $actual = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     561        $this->assertEqualHTML(
     562            '
     563                <script type="module" src="https://core.trac.wordpress.org/src.js?ver=6.9-alpha-60093-src" id="with-src-js-module"></script>
     564                <script type="module" src="https://core.trac.wordpress.org/was-empty-but-added-via-filter.js" id="without-src-but-filtered-js-module"></script>
     565            ',
     566            $actual,
     567            '<body>',
     568            "Expected only one SCRIPT tag to be printed. Snapshot:\n$actual"
     569        );
    469570    }
    470571
     
    473574    *
    474575    * @ticket 56313
     576    * @ticket 63486
    475577    *
    476578    * @covers WP_Script_Modules::register()
    477579    * @covers WP_Script_Modules::enqueue()
    478580    * @covers WP_Script_Modules::dequeue()
     581    * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    479582    * @covers WP_Script_Modules::print_enqueued_script_modules()
    480583    */
     
    571674    *
    572675    * @ticket 56313
     676    * @ticket 63486
    573677    *
    574678    * @covers WP_Script_Modules::register()
    575679    * @covers WP_Script_Modules::enqueue()
     680    * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    576681    * @covers WP_Script_Modules::print_enqueued_script_modules()
    577682    */
     
    594699     *
    595700     * @ticket 56313
     701     * @ticket 63486
    596702     *
    597703     * @covers WP_Script_Modules::register()
    598704     * @covers WP_Script_Modules::enqueue()
    599705     * @covers WP_Script_Modules::dequeue()
     706     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    600707     * @covers WP_Script_Modules::print_enqueued_script_modules()
    601708     */
     
    9611068     *
    9621069     * @ticket 56313
     1070     * @ticket 63486
    9631071     *
    9641072     * @covers WP_Script_Modules::register()
    9651073     * @covers WP_Script_Modules::enqueue()
     1074     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    9661075     * @covers WP_Script_Modules::print_enqueued_script_modules()
    9671076     * @covers WP_Script_Modules::print_import_map()
     
    9991108     *
    10001109     * @ticket 56313
     1110     * @ticket 63486
    10011111     *
    10021112     * @covers WP_Script_Modules::enqueue()
     1113     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10031114     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10041115     */
     
    10171128     *
    10181129     * @ticket 56313
     1130     * @ticket 63486
    10191131     *
    10201132     * @covers WP_Script_Modules::enqueue()
     1133     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10211134     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10221135     */
     
    10361149     *
    10371150     * @ticket 56313
     1151     * @ticket 63486
    10381152     *
    10391153     * @covers WP_Script_Modules::enqueue()
     1154     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10401155     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10411156     */
     
    10621177     *
    10631178     * @ticket 56313
     1179     * @ticket 63486
    10641180     *
    10651181     * @covers WP_Script_Modules::register()
    10661182     * @covers WP_Script_Modules::enqueue()
     1183     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
    10671184     * @covers WP_Script_Modules::print_enqueued_script_modules()
    10681185     * @covers WP_Script_Modules::print_import_map()
     
    13161433
    13171434    /**
     1435     * Tests ways of setting in_footer.
     1436     *
     1437     * @ticket 63486
     1438     * @ticket 63486
     1439     *
     1440     * @covers ::wp_register_script_module
     1441     * @covers ::wp_enqueue_script_module
     1442     * @covers WP_Script_Modules::set_in_footer
     1443     */
     1444    public function test_in_footer_methods() {
     1445        wp_register_script_module( 'default', '/default.js', array(), null );
     1446        wp_enqueue_script_module( 'default' );
     1447
     1448        wp_register_script_module( 'in-footer-via-register', '/in-footer-via-register.js', array(), null, array( 'in_footer' => true ) );
     1449        wp_enqueue_script_module( 'in-footer-via-register' );
     1450
     1451        wp_enqueue_script_module( 'in-footer-via-enqueue', '/in-footer-via-enqueue.js', array(), null, array( 'in_footer' => true ) );
     1452
     1453        wp_enqueue_script_module( 'not-in-footer-via-enqueue', '/not-in-footer-via-enqueue.js', array(), null, array( 'in_footer' => false ) );
     1454
     1455        wp_enqueue_script_module( 'in-footer-via-override', '/in-footer-via-override.js' );
     1456        wp_script_modules()->set_in_footer( 'in-footer-via-override', true );
     1457
     1458        wp_enqueue_script_module( 'not-in-footer-via-override', '/not-in-footer-via-override.js', array(), null, array( 'in_footer' => true ) );
     1459        wp_script_modules()->set_in_footer( 'not-in-footer-via-override', false );
     1460
     1461        $actual_head   = get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) );
     1462        $actual_footer = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1463
     1464        $this->assertEqualHTML(
     1465            $actual_head,
     1466            '
     1467                <script type="module" src="https://core.trac.wordpress.org/default.js" id="default-js-module"></script>
     1468                <script type="module" src="https://core.trac.wordpress.org/not-in-footer-via-enqueue.js" id="not-in-footer-via-enqueue-js-module"></script>
     1469                <script type="module" src="https://core.trac.wordpress.org/not-in-footer-via-override.js" id="not-in-footer-via-override-js-module"></script>
     1470            ',
     1471            '<body>',
     1472            "Expected equal script modules in the HEAD. Snapshot:\n$actual_head"
     1473        );
     1474        $this->assertEqualHTML(
     1475            $actual_footer,
     1476            '
     1477                <script type="module" src="https://core.trac.wordpress.org/in-footer-via-register.js" id="in-footer-via-register-js-module"></script>
     1478                <script type="module" src="https://core.trac.wordpress.org/in-footer-via-enqueue.js" id="in-footer-via-enqueue-js-module"></script>
     1479                <script type="module" src="https://core.trac.wordpress.org/in-footer-via-override.js?ver=6.9-alpha-60093-src" id="in-footer-via-override-js-module"></script>
     1480            ',
     1481            '<body>',
     1482            "Expected equal script modules in the footer. Snapshot:\n$actual_footer"
     1483        );
     1484    }
     1485
     1486    /**
    13181487     * Tests that a script module with an invalid fetchpriority value gets a value of auto.
    13191488     *
     
    13781547                            'url'                   => '/bajo.js',
    13791548                            'fetchpriority'         => 'high',
     1549                            'in_footer'             => false,
    13801550                            'data-wp-fetchpriority' => 'low',
    13811551                        ),
     
    14001570                            'url'                   => '/auto.js',
    14011571                            'fetchpriority'         => 'high',
     1572                            'in_footer'             => false,
    14021573                            'data-wp-fetchpriority' => 'auto',
    14031574                        ),
     
    14131584                'expected' => array(
    14141585                    'preload_links' => array(
    1415                         'auto' => array(
    1416                             'url'           => '/auto.js',
    1417                             'fetchpriority' => 'high',
    1418                         ),
    14191586                        'bajo' => array(
    14201587                            'url'                   => '/bajo.js',
     
    14221589                            'data-wp-fetchpriority' => 'low',
    14231590                        ),
     1591                        'auto' => array(
     1592                            'url'           => '/auto.js',
     1593                            'fetchpriority' => 'high',
     1594                        ),
    14241595                    ),
    14251596                    'script_tags'   => array(
     
    14271598                            'url'           => '/alto.js',
    14281599                            'fetchpriority' => 'high',
     1600                            'in_footer'     => false,
    14291601                        ),
    14301602                    ),
     
    15401712        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    15411713        $expected = '
    1542             <link rel="modulepreload" href="https://core.trac.wordpress.org/b.js" id="b-js-modulepreload" fetchpriority="low">
    1543             <link rel="modulepreload" href="https://core.trac.wordpress.org/c.js" id="c-js-modulepreload" fetchpriority="low">
     1714            <link rel="modulepreload" href="https://core.trac.wordpress.org/z.js" id="z-js-modulepreload" fetchpriority="high">
    15441715            <link rel="modulepreload" href="https://core.trac.wordpress.org/d.js" id="d-js-modulepreload" fetchpriority="high">
    15451716            <link rel="modulepreload" href="https://core.trac.wordpress.org/e.js" id="e-js-modulepreload" fetchpriority="low">
    1546             <link rel="modulepreload" href="https://core.trac.wordpress.org/z.js" id="z-js-modulepreload" fetchpriority="high">
     1717            <link rel="modulepreload" href="https://core.trac.wordpress.org/c.js" id="c-js-modulepreload" fetchpriority="low">
     1718            <link rel="modulepreload" href="https://core.trac.wordpress.org/b.js" id="b-js-modulepreload" fetchpriority="low">
    15471719            <link rel="modulepreload" href="https://core.trac.wordpress.org/y.js" id="y-js-modulepreload" fetchpriority="high">
    15481720            <script type="module" src="https://core.trac.wordpress.org/a.js" id="a-js-module" fetchpriority="low"></script>
     
    15851757        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    15861758        $expected = '
    1587             <link rel="modulepreload" href="https://core.trac.wordpress.org/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
    15881759            <link rel="modulepreload" href="https://core.trac.wordpress.org/d.js" id="d-js-modulepreload" fetchpriority="low">
    15891760            <link rel="modulepreload" href="https://core.trac.wordpress.org/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1761            <link rel="modulepreload" href="https://core.trac.wordpress.org/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
    15901762            <link rel="modulepreload" href="https://core.trac.wordpress.org/b.js" id="b-js-modulepreload">
     1763            <link rel="modulepreload" href="https://core.trac.wordpress.org/f.js" id="f-js-modulepreload" fetchpriority="high">
    15911764            <link rel="modulepreload" href="https://core.trac.wordpress.org/c.js" id="c-js-modulepreload" fetchpriority="high">
    1592             <link rel="modulepreload" href="https://core.trac.wordpress.org/f.js" id="f-js-modulepreload" fetchpriority="high">
    15931765            <script type="module" src="https://core.trac.wordpress.org/x.js" id="x-js-module" fetchpriority="low"></script>
    15941766            <script type="module" src="https://core.trac.wordpress.org/y.js" id="y-js-module"></script>
     
    16011773     * Tests that default script modules are printed as expected.
    16021774     *
     1775     * @ticket 63486
     1776     *
    16031777     * @covers ::wp_default_script_modules
    16041778     * @covers WP_Script_Modules::print_script_module_preloads
     1779     * @covers WP_Script_Modules::print_head_enqueued_script_modules
    16051780     * @covers WP_Script_Modules::print_enqueued_script_modules
    16061781     */
     
    16101785        wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
    16111786
    1612         $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
    1613         $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
    1614 
    1615         $actual = $this->normalize_markup_for_snapshot( $actual );
    1616 
    1617         $expected = '
    1618             <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
    1619             <script type="module" src="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
    1620             <script type="module" src="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
    1621         ';
    1622         $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1787        $actual_preloads = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ) );
     1788        $this->assertEqualHTML(
     1789            '
     1790                <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
     1791            ',
     1792            $actual_preloads,
     1793            '<body>',
     1794            "Snapshot:\n$actual_preloads"
     1795        );
     1796
     1797        $actual_head_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) ) );
     1798        $this->assertEqualHTML(
     1799            '',
     1800            $actual_head_script_modules,
     1801            '<body>',
     1802            "Snapshot:\n$actual_head_script_modules"
     1803        );
     1804
     1805        $actual_footer_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ) );
     1806        $this->assertEqualHTML(
     1807            '
     1808                <script type="module" src="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
     1809                <script type="module" src="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
     1810            ',
     1811            $actual_footer_script_modules,
     1812            '<body>',
     1813            "Snapshot:\n$actual_footer_script_modules"
     1814        );
    16231815    }
    16241816
     
    16471839        $expected = '
    16481840            <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1841            <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    16491842            <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    1650             <link rel="modulepreload" href="https://core.trac.wordpress.org/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
    16511843            <script type="module" src="https://core.trac.wordpress.org/super-important-module.js" id="super-important-js-module" fetchpriority="high"></script>
    16521844        ';
     
    17311923        );
    17321924    }
     1925
     1926    /**
     1927     * Tests various ways of printing and dependency ordering of script modules.
     1928     *
     1929     * This ensures that the global function aliases pass all the same parameters as the class methods.
     1930     *
     1931     * @ticket 63486
     1932     *
     1933     * @dataProvider data_test_register_and_enqueue_script_module
     1934     *
     1935     * @covers ::wp_register_script_module()
     1936     * @covers WP_Script_Modules::register()
     1937     * @covers ::wp_enqueue_script_module()
     1938     * @covers WP_Script_Modules::enqueue()
     1939     * @covers ::wp_dequeue_script_module()
     1940     * @covers WP_Script_Modules::dequeue()
     1941     * @covers ::wp_deregister_script_module()
     1942     * @covers WP_Script_Modules::deregister()
     1943     * @covers WP_Script_Modules::set_fetchpriority()
     1944     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     1945     * @covers WP_Script_Modules::print_enqueued_script_modules()
     1946     * @covers WP_Script_Modules::print_import_map()
     1947     * @covers WP_Script_Modules::print_script_module_preloads()
     1948     */
     1949    public function test_script_module_printing_and_dependency_ordering( bool $use_global_function, bool $only_enqueue ) {
     1950        global $wp_version;
     1951        $wp_version = '99.9.9';
     1952
     1953        $register = static function ( ...$args ) use ( $use_global_function ) {
     1954            if ( $use_global_function ) {
     1955                wp_register_script_module( ...$args );
     1956            } else {
     1957                wp_script_modules()->register( ...$args );
     1958            }
     1959        };
     1960
     1961        $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) {
     1962            if ( $use_global_function ) {
     1963                if ( $only_enqueue ) {
     1964                    wp_enqueue_script_module( ...$args );
     1965                } else {
     1966                    wp_register_script_module( ...$args );
     1967                    wp_enqueue_script_module( $args[0] );
     1968                }
     1969            } else {
     1970                if ( $only_enqueue ) {
     1971                    wp_script_modules()->enqueue( ...$args );
     1972                } else {
     1973                    wp_script_modules()->register( ...$args );
     1974                    wp_script_modules()->enqueue( $args[0] );
     1975                }
     1976            }
     1977        };
     1978
     1979        $deregister = static function ( array $ids ) use ( $use_global_function ) {
     1980            foreach ( $ids as $id ) {
     1981                if ( $use_global_function ) {
     1982                    wp_deregister_script_module( $id );
     1983                } else {
     1984                    wp_script_modules()->deregister( $id );
     1985                }
     1986            }
     1987        };
     1988
     1989        // Test script module is placed in footer when in_footer is true.
     1990        $register_and_enqueue( 'a', '/a.js', array(), '1.0.0', array( 'in_footer' => true ) );
     1991
     1992        $actual = array(
     1993            'preload_links' => $this->get_preloaded_script_modules(),
     1994            'script_tags'   => $this->get_enqueued_script_modules(),
     1995            'import_map'    => $this->get_import_map(),
     1996        );
     1997        $this->assertSame(
     1998            array(
     1999                'preload_links' => array(),
     2000                'script_tags'   => array(
     2001                    'a' => array(
     2002                        'url'           => '/a.js?ver=1.0.0',
     2003                        'fetchpriority' => 'auto',
     2004                        'in_footer'     => true,
     2005                    ),
     2006                ),
     2007                'import_map'    => array(),
     2008            ),
     2009            $actual,
     2010            "Snapshot:\n" . var_export( $actual, true )
     2011        );
     2012
     2013        $deregister( array( 'a' ) );
     2014
     2015        // Test that dependant also gets placed in footer when its dependency is in footer.
     2016        $register_and_enqueue( 'b', '/b.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2017        $register_and_enqueue( 'c', '/c.js', array( 'b' ), '1.0.0' );
     2018
     2019        $actual = array(
     2020            'preload_links' => $this->get_preloaded_script_modules(),
     2021            'script_tags'   => $this->get_enqueued_script_modules(),
     2022            'import_map'    => $this->get_import_map(),
     2023        );
     2024        $this->assertSame(
     2025            array(
     2026                'preload_links' => array(),
     2027                'script_tags'   => array(
     2028                    'b' => array(
     2029                        'url'           => '/b.js?ver=1.0.0',
     2030                        'fetchpriority' => 'auto',
     2031                        'in_footer'     => true,
     2032                    ),
     2033                    'c' => array(
     2034                        'url'           => '/c.js?ver=1.0.0',
     2035                        'fetchpriority' => 'auto',
     2036                        'in_footer'     => true,
     2037                    ),
     2038                ),
     2039                'import_map'    => array(
     2040                    'b' => '/b.js?ver=1.0.0',
     2041                ),
     2042            ),
     2043            $actual,
     2044            "Snapshot:\n" . var_export( $actual, true )
     2045        );
     2046
     2047        $deregister( array( 'b', 'c ' ) );
     2048
     2049        // Test that registered dependency in footer doesn't place dependant in footer.
     2050        $register( 'd', '/d.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2051        $register_and_enqueue( 'e', '/e.js', array( 'd' ), '1.0.0' );
     2052
     2053        $actual = array(
     2054            'preload_links' => $this->get_preloaded_script_modules(),
     2055            'script_tags'   => $this->get_enqueued_script_modules(),
     2056            'import_map'    => $this->get_import_map(),
     2057        );
     2058        $this->assertSame(
     2059            array(
     2060                'preload_links' => array(
     2061                    'd' => array(
     2062                        'url'           => '/d.js?ver=1.0.0',
     2063                        'fetchpriority' => 'auto',
     2064                    ),
     2065                ),
     2066                'script_tags'   => array(
     2067                    'e' => array(
     2068                        'url'           => '/e.js?ver=1.0.0',
     2069                        'fetchpriority' => 'auto',
     2070                        'in_footer'     => false,
     2071                    ),
     2072                ),
     2073                'import_map'    => array(
     2074                    'd' => '/d.js?ver=1.0.0',
     2075                ),
     2076            ),
     2077            $actual,
     2078            "Snapshot:\n" . var_export( $actual, true )
     2079        );
     2080
     2081        $deregister( array( 'd', 'e' ) );
     2082
     2083        // Test if one of the dependency is in footer, the dependant and other dependant dependencies are also placed in footer.
     2084        $register_and_enqueue( 'f', '/f.js', array(), '1.0.0' );
     2085        $register_and_enqueue( 'g', '/g.js', array( 'f' ), '1.0.0', array( 'in_footer' => true ) );
     2086        $register_and_enqueue( 'h', '/h.js', array( 'g' ), '1.0.0' );
     2087        $register_and_enqueue( 'i', '/i.js', array( 'h' ), '1.0.0' );
     2088
     2089        $actual = array(
     2090            'preload_links' => $this->get_preloaded_script_modules(),
     2091            'script_tags'   => $this->get_enqueued_script_modules(),
     2092            'import_map'    => $this->get_import_map(),
     2093        );
     2094
     2095        $this->assertSame(
     2096            array(
     2097                'preload_links' => array(),
     2098                'script_tags'   => array(
     2099                    'f' => array(
     2100                        'url'           => '/f.js?ver=1.0.0',
     2101                        'fetchpriority' => 'auto',
     2102                        'in_footer'     => false,
     2103                    ),
     2104                    'g' => array(
     2105                        'url'           => '/g.js?ver=1.0.0',
     2106                        'fetchpriority' => 'auto',
     2107                        'in_footer'     => true,
     2108                    ),
     2109                    'h' => array(
     2110                        'url'           => '/h.js?ver=1.0.0',
     2111                        'fetchpriority' => 'auto',
     2112                        'in_footer'     => true,
     2113                    ),
     2114                    'i' => array(
     2115                        'url'           => '/i.js?ver=1.0.0',
     2116                        'fetchpriority' => 'auto',
     2117                        'in_footer'     => true,
     2118                    ),
     2119                ),
     2120                'import_map'    => array(
     2121                    'f' => '/f.js?ver=1.0.0',
     2122                    'g' => '/g.js?ver=1.0.0',
     2123                    'h' => '/h.js?ver=1.0.0',
     2124                ),
     2125            ),
     2126            $actual,
     2127            "Snapshot:\n" . var_export( $actual, true )
     2128        );
     2129
     2130        $deregister( array( 'f', 'g', 'h', 'i' ) );
     2131
     2132        // Test dependency ordering when all scripts modules are enqueued in head.
     2133        // Expected order: j, k, l, m.
     2134        $register_and_enqueue( 'm', '/m.js', array( 'j', 'l' ), '1.0.0' );
     2135        $register_and_enqueue( 'k', '/k.js', array( 'j' ), '1.0.0' );
     2136        $register_and_enqueue( 'l', '/l.js', array( 'k' ), '1.0.0' );
     2137        $register_and_enqueue( 'j', '/j.js', array(), '1.0.0' );
     2138
     2139        $actual = array(
     2140            'preload_links' => $this->get_preloaded_script_modules(),
     2141            'script_tags'   => $this->get_enqueued_script_modules(),
     2142            'import_map'    => $this->get_import_map(),
     2143        );
     2144
     2145        $this->assertSame(
     2146            array(
     2147                'preload_links' => array(),
     2148                'script_tags'   => array(
     2149                    'j' => array(
     2150                        'url'           => '/j.js?ver=1.0.0',
     2151                        'fetchpriority' => 'auto',
     2152                        'in_footer'     => false,
     2153                    ),
     2154                    'k' => array(
     2155                        'url'           => '/k.js?ver=1.0.0',
     2156                        'fetchpriority' => 'auto',
     2157                        'in_footer'     => false,
     2158                    ),
     2159                    'l' => array(
     2160                        'url'           => '/l.js?ver=1.0.0',
     2161                        'fetchpriority' => 'auto',
     2162                        'in_footer'     => false,
     2163                    ),
     2164                    'm' => array(
     2165                        'url'           => '/m.js?ver=1.0.0',
     2166                        'fetchpriority' => 'auto',
     2167                        'in_footer'     => false,
     2168                    ),
     2169                ),
     2170                'import_map'    => array(
     2171                    'j' => '/j.js?ver=1.0.0',
     2172                    'l' => '/l.js?ver=1.0.0',
     2173                    'k' => '/k.js?ver=1.0.0',
     2174                ),
     2175            ),
     2176            $actual,
     2177            "Snapshot:\n" . var_export( $actual, true )
     2178        );
     2179
     2180        $deregister( array( 'j', 'k', 'l', 'm' ) );
     2181
     2182        // Test dependency ordering when scripts modules are enqueued in both head and footer.
     2183        // Expected order: q, n, o, p, r.
     2184        $register_and_enqueue( 'n', '/n.js', array( 'q' ), '1.0.0' );
     2185        $register_and_enqueue( 'q', '/q.js', array(), '1.0.0' );
     2186        $register_and_enqueue( 'o', '/o.js', array( 'n' ), '1.0.0', array( 'in_footer' => true ) );
     2187        $register_and_enqueue( 'r', '/r.js', array( 'q', 'o', 'p' ), '1.0.0' );
     2188        $register_and_enqueue( 'p', '/p.js', array(), '1.0.0', array( 'in_footer' => true ) );
     2189
     2190        $actual = array(
     2191            'preload_links' => $this->get_preloaded_script_modules(),
     2192            'script_tags'   => $this->get_enqueued_script_modules(),
     2193            'import_map'    => $this->get_import_map(),
     2194        );
     2195
     2196        $this->assertSame(
     2197            array(
     2198                'preload_links' => array(),
     2199                'script_tags'   => array(
     2200                    'q' => array(
     2201                        'url'           => '/q.js?ver=1.0.0',
     2202                        'fetchpriority' => 'auto',
     2203                        'in_footer'     => false,
     2204                    ),
     2205                    'n' => array(
     2206                        'url'           => '/n.js?ver=1.0.0',
     2207                        'fetchpriority' => 'auto',
     2208                        'in_footer'     => false,
     2209                    ),
     2210                    'o' => array(
     2211                        'url'           => '/o.js?ver=1.0.0',
     2212                        'fetchpriority' => 'auto',
     2213                        'in_footer'     => true,
     2214                    ),
     2215                    'p' => array(
     2216                        'url'           => '/p.js?ver=1.0.0',
     2217                        'fetchpriority' => 'auto',
     2218                        'in_footer'     => true,
     2219                    ),
     2220                    'r' => array(
     2221                        'url'           => '/r.js?ver=1.0.0',
     2222                        'fetchpriority' => 'auto',
     2223                        'in_footer'     => true,
     2224                    ),
     2225                ),
     2226                'import_map'    => array(
     2227                    'q' => '/q.js?ver=1.0.0',
     2228                    'n' => '/n.js?ver=1.0.0',
     2229                    'o' => '/o.js?ver=1.0.0',
     2230                    'p' => '/p.js?ver=1.0.0',
     2231                ),
     2232            ),
     2233            $actual,
     2234            "Snapshot:\n" . var_export( $actual, true )
     2235        );
     2236    }
     2237
     2238    /**
     2239     * Tests various ways of printing and dependency ordering of script modules.
     2240     *
     2241     * @ticket 63486
     2242     *
     2243     * @dataProvider data_test_register_and_enqueue_script_module
     2244     *
     2245     * @covers ::wp_register_script_module()
     2246     * @covers WP_Script_Modules::register()
     2247     * @covers ::wp_enqueue_script_module()
     2248     * @covers WP_Script_Modules::enqueue()
     2249     * @covers ::wp_dequeue_script_module()
     2250     * @covers WP_Script_Modules::dequeue()
     2251     * @covers ::wp_deregister_script_module()
     2252     * @covers WP_Script_Modules::deregister()
     2253     * @covers WP_Script_Modules::set_fetchpriority()
     2254     * @covers WP_Script_Modules::print_head_enqueued_script_modules()
     2255     * @covers WP_Script_Modules::print_enqueued_script_modules()
     2256     * @covers WP_Script_Modules::print_import_map()
     2257     * @covers WP_Script_Modules::print_script_module_preloads()
     2258     */
     2259    public function test_static_import_dependency_with_dynamic_imports_depending_on_static_import_dependency() {
     2260        $get_dependency = function ( string $id, string $import ): array {
     2261            return compact( 'id', 'import' );
     2262        };
     2263
     2264        wp_register_script_module( 'enqueued', '/enqueued.js', array( $get_dependency( 'static1', 'static' ) ), null );
     2265        wp_register_script_module( 'static1', '/static1.js', array( $get_dependency( 'dynamic1', 'dynamic' ) ), null );
     2266        wp_register_script_module( 'dynamic1', '/dynamic1.js', array( $get_dependency( 'static2', 'static' ) ), null );
     2267        wp_register_script_module( 'static2', '/static2.js', array(), null );
     2268
     2269        wp_enqueue_script_module( 'enqueued' );
     2270        $import_map     = $this->get_import_map();
     2271        $preload_links  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     2272        $script_modules = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     2273
     2274        $this->assertEquals(
     2275            array(
     2276                'static1'  => '/static1.js',
     2277                'dynamic1' => '/dynamic1.js',
     2278                'static2'  => '/static2.js',
     2279            ),
     2280            $import_map,
     2281            "Expected import map to match snapshot:\n" . var_export( $import_map, true )
     2282        );
     2283        $this->assertEqualHTML(
     2284            '
     2285                <link rel="modulepreload" href="https://core.trac.wordpress.org/static1.js" id="static1-js-modulepreload">
     2286            ',
     2287            $preload_links,
     2288            '<body>',
     2289            "Expected preload links to match snapshot:\n$preload_links"
     2290        );
     2291        $this->assertEqualHTML(
     2292            '
     2293                <script type="module" src="https://core.trac.wordpress.org/enqueued.js" id="enqueued-js-module"></script>
     2294            ',
     2295            $script_modules,
     2296            '<body>',
     2297            "Expected script modules to match snapshot:\n$script_modules"
     2298        );
     2299    }
    17332300}
Note: See TracChangeset for help on using the changeset viewer.