From 9ac3c9fb2e86889596c93f9242733f020b1f1c4c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 17:27:07 -0800 Subject: [PATCH 1/7] Script Loader: Preserve duplicate query variables in enqueued script and style URLs. Previously, using 'add_query_arg()' to append versions or handle-specific query arguments would strip any existing duplicate query variables in the source URL (common in Google Fonts URLs). This change refactors 'WP_Styles::_css_href()' and 'WP_Scripts::do_item()' to manually append these parameters to the URL string. This ensures all original query variables are preserved exactly as provided. It also improves fragment handling by ensuring query parameters are inserted before any '#' anchor while maintaining the anchor's presence. Regression tests are added to verify preservation of duplicate query variables and fragments using 'WP_HTML_Tag_Processor' for precise attribute verification. Follow-up to r61397. See #64372. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 29 +++-- src/wp-includes/class-wp-styles.php | 29 +++-- tests/phpunit/tests/dependencies/scripts.php | 123 ++++++++++++++----- tests/phpunit/tests/dependencies/styles.php | 61 +++++++++ 4 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 486dca3009489..cddc393f87177 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -417,19 +417,32 @@ public function do_item( $handle, $group = false ) { $src = $this->base_url . $src; } - $query_args = array(); + $added_ver = ''; if ( empty( $obj->ver ) && null !== $obj->ver && is_string( $this->default_version ) ) { - $query_args['ver'] = $this->default_version; + $added_ver = 'ver=' . rawurlencode( $this->default_version ); } elseif ( is_scalar( $obj->ver ) ) { - $query_args['ver'] = (string) $obj->ver; + $added_ver = 'ver=' . rawurlencode( (string) $obj->ver ); } - if ( isset( $this->args[ $handle ] ) ) { - parse_str( $this->args[ $handle ], $parsed_args ); - if ( $parsed_args ) { - $query_args = array_merge( $query_args, $parsed_args ); + + $added_args = isset( $this->args[ $handle ] ) ? (string) $this->args[ $handle ] : ''; + + if ( $added_ver || $added_args ) { + $fragment = strstr( $src, '#' ); + if ( false !== $fragment ) { + $src = substr( $src, 0, -strlen( $fragment ) ); + } + + if ( $added_ver ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_ver; + } + if ( $added_args ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; + } + + if ( false !== $fragment ) { + $src .= $fragment; } } - $src = add_query_arg( rawurlencode_deep( $query_args ), $src ); /** This filter is documented in wp-includes/class-wp-scripts.php */ $src = esc_url_raw( apply_filters( 'script_loader_src', $src, $handle ) ); diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 53437fe23b1e3..132da1a42b355 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -407,19 +407,32 @@ public function _css_href( $src, $ver, $handle ) { $src = $this->base_url . $src; } - $query_args = array(); + $added_ver = ''; if ( empty( $ver ) && null !== $ver && is_string( $this->default_version ) ) { - $query_args['ver'] = $this->default_version; + $added_ver = 'ver=' . rawurlencode( $this->default_version ); } elseif ( is_scalar( $ver ) ) { - $query_args['ver'] = (string) $ver; + $added_ver = 'ver=' . rawurlencode( (string) $ver ); } - if ( isset( $this->args[ $handle ] ) ) { - parse_str( $this->args[ $handle ], $parsed_args ); - if ( $parsed_args ) { - $query_args = array_merge( $query_args, $parsed_args ); + + $added_args = isset( $this->args[ $handle ] ) ? (string) $this->args[ $handle ] : ''; + + if ( $added_ver || $added_args ) { + $fragment = strstr( $src, '#' ); + if ( false !== $fragment ) { + $src = substr( $src, 0, -strlen( $fragment ) ); + } + + if ( $added_ver ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_ver; + } + if ( $added_args ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; + } + + if ( false !== $fragment ) { + $src .= $fragment; } } - $src = add_query_arg( rawurlencode_deep( $query_args ), $src ); /** * Filters an enqueued style's fully-qualified URL. diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 6050983cc5f5e..e2cba68daf096 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -631,7 +631,7 @@ public function data_provider_to_test_various_strategy_dependency_chains() { scriptEventLog.push( "blocking-not-async-without-dependency: before inline" ) //# sourceURL=blocking-not-async-without-dependency-js-before - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + HTML , ), @@ -1031,7 +1031,7 @@ public function data_provider_to_test_various_strategy_dependency_chains() { $this->add_test_inline_script( $handle, 'after' ); }, 'expected_markup' => << + - - + + + - + - - + + + HTML , ), @@ -4192,6 +4192,67 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque $this->assertEqualHTML( $expected, $markup, '', 'Expected equal snapshot for wp_print_scripts() with version ' . var_export( $version, true ) . ":\n$markup" ); } + /** + * Tests that duplicate query vars are preserved in scripts. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_scripts() { + $url = 'https://example.com/script.js?arg=1&arg=2'; + wp_enqueue_script( 'duplicate-args-script', $url, array(), '1.0' ); + + $output = get_echo( 'wp_print_scripts' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'script' ) ); + $this->assertSame( 'https://example.com/script.js?arg=1&arg=2&ver=1.0', $processor->get_attribute( 'src' ) ); + } + + /** + * Tests that duplicate query vars are preserved in scripts when version is null. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_scripts_null_version() { + $url = 'https://example.com/script.js?arg=1&arg=2'; + wp_enqueue_script( 'duplicate-args-script', $url, array(), null ); + + $output = get_echo( 'wp_print_scripts' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'script' ) ); + $this->assertSame( 'https://example.com/script.js?arg=1&arg=2', $processor->get_attribute( 'src' ) ); + } + + /** + * Tests that duplicate query vars in handle args are preserved in scripts. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_handle_args_scripts() { + wp_enqueue_script( 'test-script?a=1&a=2', 'https://example.com/test-script.js', array(), '1.0' ); + $output = get_echo( 'wp_print_scripts' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'script' ) ); + $this->assertSame( 'https://example.com/test-script.js?ver=1.0&a=1&a=2', $processor->get_attribute( 'src' ) ); + } + + /** + * Tests that duplicate query vars and fragments are preserved in scripts. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_and_fragments_preserved_in_scripts() { + $url = 'https://example.com/script.js?arg=1&arg=2#anchor'; + wp_enqueue_script( 'test-fragment', $url, array(), '1.0' ); + $output = get_echo( 'wp_print_scripts' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'script' ) ); + $this->assertSame( 'https://example.com/script.js?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'src' ) ); + } + /** * Data provider for: * - test_varying_versions_added_to_handle_args_enqueued_scripts diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index bbaf9432d8df0..3c0719f6714de 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -961,6 +961,67 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque $this->assertEqualHTML( $expected, $markup, '', 'Expected equal snapshot for wp_print_styles() with version ' . var_export( $version, true ) . ":\n$markup" ); } + /** + * Tests that duplicate query vars are preserved in styles. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_styles() { + $url = 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap'; + wp_enqueue_style( 'fonts-source-google', $url, array(), '1.0' ); + + $output = get_echo( 'wp_print_styles' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'link' ) ); + $this->assertSame( 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap&ver=1.0', $processor->get_attribute( 'href' ) ); + } + + /** + * Tests that duplicate query vars are preserved in styles when version is null. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_styles_null_version() { + $url = 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap'; + wp_enqueue_style( 'fonts-source-google', $url, array(), null ); + + $output = get_echo( 'wp_print_styles' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'link' ) ); + $this->assertSame( 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', $processor->get_attribute( 'href' ) ); + } + + /** + * Tests that duplicate query vars in handle args are preserved in styles. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_preserved_in_handle_args_styles() { + wp_enqueue_style( 'test-style?a=1&a=2', 'https://example.com/test-style.css', array(), '1.0' ); + $output = get_echo( 'wp_print_styles' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'link' ) ); + $this->assertSame( 'https://example.com/test-style.css?ver=1.0&a=1&a=2', $processor->get_attribute( 'href' ) ); + } + + /** + * Tests that duplicate query vars and fragments are preserved in styles. + * + * @ticket 64372 + */ + public function test_duplicate_query_vars_and_fragments_preserved_in_styles() { + $url = 'https://example.com/style.css?arg=1&arg=2#anchor'; + wp_enqueue_style( 'test-fragment', $url, array(), '1.0' ); + $output = get_echo( 'wp_print_styles' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'link' ) ); + $this->assertSame( 'https://example.com/style.css?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'href' ) ); + } + /** * Data provider for: * - test_varying_versions_added_to_handle_args_enqueued_styles From 04c71260a9735f18c83961b0d21d3a6f49a531c8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 17:37:57 -0800 Subject: [PATCH 2/7] Use null coalescing operator for '' assignment. Refactors the assignment of '' in 'WP_Styles::_css_href()' and 'WP_Scripts::do_item()' to use the more concise null coalescing operator. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 2 +- src/wp-includes/class-wp-styles.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index cddc393f87177..77b3f279046b3 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -424,7 +424,7 @@ public function do_item( $handle, $group = false ) { $added_ver = 'ver=' . rawurlencode( (string) $obj->ver ); } - $added_args = isset( $this->args[ $handle ] ) ? (string) $this->args[ $handle ] : ''; + $added_args = (string) ( $this->args[ $handle ] ?? '' ); if ( $added_ver || $added_args ) { $fragment = strstr( $src, '#' ); diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 132da1a42b355..e137e09847b4a 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -414,7 +414,7 @@ public function _css_href( $src, $ver, $handle ) { $added_ver = 'ver=' . rawurlencode( (string) $ver ); } - $added_args = isset( $this->args[ $handle ] ) ? (string) $this->args[ $handle ] : ''; + $added_args = (string) ( $this->args[ $handle ] ?? '' ); if ( $added_ver || $added_args ) { $fragment = strstr( $src, '#' ); From 15dec30cb6015b8c3fbfc23759eff58f675ea715 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 17:41:27 -0800 Subject: [PATCH 3/7] Refactor version handling to reduce redundancy. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 12 ++++++------ src/wp-includes/class-wp-styles.php | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 77b3f279046b3..83a2cf6266c2d 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -417,23 +417,23 @@ public function do_item( $handle, $group = false ) { $src = $this->base_url . $src; } - $added_ver = ''; + $ver_to_add = ''; if ( empty( $obj->ver ) && null !== $obj->ver && is_string( $this->default_version ) ) { - $added_ver = 'ver=' . rawurlencode( $this->default_version ); + $ver_to_add = $this->default_version; } elseif ( is_scalar( $obj->ver ) ) { - $added_ver = 'ver=' . rawurlencode( (string) $obj->ver ); + $ver_to_add = (string) $obj->ver; } $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( $added_ver || $added_args ) { + if ( '' !== $ver_to_add || $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( $added_ver ) { - $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_ver; + if ( '' !== $ver_to_add ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } if ( $added_args ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index e137e09847b4a..c8687c8dbf156 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -407,23 +407,23 @@ public function _css_href( $src, $ver, $handle ) { $src = $this->base_url . $src; } - $added_ver = ''; + $ver_to_add = ''; if ( empty( $ver ) && null !== $ver && is_string( $this->default_version ) ) { - $added_ver = 'ver=' . rawurlencode( $this->default_version ); + $ver_to_add = $this->default_version; } elseif ( is_scalar( $ver ) ) { - $added_ver = 'ver=' . rawurlencode( (string) $ver ); + $ver_to_add = (string) $ver; } $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( $added_ver || $added_args ) { + if ( '' !== $ver_to_add || $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( $added_ver ) { - $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_ver; + if ( '' !== $ver_to_add ) { + $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } if ( $added_args ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; From 9ee6eb739726ad5ff87610b71f8091fee24a316a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 17:43:53 -0800 Subject: [PATCH 4/7] Use truthy checks for version and added args. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 4 ++-- src/wp-includes/class-wp-styles.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 83a2cf6266c2d..cc94038ad1d19 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -426,13 +426,13 @@ public function do_item( $handle, $group = false ) { $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( '' !== $ver_to_add || $added_args ) { + if ( $ver_to_add || $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( '' !== $ver_to_add ) { + if ( $ver_to_add ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } if ( $added_args ) { diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index c8687c8dbf156..c3ab2d6129a8a 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -416,13 +416,13 @@ public function _css_href( $src, $ver, $handle ) { $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( '' !== $ver_to_add || $added_args ) { + if ( $ver_to_add || $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( '' !== $ver_to_add ) { + if ( $ver_to_add ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } if ( $added_args ) { From 84d1ef4c0832fd58f2c5b3085da7d2e7973f0e8b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Mar 2026 22:46:10 -0800 Subject: [PATCH 5/7] Use explicit empty string checks to preserve '0' query arg. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 6 +++--- src/wp-includes/class-wp-styles.php | 6 +++--- tests/phpunit/tests/dependencies/scripts.php | 14 ++++++++++++++ tests/phpunit/tests/dependencies/styles.php | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index cc94038ad1d19..b098de7e20e56 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -426,16 +426,16 @@ public function do_item( $handle, $group = false ) { $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( $ver_to_add || $added_args ) { + if ( '' !== $ver_to_add || '' !== $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( $ver_to_add ) { + if ( '' !== $ver_to_add ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } - if ( $added_args ) { + if ( '' !== $added_args ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; } diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index c3ab2d6129a8a..1edaeedb2660b 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -416,16 +416,16 @@ public function _css_href( $src, $ver, $handle ) { $added_args = (string) ( $this->args[ $handle ] ?? '' ); - if ( $ver_to_add || $added_args ) { + if ( '' !== $ver_to_add || '' !== $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } - if ( $ver_to_add ) { + if ( '' !== $ver_to_add ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } - if ( $added_args ) { + if ( '' !== $added_args ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; } diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index e2cba68daf096..cb3599459d0a0 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4253,6 +4253,20 @@ public function test_duplicate_query_vars_and_fragments_preserved_in_scripts() { $this->assertSame( 'https://example.com/script.js?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'src' ) ); } + /** + * Tests that '0' as a query arg in a handle is preserved in scripts. + * + * @ticket 64372 + */ + public function test_zero_query_var_preserved_in_handle_args_scripts() { + wp_enqueue_script( 'test-script?0', 'https://example.com/test-script.js', array(), '1.0' ); + $output = get_echo( 'wp_print_scripts' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'script' ) ); + $this->assertSame( 'https://example.com/test-script.js?ver=1.0&0', $processor->get_attribute( 'src' ) ); + } + /** * Data provider for: * - test_varying_versions_added_to_handle_args_enqueued_scripts diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index 3c0719f6714de..7e850189f1517 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -1022,6 +1022,20 @@ public function test_duplicate_query_vars_and_fragments_preserved_in_styles() { $this->assertSame( 'https://example.com/style.css?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'href' ) ); } + /** + * Tests that '0' as a query arg in a handle is preserved in styles. + * + * @ticket 64372 + */ + public function test_zero_query_var_preserved_in_handle_args_styles() { + wp_enqueue_style( 'test-style?0', 'https://example.com/test-style.css', array(), '1.0' ); + $output = get_echo( 'wp_print_styles' ); + $processor = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $processor->next_tag( 'link' ) ); + $this->assertSame( 'https://example.com/test-style.css?ver=1.0&0', $processor->get_attribute( 'href' ) ); + } + /** * Data provider for: * - test_varying_versions_added_to_handle_args_enqueued_styles From d8a07c1d918bd0075a970854f5cabc04aa69f1b3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 5 Mar 2026 08:25:29 -0800 Subject: [PATCH 6/7] Refactor regression tests into single methods with data providers. Consolidates redundant duplicate query variable and fragment tests into consolidated methods with associative data providers. Adds native PHP type hints for parameters and return values, along with PHPStan array shape documentation. Also applies standard WordPress code formatting. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/phpunit/tests/dependencies/scripts.php | 99 ++++++++------------ tests/phpunit/tests/dependencies/styles.php | 99 ++++++++------------ 2 files changed, 82 insertions(+), 116 deletions(-) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index cb3599459d0a0..fc4f78300474d 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4193,78 +4193,61 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque } /** - * Tests that duplicate query vars are preserved in scripts. - * - * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_scripts() { - $url = 'https://example.com/script.js?arg=1&arg=2'; - wp_enqueue_script( 'duplicate-args-script', $url, array(), '1.0' ); - - $output = get_echo( 'wp_print_scripts' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'script' ) ); - $this->assertSame( 'https://example.com/script.js?arg=1&arg=2&ver=1.0', $processor->get_attribute( 'src' ) ); - } - - /** - * Tests that duplicate query vars are preserved in scripts when version is null. + * Tests that duplicate query vars and fragments are preserved in scripts. * * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_scripts_null_version() { - $url = 'https://example.com/script.js?arg=1&arg=2'; - wp_enqueue_script( 'duplicate-args-script', $url, array(), null ); - - $output = get_echo( 'wp_print_scripts' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'script' ) ); - $this->assertSame( 'https://example.com/script.js?arg=1&arg=2', $processor->get_attribute( 'src' ) ); - } - - /** - * Tests that duplicate query vars in handle args are preserved in scripts. * - * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_handle_args_scripts() { - wp_enqueue_script( 'test-script?a=1&a=2', 'https://example.com/test-script.js', array(), '1.0' ); - $output = get_echo( 'wp_print_scripts' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'script' ) ); - $this->assertSame( 'https://example.com/test-script.js?ver=1.0&a=1&a=2', $processor->get_attribute( 'src' ) ); - } - - /** - * Tests that duplicate query vars and fragments are preserved in scripts. + * @dataProvider data_duplicate_query_vars_and_fragments_preserved_in_scripts * - * @ticket 64372 + * @param string $src The script's source URL. + * @param string|null $ver The script's version. + * @param string $expected_url The expected URL. + * @param string $handle Optional. The script's registered handle. Default 'test-script'. */ - public function test_duplicate_query_vars_and_fragments_preserved_in_scripts() { - $url = 'https://example.com/script.js?arg=1&arg=2#anchor'; - wp_enqueue_script( 'test-fragment', $url, array(), '1.0' ); + public function test_duplicate_query_vars_and_fragments_preserved_in_scripts( string $src, ?string $ver, string $expected_url, string $handle = 'test-script' ): void { + wp_enqueue_script( $handle, $src, array(), $ver ); $output = get_echo( 'wp_print_scripts' ); $processor = new WP_HTML_Tag_Processor( $output ); $this->assertTrue( $processor->next_tag( 'script' ) ); - $this->assertSame( 'https://example.com/script.js?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'src' ) ); + $this->assertSame( $expected_url, $processor->get_attribute( 'src' ) ); } /** - * Tests that '0' as a query arg in a handle is preserved in scripts. + * Data provider for test_duplicate_query_vars_and_fragments_preserved_in_scripts. * - * @ticket 64372 + * @return array Data provider. */ - public function test_zero_query_var_preserved_in_handle_args_scripts() { - wp_enqueue_script( 'test-script?0', 'https://example.com/test-script.js', array(), '1.0' ); - $output = get_echo( 'wp_print_scripts' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'script' ) ); - $this->assertSame( 'https://example.com/test-script.js?ver=1.0&0', $processor->get_attribute( 'src' ) ); + public function data_duplicate_query_vars_and_fragments_preserved_in_scripts(): array { + return array( + 'duplicate query vars' => array( + 'src' => 'https://example.com/script.js?arg=1&arg=2', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/script.js?arg=1&arg=2&ver=1.0', + ), + 'duplicate query vars, null version' => array( + 'src' => 'https://example.com/script.js?arg=1&arg=2', + 'ver' => null, + 'expected_url' => 'https://example.com/script.js?arg=1&arg=2', + ), + 'duplicate query vars in handle' => array( + 'src' => 'https://example.com/test-script.js', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/test-script.js?ver=1.0&a=1&a=2', + 'handle' => 'test-script?a=1&a=2', + ), + 'duplicate query vars and fragments' => array( + 'src' => 'https://example.com/script.js?arg=1&arg=2#anchor', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/script.js?arg=1&arg=2&ver=1.0#anchor', + ), + 'zero query var in handle' => array( + 'src' => 'https://example.com/test-script.js', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/test-script.js?ver=1.0&0', + 'handle' => 'test-script?0', + ), + ); } /** diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index 7e850189f1517..afa1583279311 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -962,78 +962,61 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque } /** - * Tests that duplicate query vars are preserved in styles. - * - * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_styles() { - $url = 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap'; - wp_enqueue_style( 'fonts-source-google', $url, array(), '1.0' ); - - $output = get_echo( 'wp_print_styles' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'link' ) ); - $this->assertSame( 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap&ver=1.0', $processor->get_attribute( 'href' ) ); - } - - /** - * Tests that duplicate query vars are preserved in styles when version is null. + * Tests that duplicate query vars and fragments are preserved in styles. * * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_styles_null_version() { - $url = 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap'; - wp_enqueue_style( 'fonts-source-google', $url, array(), null ); - - $output = get_echo( 'wp_print_styles' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'link' ) ); - $this->assertSame( 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', $processor->get_attribute( 'href' ) ); - } - - /** - * Tests that duplicate query vars in handle args are preserved in styles. * - * @ticket 64372 - */ - public function test_duplicate_query_vars_preserved_in_handle_args_styles() { - wp_enqueue_style( 'test-style?a=1&a=2', 'https://example.com/test-style.css', array(), '1.0' ); - $output = get_echo( 'wp_print_styles' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'link' ) ); - $this->assertSame( 'https://example.com/test-style.css?ver=1.0&a=1&a=2', $processor->get_attribute( 'href' ) ); - } - - /** - * Tests that duplicate query vars and fragments are preserved in styles. + * @dataProvider data_duplicate_query_vars_and_fragments_preserved_in_styles * - * @ticket 64372 + * @param string $src The stylesheet's source URL. + * @param string|null $ver The style's version. + * @param string $expected_url The expected URL. + * @param string $handle Optional. The style's registered handle. Default 'test-style'. */ - public function test_duplicate_query_vars_and_fragments_preserved_in_styles() { - $url = 'https://example.com/style.css?arg=1&arg=2#anchor'; - wp_enqueue_style( 'test-fragment', $url, array(), '1.0' ); + public function test_duplicate_query_vars_and_fragments_preserved_in_styles( string $src, ?string $ver, string $expected_url, string $handle = 'test-style' ): void { + wp_enqueue_style( $handle, $src, array(), $ver ); $output = get_echo( 'wp_print_styles' ); $processor = new WP_HTML_Tag_Processor( $output ); $this->assertTrue( $processor->next_tag( 'link' ) ); - $this->assertSame( 'https://example.com/style.css?arg=1&arg=2&ver=1.0#anchor', $processor->get_attribute( 'href' ) ); + $this->assertSame( $expected_url, $processor->get_attribute( 'href' ) ); } /** - * Tests that '0' as a query arg in a handle is preserved in styles. + * Data provider for test_duplicate_query_vars_and_fragments_preserved_in_styles. * - * @ticket 64372 + * @return array Data provider. */ - public function test_zero_query_var_preserved_in_handle_args_styles() { - wp_enqueue_style( 'test-style?0', 'https://example.com/test-style.css', array(), '1.0' ); - $output = get_echo( 'wp_print_styles' ); - $processor = new WP_HTML_Tag_Processor( $output ); - - $this->assertTrue( $processor->next_tag( 'link' ) ); - $this->assertSame( 'https://example.com/test-style.css?ver=1.0&0', $processor->get_attribute( 'href' ) ); + public function data_duplicate_query_vars_and_fragments_preserved_in_styles(): array { + return array( + 'duplicate query vars' => array( + 'src' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', + 'ver' => '1.0', + 'expected_url' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap&ver=1.0', + ), + 'duplicate query vars, null version' => array( + 'src' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', + 'ver' => null, + 'expected_url' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', + ), + 'duplicate query vars in handle' => array( + 'src' => 'https://example.com/test-style.css', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/test-style.css?ver=1.0&a=1&a=2', + 'handle' => 'test-style?a=1&a=2', + ), + 'duplicate query vars and fragments' => array( + 'src' => 'https://example.com/style.css?arg=1&arg=2#anchor', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/style.css?arg=1&arg=2&ver=1.0#anchor', + ), + 'zero query var in handle' => array( + 'src' => 'https://example.com/test-style.css', + 'ver' => '1.0', + 'expected_url' => 'https://example.com/test-style.css?ver=1.0&0', + 'handle' => 'test-style?0', + ), + ); } /** From 871d8fbe838255d7bea4070207d30f620e691d4b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 5 Mar 2026 08:28:34 -0800 Subject: [PATCH 7/7] Tests: Add 'false' version cases to duplicate query var data providers. Updates regression tests to include coverage for 'false' version strings, which trigger the default WordPress version. Also refines parameter type hints to accurately reflect allowed 'string|bool|null' values. Co-authored-by: gemini-cli <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/phpunit/tests/dependencies/scripts.php | 29 ++++++++++++-------- tests/phpunit/tests/dependencies/styles.php | 29 ++++++++++++-------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index fc4f78300474d..5f1c30fe4cf47 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4199,12 +4199,12 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque * * @dataProvider data_duplicate_query_vars_and_fragments_preserved_in_scripts * - * @param string $src The script's source URL. - * @param string|null $ver The script's version. - * @param string $expected_url The expected URL. - * @param string $handle Optional. The script's registered handle. Default 'test-script'. + * @param string $src The script's source URL. + * @param string|bool|null $ver The script's version. + * @param string $expected_url The expected URL. + * @param string $handle Optional. The script's registered handle. Default 'test-script'. */ - public function test_duplicate_query_vars_and_fragments_preserved_in_scripts( string $src, ?string $ver, string $expected_url, string $handle = 'test-script' ): void { + public function test_duplicate_query_vars_and_fragments_preserved_in_scripts( string $src, $ver, string $expected_url, string $handle = 'test-script' ): void { wp_enqueue_script( $handle, $src, array(), $ver ); $output = get_echo( 'wp_print_scripts' ); $processor = new WP_HTML_Tag_Processor( $output ); @@ -4216,32 +4216,39 @@ public function test_duplicate_query_vars_and_fragments_preserved_in_scripts( st /** * Data provider for test_duplicate_query_vars_and_fragments_preserved_in_scripts. * - * @return array Data provider. + * @return array Data provider. */ public function data_duplicate_query_vars_and_fragments_preserved_in_scripts(): array { + $ver = get_bloginfo( 'version' ); + return array( - 'duplicate query vars' => array( + 'duplicate query vars' => array( 'src' => 'https://example.com/script.js?arg=1&arg=2', 'ver' => '1.0', 'expected_url' => 'https://example.com/script.js?arg=1&arg=2&ver=1.0', ), - 'duplicate query vars, null version' => array( + 'duplicate query vars, null version' => array( 'src' => 'https://example.com/script.js?arg=1&arg=2', 'ver' => null, 'expected_url' => 'https://example.com/script.js?arg=1&arg=2', ), - 'duplicate query vars in handle' => array( + 'duplicate query vars, false version' => array( + 'src' => 'https://example.com/script.js?arg=1&arg=2', + 'ver' => false, + 'expected_url' => "https://example.com/script.js?arg=1&arg=2&ver=$ver", + ), + 'duplicate query vars in handle' => array( 'src' => 'https://example.com/test-script.js', 'ver' => '1.0', 'expected_url' => 'https://example.com/test-script.js?ver=1.0&a=1&a=2', 'handle' => 'test-script?a=1&a=2', ), - 'duplicate query vars and fragments' => array( + 'duplicate query vars and fragments' => array( 'src' => 'https://example.com/script.js?arg=1&arg=2#anchor', 'ver' => '1.0', 'expected_url' => 'https://example.com/script.js?arg=1&arg=2&ver=1.0#anchor', ), - 'zero query var in handle' => array( + 'zero query var in handle' => array( 'src' => 'https://example.com/test-script.js', 'ver' => '1.0', 'expected_url' => 'https://example.com/test-script.js?ver=1.0&0', diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index afa1583279311..de6fefb94b5bb 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -968,12 +968,12 @@ public function test_varying_versions_added_to_handle_args_registered_then_enque * * @dataProvider data_duplicate_query_vars_and_fragments_preserved_in_styles * - * @param string $src The stylesheet's source URL. - * @param string|null $ver The style's version. - * @param string $expected_url The expected URL. - * @param string $handle Optional. The style's registered handle. Default 'test-style'. + * @param string $src The stylesheet's source URL. + * @param string|bool|null $ver The style's version. + * @param string $expected_url The expected URL. + * @param string $handle Optional. The style's registered handle. Default 'test-style'. */ - public function test_duplicate_query_vars_and_fragments_preserved_in_styles( string $src, ?string $ver, string $expected_url, string $handle = 'test-style' ): void { + public function test_duplicate_query_vars_and_fragments_preserved_in_styles( string $src, $ver, string $expected_url, string $handle = 'test-style' ): void { wp_enqueue_style( $handle, $src, array(), $ver ); $output = get_echo( 'wp_print_styles' ); $processor = new WP_HTML_Tag_Processor( $output ); @@ -985,32 +985,39 @@ public function test_duplicate_query_vars_and_fragments_preserved_in_styles( str /** * Data provider for test_duplicate_query_vars_and_fragments_preserved_in_styles. * - * @return array Data provider. + * @return array Data provider. */ public function data_duplicate_query_vars_and_fragments_preserved_in_styles(): array { + $ver = get_bloginfo( 'version' ); + return array( - 'duplicate query vars' => array( + 'duplicate query vars' => array( 'src' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', 'ver' => '1.0', 'expected_url' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap&ver=1.0', ), - 'duplicate query vars, null version' => array( + 'duplicate query vars, null version' => array( 'src' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', 'ver' => null, 'expected_url' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', ), - 'duplicate query vars in handle' => array( + 'duplicate query vars, false version' => array( + 'src' => 'https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap', + 'ver' => false, + 'expected_url' => "https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Montserrat:wght@700&display=swap&ver=$ver", + ), + 'duplicate query vars in handle' => array( 'src' => 'https://example.com/test-style.css', 'ver' => '1.0', 'expected_url' => 'https://example.com/test-style.css?ver=1.0&a=1&a=2', 'handle' => 'test-style?a=1&a=2', ), - 'duplicate query vars and fragments' => array( + 'duplicate query vars and fragments' => array( 'src' => 'https://example.com/style.css?arg=1&arg=2#anchor', 'ver' => '1.0', 'expected_url' => 'https://example.com/style.css?arg=1&arg=2&ver=1.0#anchor', ), - 'zero query var in handle' => array( + 'zero query var in handle' => array( 'src' => 'https://example.com/test-style.css', 'ver' => '1.0', 'expected_url' => 'https://example.com/test-style.css?ver=1.0&0',