diff --git a/phpcompat.xml.dist b/phpcompat.xml.dist
index 66ea48ab78b8e..a4771be79618b 100644
--- a/phpcompat.xml.dist
+++ b/phpcompat.xml.dist
@@ -114,4 +114,12 @@
/sodium_compat/src/PHP52/SplFixedArray\.php$
+
+
+ /src/wp-includes/script-loader\.php$
+
+
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index 87a8339ec89e2..370b341ddd21b 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -2264,10 +2264,14 @@ function wp_print_head_scripts() {
/**
* Private, for use in *_footer_scripts hooks
*
- * In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()},
- * this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of
- * {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted
- * to the HEAD by means of the template enhancement output buffer.
+ * In classic themes, when block styles are loaded on demand via wp_load_classic_theme_block_styles_on_demand(),
+ * this function is replaced by a closure in wp_hoist_late_printed_styles() which will capture the printing of
+ * two sets of "late" styles to be hoisted to the HEAD by means of the template enhancement output buffer:
+ *
+ * 1. Styles related to blocks are inserted right after the wp-block-library stylesheet.
+ * 2. All other styles are appended to the end of the HEAD.
+ *
+ * The closure calls print_footer_scripts() to print scripts in the footer as usual.
*
* @since 3.3.0
*/
@@ -3601,20 +3605,23 @@ function wp_load_classic_theme_block_styles_on_demand() {
// The following two filters are added by default for block themes in _add_default_theme_supports().
/*
- * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally,
- * and so that block-specific styles will only be enqueued when they are used on the page.
- * A priority of zero allows for this to be easily overridden by themes which wish to opt out.
+ * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, and so
+ * that block-specific styles will only be enqueued when they are used on the page. A priority of zero allows for
+ * this to be easily overridden by themes which wish to opt out. If a site has explicitly opted out of loading
+ * separate block styles, then abort.
*/
add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
+ if ( ! wp_should_load_separate_core_block_assets() ) {
+ return;
+ }
/*
* Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
- * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out.
+ * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. If a site
+ * has explicitly opted out of loading block styles on demand, then abort.
*/
add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );
-
- // If a site has explicitly opted out of loading block styles on demand via filters with priorities higher than above, then abort.
- if ( ! wp_should_load_separate_core_block_assets() || ! wp_should_load_block_assets_on_demand() ) {
+ if ( ! wp_should_load_block_assets_on_demand() ) {
return;
}
@@ -3637,37 +3644,73 @@ function wp_hoist_late_printed_styles() {
}
/*
- * While normally late styles are printed, there is a filter to disable prevent this, so this makes sure they are
- * printed. Note that this filter was intended to control whether to print the styles queued too late for the HTML
- * head. This filter was introduced in . However, with the template
- * enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can
- * always hoist it to the HEAD.
+ * Add a placeholder comment into the inline styles for wp-block-library, after which where the late block styles
+ * can be hoisted from the footer to be printed in the header by means of a filter below on the template enhancement
+ * output buffer. The `wp_print_styles` action is used to ensure that if the inline style gets replaced at
+ * `enqueue_block_assets` or `wp_enqueue_scripts` that the placeholder will be sure to be present.
*/
- add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX );
+ $placeholder = sprintf( '/*%s*/', uniqid( 'wp_block_styles_on_demand_placeholder:' ) );
+ add_action(
+ 'wp_print_styles',
+ static function () use ( $placeholder ) {
+ wp_add_inline_style( 'wp-block-library', $placeholder );
+ }
+ );
/*
- * Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header
- * by means of a filter below on the template enhancement output buffer.
+ * Create a substitute for `print_late_styles()` which is aware of block styles. This substitute does not print
+ * the styles, but it captures what would be printed for block styles and non-block styles so that they can be
+ * later hoisted to the HEAD in the template enhancement output buffer. This will run at `wp_print_footer_scripts`
+ * before `print_footer_scripts()` is called.
*/
- $placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) );
+ $printed_block_styles = '';
+ $printed_late_styles = '';
+ $capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) {
+ // Gather the styles related to on-demand block enqueues.
+ $all_block_style_handles = array();
+ foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) {
+ foreach ( $block_type->style_handles as $style_handle ) {
+ $all_block_style_handles[] = $style_handle;
+ }
+ }
+ $all_block_style_handles = array_merge(
+ $all_block_style_handles,
+ array(
+ 'global-styles',
+ 'block-style-variation-styles',
+ 'core-block-supports',
+ 'core-block-supports-duotone',
+ )
+ );
- wp_add_inline_style( 'wp-block-library', $placeholder );
+ /*
+ * First print all styles related to blocks which should inserted right after the wp-block-library stylesheet
+ * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`.
+ */
+ $enqueued_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) );
+ if ( count( $enqueued_block_styles ) > 0 ) {
+ ob_start();
+ wp_styles()->do_items( $enqueued_block_styles );
+ $printed_block_styles = ob_get_clean();
+ }
- // Wrap print_late_styles() with a closure that captures the late-printed styles.
- $printed_late_styles = '';
- $capture_late_styles = static function () use ( &$printed_late_styles ) {
+ /*
+ * Print all remaining styles not related to blocks. This contains a subset of the logic from
+ * `print_late_styles()`, without admin-specific logic and the `print_late_styles` filter to control whether
+ * late styles are printed (since they are being hoisted anyway).
+ */
ob_start();
- print_late_styles();
+ wp_styles()->do_footer_items();
$printed_late_styles = ob_get_clean();
};
/*
- * If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts()
- * was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the
+ * If `_wp_footer_scripts()` was unhooked from the `wp_print_footer_scripts` action, or if `wp_print_footer_scripts()`
+ * was unhooked from running at the `wp_footer` action, then only add a callback to `wp_footer` which will capture the
* late-printed styles.
*
- * Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then
- * swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before
+ * Otherwise, in the normal case where `_wp_footer_scripts()` will run at the `wp_print_footer_scripts` action, then
+ * swap out `_wp_footer_scripts()` with an alternative which captures the printed styles (for hoisting to HEAD) before
* proceeding with printing the footer scripts.
*/
$wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
@@ -3689,65 +3732,99 @@ static function () use ( $capture_late_styles ) {
// Replace placeholder with the captured late styles.
add_filter(
'wp_template_enhancement_output_buffer',
- function ( $buffer ) use ( $placeholder, &$printed_late_styles ) {
+ static function ( $buffer ) use ( $placeholder, &$printed_block_styles, &$printed_late_styles ) {
// Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
$processor = new class( $buffer ) extends WP_HTML_Tag_Processor {
- public function get_span(): WP_HTML_Span {
- $instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class.
- $instance->set_bookmark( 'here' );
- return $instance->bookmarks['here'];
+ /**
+ * Gets the span for the current token.
+ *
+ * @return WP_HTML_Span Current token span.
+ */
+ private function get_span(): WP_HTML_Span {
+ // Note: This call will never fail according to the usage of this class, given it is always called after ::next_tag() is true.
+ $this->set_bookmark( 'here' );
+ return $this->bookmarks['here'];
+ }
+
+ /**
+ * Inserts text before the current token.
+ *
+ * @param string $text Text to insert.
+ */
+ public function insert_before( string $text ) {
+ $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text );
+ }
+
+ /**
+ * Inserts text after the current token.
+ *
+ * @param string $text Text to insert.
+ */
+ public function insert_after( string $text ) {
+ $span = $this->get_span();
+
+ $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text );
+ }
+
+ /**
+ * Removes the current token.
+ */
+ public function remove() {
+ $span = $this->get_span();
+
+ $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' );
}
};
- // Loop over STYLE tags.
+ /*
+ * Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles
+ * at (or else print everything there). The placeholder CSS comment will always be added to the
+ * wp-block-library inline style since it gets printed at `wp_head` before the blocks are rendered.
+ * This means that there may not actually be any block styles to hoist from the footer to insert after this
+ * inline style. The placeholder CSS comment needs to be added so that the inline style gets printed, but
+ * if the resulting inline style is empty after the placeholder is removed, then the inline style is
+ * removed.
+ */
while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
-
- // We've encountered the inline style for the 'wp-block-library' stylesheet which probably has the placeholder comment.
if (
- ! $processor->is_tag_closer() &&
'STYLE' === $processor->get_tag() &&
'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
) {
- // If the inline style lacks the placeholder comment, then we have to continue until we get to to append the styles there.
$css_text = $processor->get_modifiable_text();
- if ( ! str_contains( $css_text, $placeholder ) ) {
- continue;
- }
- // Remove the placeholder now that we've located the inline style.
- $processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) );
- $buffer = $processor->get_updated_html();
+ /*
+ * A placeholder CSS comment is added to the inline style in order to force an inline STYLE tag to
+ * be printed. Now that we've located the inline style, the placeholder comment can be removed. If
+ * there is no CSS left in the STYLE tag after removing the placeholder (aside from the sourceURL
+ * comment, then remove the STYLE entirely.)
+ */
+ $css_text = str_replace( $placeholder, '', $css_text );
+ if ( preg_match( ':^/\*# sourceURL=\S+? \*/$:', trim( $css_text ) ) ) {
+ $processor->remove();
+ } else {
+ $processor->set_modifiable_text( $css_text );
+ }
// Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
- $span = $processor->get_span();
- $buffer = implode(
- '',
- array(
- substr( $buffer, 0, $span->start + $span->length ),
- $printed_late_styles,
- substr( $buffer, $span->start + $span->length ),
- )
- );
- break;
- }
+ if ( '' !== $printed_block_styles ) {
+ $processor->insert_after( $printed_block_styles );
- // As a fallback, append the hoisted late styles to the end of the HEAD.
- if ( $processor->is_tag_closer() && 'HEAD' === $processor->get_tag() ) {
- $span = $processor->get_span();
- $buffer = implode(
- '',
- array(
- substr( $buffer, 0, $span->start ),
- $printed_late_styles,
- substr( $buffer, $span->start ),
- )
- );
+ // Prevent printing them again at .
+ $printed_block_styles = '';
+ }
+
+ // If there aren't any late styles, there's no need to continue to finding .
+ if ( '' === $printed_late_styles ) {
+ break;
+ }
+ } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) {
+ $processor->insert_before( $printed_block_styles . $printed_late_styles );
break;
}
}
- return $buffer;
+ return $processor->get_updated_html();
}
);
}
diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php
index 6118a2a43b577..a304fff95f865 100644
--- a/tests/phpunit/tests/template.php
+++ b/tests/phpunit/tests/template.php
@@ -127,10 +127,7 @@ public function set_up() {
$this->original_wp_styles = $wp_styles;
$wp_scripts = null;
$wp_styles = null;
- wp_scripts();
- wp_styles();
- $this->original_theme_features = $GLOBALS['_wp_theme_features'];
foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) {
$this->original_ini_config[ $option ] = ini_get( $option );
}
@@ -141,7 +138,6 @@ public function tear_down() {
$wp_scripts = $this->original_wp_scripts;
$wp_styles = $this->original_wp_styles;
- $GLOBALS['_wp_theme_features'] = $this->original_theme_features;
foreach ( $this->original_ini_config as $option => $value ) {
ini_set( $option, $value );
}
@@ -149,6 +145,7 @@ public function tear_down() {
unregister_post_type( 'cpt' );
unregister_taxonomy( 'taxo' );
$this->set_permalink_structure( '' );
+
parent::tear_down();
}
@@ -630,7 +627,7 @@ public function test_wp_start_template_enhancement_output_buffer_begins_with_fil
add_filter(
'wp_template_enhancement_output_buffer',
static function () {
- return 'Hey!';
+ return 'Hey!';
}
);
$level = ob_get_level();
@@ -1422,7 +1419,7 @@ public function data_wp_load_classic_theme_block_styles_on_demand(): array {
add_filter( 'should_load_separate_core_block_assets', '__return_false' );
},
'expected_load_separate' => false,
- 'expected_on_demand' => true,
+ 'expected_on_demand' => false,
'expected_buffer_started' => false,
),
'classic_theme_with_should_load_block_assets_on_demand_out_out' => array(
@@ -1475,33 +1472,197 @@ public function test_wp_load_classic_theme_block_styles_on_demand( string $theme
/**
* Data provider.
*
- * @return array
+ * @return array
*/
public function data_wp_hoist_late_printed_styles(): array {
+ $common_expected_head_styles = array(
+ 'wp-img-auto-sizes-contain-inline-css',
+ 'early-css',
+ 'early-inline-css',
+ 'wp-emoji-styles-inline-css',
+ 'wp-block-library-css',
+ 'wp-block-separator-css',
+ 'global-styles-inline-css',
+ 'core-block-supports-inline-css',
+ 'classic-theme-styles-css',
+ 'normal-css',
+ 'normal-inline-css',
+ 'wp-custom-css',
+ 'late-css',
+ 'late-inline-css',
+ );
+
return array(
- 'no_actions_removed' => array(
- 'set_up' => null,
+ 'standard_classic_theme_config_with_min_styles_inlined' => array(
+ 'set_up' => null,
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => $common_expected_head_styles,
+ 'BODY' => array(),
+ ),
+ ),
+ 'standard_classic_theme_config_with_max_styles_inlined' => array(
+ 'set_up' => null,
+ 'inline_size_limit' => PHP_INT_MAX,
+ 'expected_styles' => array(
+ 'HEAD' => array(
+ 'wp-img-auto-sizes-contain-inline-css',
+ 'early-css',
+ 'early-inline-css',
+ 'wp-emoji-styles-inline-css',
+ 'wp-block-library-inline-css',
+ 'wp-block-separator-inline-css',
+ 'global-styles-inline-css',
+ 'core-block-supports-inline-css',
+ 'classic-theme-styles-inline-css',
+ 'normal-css',
+ 'normal-inline-css',
+ 'wp-custom-css',
+ 'late-css',
+ 'late-inline-css',
+ ),
+ 'BODY' => array(),
+ ),
+ ),
+ 'standard_classic_theme_config_extra_block_library_inline_style' => array(
+ 'set_up' => static function () {
+ add_action(
+ 'enqueue_block_assets',
+ static function () {
+ wp_add_inline_style( 'wp-block-library', '/* Extra CSS which prevents empty inline style containing placeholder from being removed. */' );
+ }
+ );
+ },
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => ( function ( $expected_styles ) {
+ // Insert 'wp-block-library-inline-css' right after 'wp-block-library-css'.
+ $i = array_search( 'wp-block-library-css', $expected_styles, true );
+ $this->assertIsInt( $i, 'Expected wp-block-library-css to be among the styles.' );
+ array_splice( $expected_styles, $i + 1, 0, 'wp-block-library-inline-css' );
+ return $expected_styles;
+ } )( $common_expected_head_styles ),
+ 'BODY' => array(),
+ ),
),
- '_wp_footer_scripts_removed' => array(
- 'set_up' => static function () {
+ 'classic_theme_opt_out_separate_block_styles' => array(
+ 'set_up' => static function () {
+ add_filter( 'should_load_separate_core_block_assets', '__return_false' );
+ },
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => array(
+ 'wp-img-auto-sizes-contain-inline-css',
+ 'early-css',
+ 'early-inline-css',
+ 'wp-emoji-styles-inline-css',
+ 'wp-block-library-css',
+ 'classic-theme-styles-css',
+ 'global-styles-inline-css',
+ 'normal-css',
+ 'normal-inline-css',
+ 'wp-custom-css',
+ ),
+ 'BODY' => array(
+ 'late-css',
+ 'late-inline-css',
+ 'core-block-supports-inline-css',
+ ),
+ ),
+ ),
+ '_wp_footer_scripts_removed' => array(
+ 'set_up' => static function () {
remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
},
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => $common_expected_head_styles,
+ 'BODY' => array(),
+ ),
),
- 'wp_print_footer_scripts_removed' => array(
- 'set_up' => static function () {
+ 'wp_print_footer_scripts_removed' => array(
+ 'set_up' => static function () {
remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
},
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => $common_expected_head_styles,
+ 'BODY' => array(),
+ ),
),
- 'both_actions_removed' => array(
- 'set_up' => static function () {
+ 'both_actions_removed' => array(
+ 'set_up' => static function () {
remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
remove_action( 'wp_footer', 'wp_print_footer_scripts' );
},
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => $common_expected_head_styles,
+ 'BODY' => array(),
+ ),
),
- 'block_library_removed' => array(
- 'set_up' => static function () {
- wp_deregister_style( 'wp-block-library' );
+ 'disable_block_library' => array(
+ 'set_up' => static function () {
+ add_action(
+ 'enqueue_block_assets',
+ function (): void {
+ wp_deregister_style( 'wp-block-library' );
+ wp_register_style( 'wp-block-library', '' );
+ }
+ );
+ add_filter( 'should_load_separate_core_block_assets', '__return_false' );
},
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => array(
+ 'wp-img-auto-sizes-contain-inline-css',
+ 'early-css',
+ 'early-inline-css',
+ 'wp-emoji-styles-inline-css',
+ 'classic-theme-styles-css',
+ 'global-styles-inline-css',
+ 'normal-css',
+ 'normal-inline-css',
+ 'wp-custom-css',
+ ),
+ 'BODY' => array(
+ 'late-css',
+ 'late-inline-css',
+ 'core-block-supports-inline-css',
+ ),
+ ),
+ ),
+ 'override_block_library_inline_style_late' => array(
+ 'set_up' => static function () {
+ add_action(
+ 'enqueue_block_assets',
+ function (): void {
+ // This tests what happens when the placeholder comment gets replaced unexpectedly.
+ wp_styles()->registered['wp-block-library']->extra['after'] = array( '/* OVERRIDDEN! */' );
+ }
+ );
+ },
+ 'inline_size_limit' => 0,
+ 'expected_styles' => array(
+ 'HEAD' => array(
+ 'wp-img-auto-sizes-contain-inline-css',
+ 'early-css',
+ 'early-inline-css',
+ 'wp-emoji-styles-inline-css',
+ 'wp-block-library-css',
+ 'wp-block-library-inline-css', // This contains the "OVERRIDDEN" text.
+ 'wp-block-separator-css',
+ 'global-styles-inline-css',
+ 'core-block-supports-inline-css',
+ 'classic-theme-styles-css',
+ 'normal-css',
+ 'normal-inline-css',
+ 'wp-custom-css',
+ 'late-css',
+ 'late-inline-css',
+ ),
+ 'BODY' => array(),
+ ),
),
);
}
@@ -1510,26 +1671,68 @@ public function data_wp_hoist_late_printed_styles(): array {
* Tests that wp_hoist_late_printed_styles() adds a placeholder for delayed CSS, then removes it and adds all CSS to the head including late enqueued styles.
*
* @ticket 64099
+ * @covers ::wp_load_classic_theme_block_styles_on_demand
* @covers ::wp_hoist_late_printed_styles
*
* @dataProvider data_wp_hoist_late_printed_styles
*/
- public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
+ public function test_wp_hoist_late_printed_styles( ?Closure $set_up, int $inline_size_limit, array $expected_styles ): void {
+ switch_theme( 'default' );
+ global $wp_styles;
+ $wp_styles = null;
+
+ // Disable the styles_inline_size_limit in order to prevent changes from invalidating the snapshots.
+ add_filter(
+ 'styles_inline_size_limit',
+ static function () use ( $inline_size_limit ): int {
+ return $inline_size_limit;
+ }
+ );
+
+ add_filter(
+ 'wp_get_custom_css',
+ static function () {
+ return '/* CUSTOM CSS from Customizer */';
+ }
+ );
+
if ( $set_up ) {
$set_up();
}
- switch_theme( 'default' );
+ wp_load_classic_theme_block_styles_on_demand();
+
+ // Ensure that separate core block assets get registered.
+ register_core_block_style_handles();
+ $this->assertTrue( WP_Block_Type_Registry::get_instance()->is_registered( 'core/separator' ), 'Expected the core/separator block to be registered.' );
+
+ // Ensure stylesheet files exist on the filesystem since a build may not have been done.
+ $this->ensure_style_asset_file_created(
+ 'wp-block-library',
+ wp_should_load_separate_core_block_assets() ? 'css/dist/block-library/common.css' : 'css/dist/block-library/style.css'
+ );
+ if ( wp_should_load_separate_core_block_assets() ) {
+ $this->ensure_style_asset_file_created( 'wp-block-separator', 'blocks/separator/style.css' );
+ }
+ $this->assertFalse( wp_is_block_theme(), 'Test is not relevant to block themes (only classic themes).' );
- // Enqueue a style
- wp_enqueue_style( 'early', 'http://example.com/style.css' );
+ // Enqueue a style early, before wp_enqueue_scripts.
+ wp_enqueue_style( 'early', 'https://example.com/style.css' );
wp_add_inline_style( 'early', '/* EARLY */' );
- wp_hoist_late_printed_styles();
+ // Enqueue a style at the normal spot.
+ add_action(
+ 'wp_enqueue_scripts',
+ static function () {
+ wp_enqueue_style( 'normal', 'https://example.com/normal.css' );
+ wp_add_inline_style( 'normal', '/* NORMAL */' );
+ }
+ );
- // Ensure late styles are printed.
- add_filter( 'print_late_styles', '__return_false', 1000 );
- $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' );
+ // Call wp_hoist_late_printed_styles() if wp_load_classic_theme_block_styles_on_demand() queued it up.
+ if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) {
+ wp_hoist_late_printed_styles();
+ }
// Simulate wp_head.
$head_output = get_echo( 'wp_head' );
@@ -1537,21 +1740,32 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
$this->assertStringContainsString( 'early', $head_output, 'Expected the early-enqueued stylesheet to be present.' );
// Enqueue a late style (after wp_head).
- wp_enqueue_style( 'late', 'http://example.com/late-style.css', array(), null );
- wp_add_inline_style( 'late', '/* EARLY */' );
+ wp_enqueue_style( 'late', 'https://example.com/late-style.css', array(), null );
+ wp_add_inline_style( 'late', '/* LATE */' );
+
+ // Simulate the_content().
+ $content = apply_filters(
+ 'the_content',
+ '
'
+ );
// Simulate footer scripts.
$footer_output = get_echo( 'wp_footer' );
// Create a simulated output buffer.
- $buffer = '' . $head_output . 'Content' . $footer_output . '';
+ $buffer = '' . $head_output . '' . $content . '' . $footer_output . '';
+
+ $placeholder_regexp = '#/\*wp_block_styles_on_demand_placeholder:[a-f0-9]+\*/#';
+ if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) {
+ $this->assertMatchesRegularExpression( $placeholder_regexp, $buffer, 'Expected the placeholder to be present in the buffer.' );
+ }
// Apply the output buffer filter.
$filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer );
- $this->assertStringContainsString( '', $buffer, 'Expected the closing HEAD tag to be in the response.' );
+ $this->assertStringContainsString( '', $filtered_buffer, 'Expected the closing HEAD tag to be in the response.' );
- $this->assertDoesNotMatchRegularExpression( '#/\*wp_late_styles_placeholder:[a-f0-9-]+\*/#', $filtered_buffer, 'Expected the placeholder to be removed.' );
+ $this->assertDoesNotMatchRegularExpression( $placeholder_regexp, $filtered_buffer, 'Expected the placeholder to be removed.' );
$found_styles = array(
'HEAD' => array(),
'BODY' => array(),
@@ -1569,21 +1783,58 @@ public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
}
}
- $expected = array(
- 'early-css',
- 'early-inline-css',
- 'late-css',
- 'late-inline-css',
+ /*
+ * Since new styles could appear at any time and since certain styles leak in from the global scope not being
+ * properly reset somewhere else in the test suite, we only check that the expected styles are at least present
+ * and in the same order. When new styles are introduced in core, they may be added to this array as opposed to
+ * updating the arrays in the data provider, if appropriate.
+ */
+ $ignored_styles = array(
+ 'core-block-supports-duotone-inline-css',
+ 'wp-block-library-theme-css',
+ 'wp-block-template-skip-link-inline-css',
);
- foreach ( $expected as $style_id ) {
- $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD.' );
+
+ $found_subset_styles = array();
+ foreach ( array( 'HEAD', 'BODY' ) as $group ) {
+ $found_subset_styles[ $group ] = array_values( array_diff( $found_styles[ $group ], $ignored_styles ) );
}
+
$this->assertSame(
- $expected,
- array_values( array_intersect( $found_styles['HEAD'], $expected ) ),
- 'Expected styles to be printed in the same order.'
+ $expected_styles,
+ $found_subset_styles,
+ 'Expected the same styles. Snapshot: ' . self::get_array_snapshot_export( $found_subset_styles )
);
- $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' );
+ }
+
+ /**
+ * Ensures a CSS file is on the filesystem.
+ *
+ * This is needed because unit tests may be run without a build step having been done. Something similar can be seen
+ * elsewhere in tests for the `wp-emoji-loader.js` script:
+ *
+ * self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' );
+ *
+ * @param string $handle Style handle.
+ * @param string $relative_path Relative path to the CSS file in wp-includes.
+ *
+ * @throws Exception If the supplied style handle is not registered as expected.
+ */
+ private function ensure_style_asset_file_created( string $handle, string $relative_path ) {
+ $dependency = wp_styles()->query( $handle );
+ if ( ! $dependency ) {
+ throw new Exception( "The stylesheet for $handle is not registered." );
+ }
+ $dependency->src = includes_url( $relative_path );
+ $path = ABSPATH . WPINC . '/' . $relative_path;
+ if ( ! file_exists( $path ) ) {
+ $dir = dirname( $path );
+ if ( ! file_exists( $dir ) ) {
+ mkdir( $dir, 0777, true );
+ }
+ file_put_contents( $path, "/* CSS for $handle */" );
+ }
+ wp_style_add_data( $handle, 'path', $path );
}
public function assertTemplateHierarchy( $url, array $expected, $message = '' ) {
@@ -1593,6 +1844,49 @@ public function assertTemplateHierarchy( $url, array $expected, $message = '' )
$this->assertSame( $expected, $hierarchy, $message );
}
+ /**
+ * Exports PHP array as string formatted as a snapshot for pasting into a data provider.
+ *
+ * Unfortunately, `var_export()` always includes array indices even for lists. For example:
+ *
+ * var_export( array( 'a', 'b', 'c' ) );
+ *
+ * Results in:
+ *
+ * array (
+ * 0 => 'a',
+ * 1 => 'b',
+ * 2 => 'c',
+ * )
+ *
+ * This makes it unhelpful when outputting a snapshot to update a unit test. So this function strips out the indices
+ * to facilitate copy/pasting the snapshot from an assertion error message into the data provider. For example:
+ *
+ * array(
+ * 'a',
+ * 'b',
+ * 'c',
+ * )
+ *
+ *
+ * @param array $snapshot Snapshot.
+ * @return string Snapshot export.
+ */
+ private static function get_array_snapshot_export( array $snapshot ): string {
+ $export = var_export( $snapshot, true );
+ $export = preg_replace( '/\barray \($/m', 'array(', $export );
+ $export = preg_replace( '/^(\s+)\d+\s+=>\s+/m', '$1', $export );
+ $export = preg_replace( '/=> *\n +/', '=> ', $export );
+ $export = preg_replace( '/array\(\n\s+\)/', 'array()', $export );
+ return preg_replace_callback(
+ '/(^ +)/m',
+ static function ( $matches ) {
+ return str_repeat( "\t", strlen( $matches[0] ) / 2 );
+ },
+ $export
+ );
+ }
+
protected static function get_query_template_conditions() {
return array(
'embed' => 'is_embed',