From 0789f1424330429b94cd57d05ab82cfba436c3cf Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 22 Sep 2026 16:39:53 -0500 Subject: [PATCH 01/16] Experiment with admin notices --- src/wp-admin/includes/admin-filters.php | 4 ++++ src/wp-includes/functions.php | 29 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 5337cc02c88c9..74a974663dbd8 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -160,6 +160,10 @@ // Show a "postbox" with the text suggestions for a privacy policy. add_action( 'admin_notices', array( 'WP_Privacy_Policy_Content', 'notice' ) ); +// Generate a wrapper for admin notices. +add_action( 'admin_notices', 'wp_admin_notice_wrapper_start', 1 ); +add_action( 'admin_notices', 'wp_admin_notice_wrapper', 99999 ); + // Add the suggested policy text from WordPress. add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'add_suggested_content' ), 1 ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 89d27f75e9f58..686da5eaa8c35 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9388,6 +9388,35 @@ function wp_admin_notice( $message, $args = array() ) { echo wp_kses_post( wp_get_admin_notice( $message, $args ) ); } +/** + * Buffer all admin notices. + * + * @since 7.2.0 + */ +function wp_admin_notice_wrapper_start() { + ob_start(); +} + +/** + * Outputs the buffered admin notices inside a wrapper. + * + * @since 7.2.0 + */ +function wp_admin_notice_wrapper() { + ob_start(); + $all_notices = ob_get_clean(); + if ( $all_notices ) { + $tags = new WP_HTML_Tag_Processor( $all_notices ); + $notice_count = 0; + while ( $tags->next_tag( '.notice' ) ) { + ++$notice_count; + } + // translators: %d is the number of admin notices found. + echo ''; + return; + } +} + /** * Checks if a mime type is for a HEIC/HEIF image. * From f890f4c1c85fbef00fcfa948bd55786af851b5e5 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 22 Sep 2026 17:24:34 -0500 Subject: [PATCH 02/16] Capture notices & add to title. This can also be used potentially to get notices to a separate display context. Captures all non-empty messages. Messages with no text are assumed to be placeholders for JS-driven messages to be generated later. --- src/wp-admin/admin.php | 2 + src/wp-admin/includes/admin-filters.php | 4 - .../class-wp-admin-notice-collector.php | 165 ++++++++++++++++++ src/wp-includes/functions.php | 29 --- src/wp-settings.php | 1 + 5 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 src/wp-includes/class-wp-admin-notice-collector.php diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 82ab6b93ac99e..b9eff4d52b342 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -101,6 +101,8 @@ require_once ABSPATH . 'wp-admin/includes/admin.php'; +WP_Admin_Notice_Collector::init(); + auth_redirect(); // Schedule Trash collection. diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 74a974663dbd8..5337cc02c88c9 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -160,10 +160,6 @@ // Show a "postbox" with the text suggestions for a privacy policy. add_action( 'admin_notices', array( 'WP_Privacy_Policy_Content', 'notice' ) ); -// Generate a wrapper for admin notices. -add_action( 'admin_notices', 'wp_admin_notice_wrapper_start', 1 ); -add_action( 'admin_notices', 'wp_admin_notice_wrapper', 99999 ); - // Add the suggested policy text from WordPress. add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'add_suggested_content' ), 1 ); diff --git a/src/wp-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php new file mode 100644 index 0000000000000..f38ab39acb0ce --- /dev/null +++ b/src/wp-includes/class-wp-admin-notice-collector.php @@ -0,0 +1,165 @@ +` tag, + * the admin page title is modified via the {@see 'admin_title'} + * filter, the admin page is wrapped in an output buffer, and an + * `ob_start()` callback replaces the placeholder with the final count once + * the buffer is flushed. + * + * @since x.x.x + */ +class WP_Admin_Notice_Collector { + + /** + * Placeholder inserted into the admin title, replaced with the + * actual notice count once it is known. + * + * @since x.x.x + * @var string + */ + const TITLE_PLACEHOLDER = '%%wp_admin_notice_count%%'; + + /** + * Notices captured during the current request. + * + * @since x.x.x + * @var array[] + */ + protected static $notices = array(); + + /** + * Whether the collector's hooks have already been registered. + * + * @since x.x.x + * @var bool + */ + protected static $hooked = false; + + /** + * Registers the hooks used to collect notices and to annotate the admin title. + * + * @since x.x.x + */ + public static function init() { + if ( self::$hooked ) { + return; + } + + self::$hooked = true; + + add_filter( 'wp_admin_notice_markup', array( __CLASS__, 'capture_notice' ), 10, 3 ); + + // Ajax/REST admin requests don't render a , so skip the buffer there. + if ( is_admin() && ! wp_doing_ajax() ) { + add_filter( 'admin_title', array( __CLASS__, 'add_title_placeholder' ), 10, 2 ); + ob_start( array( __CLASS__, 'inject_notice_count' ) ); + } + } + + /** + * Captures a single admin notice without altering its markup. + * + * Hooked to the {@see 'wp_admin_notice_markup'} filter. + * + * @since x.x.x + * + * @param string $markup The HTML markup for the admin notice. + * @param string $message The message for the admin notice. + * @param array $args The arguments for the admin notice. + * @return string The unmodified markup, so the filter remains transparent. + */ + public static function capture_notice( $markup, $message, $args ) { + if ( '' !== trim( wp_strip_all_tags( $message ) ) ) { + self::$notices[] = array( + 'markup' => $markup, + 'message' => $message, + 'args' => $args, + ); + } + + return $markup; + } + + /** + * Returns all notices captured during the current request. + * + * @since x.x.x + * + * @return array[] Array of captured notices. Each entry contains the + * notice's 'markup', 'message', and 'args'. + */ + public static function get_notices() { + return self::$notices; + } + + /** + * Returns the number of notices captured during the current request. + * + * @since x.x.x + * + * @return int Notice count. + */ + public static function get_notice_count() { + return count( self::$notices ); + } + + /** + * Clears all previously captured notices. + * + * @since x.x.x + */ + public static function reset() { + self::$notices = array(); + } + + /** + * Prepends a title placeholder via the {@see 'admin_title'} filter. + * + * The real count isn't known yet at this point, since notices are usually + * produced later in the page. inject_notice_count() resolves it. + * + * @since x.x.x + * + * @param string $admin_title The page title, with extra context added. + * @param string $title The original page title. + * @return string The admin title with the placeholder prepended. + */ + public static function add_title_placeholder( $admin_title, $title ) { + return self::TITLE_PLACEHOLDER . $admin_title; + } + + /** + * Replaces the title placeholder with the final notice count. + * + * Used as an `ob_start()` callback so it runs once the full page - including + * any notices printed after `<title>` - has been buffered. + * + * @since x.x.x + * + * @param string $buffer The full buffered page output. + * @return string The page output with the placeholder resolved. + */ + public static function inject_notice_count( $buffer ) { + $count = self::get_notice_count(); + + if ( 0 === $count ) { + return str_replace( self::TITLE_PLACEHOLDER, '', $buffer ); + } + + /* translators: %d: Number of admin notices on the current page. */ + $prefix = sprintf( _n( '(%d notice) ', '(%d notices) ', $count ), $count ); + + return str_replace( self::TITLE_PLACEHOLDER, $prefix, $buffer ); + } +} diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 686da5eaa8c35..89d27f75e9f58 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9388,35 +9388,6 @@ function wp_admin_notice( $message, $args = array() ) { echo wp_kses_post( wp_get_admin_notice( $message, $args ) ); } -/** - * Buffer all admin notices. - * - * @since 7.2.0 - */ -function wp_admin_notice_wrapper_start() { - ob_start(); -} - -/** - * Outputs the buffered admin notices inside a wrapper. - * - * @since 7.2.0 - */ -function wp_admin_notice_wrapper() { - ob_start(); - $all_notices = ob_get_clean(); - if ( $all_notices ) { - $tags = new WP_HTML_Tag_Processor( $all_notices ); - $notice_count = 0; - while ( $tags->next_tag( '.notice' ) ) { - ++$notice_count; - } - // translators: %d is the number of admin notices found. - echo '<aside id="wp-admin-notice-wrapper" aria-label="' . sprintf( _n( 'Notice (%d)', 'Notices (%d)', $notice_count ), $notice_count ) . '">' . $all_notices . '</aside>'; - return; - } -} - /** * Checks if a mime type is for a HEIC/HEIF image. * diff --git a/src/wp-settings.php b/src/wp-settings.php index 1d55db83e2d85..8c9ea4a86be37 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -115,6 +115,7 @@ require ABSPATH . WPINC . '/formatting.php'; require ABSPATH . WPINC . '/meta.php'; require ABSPATH . WPINC . '/functions.php'; +require ABSPATH . WPINC . '/class-wp-admin-notice-collector.php'; require ABSPATH . WPINC . '/class-wp-meta-query.php'; require ABSPATH . WPINC . '/class-wp-matchesmapregex.php'; require ABSPATH . WPINC . '/class-wp.php'; From 6a28fd65651405b10613625ab53c53594c54fd2b Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Tue, 22 Sep 2026 17:42:39 -0500 Subject: [PATCH 03/16] Move notices inside an `aside`. --- src/js/_enqueues/admin/common.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index fe83cc7e9f2ae..db9103d47b9af 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1084,7 +1084,11 @@ $( function() { if ( ! $headerEnd.length ) { $headerEnd = $( '.wrap h1, .wrap h2' ).first(); } - $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd ); + var $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); + if ( $notices.length ) { + var $wpAdminNotices = $( '<aside id="wp-admin-notices" aria-label="' + esc_attr__( 'Admin Notices' ) + '"></aside>' ).insertAfter( $headerEnd ); + $notices.appendTo( $wpAdminNotices ); + } /** * Makes notices dismissible. From ff55f84488c2453940fcdc2467cc8fc7caf61a0c Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Tue, 22 Sep 2026 17:57:08 -0500 Subject: [PATCH 04/16] Ha. That's not PHP... --- src/js/_enqueues/admin/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index db9103d47b9af..fb2120acc4f44 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1086,7 +1086,7 @@ $( function() { } var $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); if ( $notices.length ) { - var $wpAdminNotices = $( '<aside id="wp-admin-notices" aria-label="' + esc_attr__( 'Admin Notices' ) + '"></aside>' ).insertAfter( $headerEnd ); + var $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); $notices.appendTo( $wpAdminNotices ); } From 1d25d9a6f8a4cb8fdb29e88aa093c576874c1b4d Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Tue, 22 Sep 2026 17:58:05 -0500 Subject: [PATCH 05/16] Update class-wp-admin-notice-collector.php --- src/wp-includes/class-wp-admin-notice-collector.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php index f38ab39acb0ce..d4da2f9e5f394 100644 --- a/src/wp-includes/class-wp-admin-notice-collector.php +++ b/src/wp-includes/class-wp-admin-notice-collector.php @@ -10,11 +10,11 @@ /** * Capture admin notices generated in PHP during a request. * - * Notices are captured via the {@see 'wp_admin_notice_markup'} filter. - * a title placeholder is inserted into the admin page's `<title>` tag, - * the admin page title is modified via the {@see 'admin_title'} - * filter, the admin page is wrapped in an output buffer, and an - * `ob_start()` callback replaces the placeholder with the final count once + * Notices are captured via the {@see 'wp_admin_notice_markup'} filter. + * a title placeholder is inserted into the admin page's `<title>` tag, + * the admin page title is modified via the {@see 'admin_title'} + * filter, the admin page is wrapped in an output buffer, and an + * `ob_start()` callback replaces the placeholder with the final count once * the buffer is flushed. * * @since x.x.x From 300b858cbf156c3b673999fd0b4b83a953601b03 Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 25 Sep 2026 17:35:26 -0500 Subject: [PATCH 06/16] Make moveAdminNotices() as a named function. --- src/js/_enqueues/admin/common.js | 42 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index fb2120acc4f44..04aefbe3aadf9 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1075,21 +1075,6 @@ $( function() { }); } - /* - * The `.below-h2` class is here just for backward compatibility with plugins - * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. - * If '.wp-header-end' is found, append the notices after it otherwise - * after the first h1 or h2 heading found within the main content. - */ - if ( ! $headerEnd.length ) { - $headerEnd = $( '.wrap h1, .wrap h2' ).first(); - } - var $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); - if ( $notices.length ) { - var $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); - $notices.appendTo( $wpAdminNotices ); - } - /** * Makes notices dismissible. * @@ -1529,6 +1514,32 @@ $( function() { input.on('change', toggleUploadButton); })(); + /** + * Moves the admin notices into an `aside` element after the first `h1`. + * Prior to 7.2.0, admin notices were only moved after the first `h1`; + * The `aside` wrapper was added in 7.2.0. + * + * @since 7.2.0 + * + * @return {void} + */ + function moveAdminNotices() { + /* + * The `.below-h2` class is here just for backward compatibility with plugins + * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. + * If '.wp-header-end' is found, append the notices after it otherwise + * after the first h1 or h2 heading found within the main content. + */ + if ( ! $headerEnd.length ) { + $headerEnd = $( '.wrap h1, .wrap h2' ).first(); + } + var $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); + if ( $notices.length ) { + var $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); + $notices.appendTo( $wpAdminNotices ); + } + } + /** * Pins the menu while distraction-free writing is enabled. * @@ -2131,6 +2142,7 @@ $( function() { }); window.wpResponsive.init(); + moveAdminNotices(); setPinMenu(); setMenuState(); makeNoticesDismissible(); From 001aabd0f78fe8b59f86ddbc4fb8e429fa64ebb6 Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 25 Sep 2026 17:42:08 -0500 Subject: [PATCH 07/16] Add a unique ID to all admin notices. Sets all unique IDs as a `data-id`; if there is not ID attribute, sets as the ID attribute as well. --- src/wp-includes/functions.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 89d27f75e9f58..fa533bf2c8b83 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9278,6 +9278,10 @@ function wp_get_admin_notice( $message, $args = array() ) { */ $args = apply_filters( 'wp_admin_notice_args', $args, $message ); + // Unique ID is intended for internal use, and is not a filterable argument. + $unique_id = wp_unique_prefixed_id( 'wp-admin-notice-' ); + $args['notice_id'] = $unique_id; + $wrap_with_p = false !== $args['paragraph_wrap']; $wrap_opener = $wrap_with_p ? '<p>' : ''; $wrap_closer = $wrap_with_p ? '</p>' : ''; @@ -9287,8 +9291,11 @@ function wp_get_admin_notice( $message, $args = array() ) { if ( is_string( $args['id'] ) ) { $trimmed_id = trim( $args['id'] ); + $html_builder->set_attribute( 'data-id', $unique_id ); if ( '' !== $trimmed_id ) { $html_builder->set_attribute( 'id', $trimmed_id ); + } else { + $html_builder->set_attribute( 'id', $unique_id ); } } From bd5a321280d23f7d35f4a91b46b54444a933b3aa Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 25 Sep 2026 17:42:59 -0500 Subject: [PATCH 08/16] Key the notice array by unique ID. --- src/wp-includes/class-wp-admin-notice-collector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php index d4da2f9e5f394..91a383072516e 100644 --- a/src/wp-includes/class-wp-admin-notice-collector.php +++ b/src/wp-includes/class-wp-admin-notice-collector.php @@ -81,7 +81,7 @@ public static function init() { */ public static function capture_notice( $markup, $message, $args ) { if ( '' !== trim( wp_strip_all_tags( $message ) ) ) { - self::$notices[] = array( + self::$notices[ $args['notice_id'] ] = array( 'markup' => $markup, 'message' => $message, 'args' => $args, From 870103551859222456c9c8e190b47ecd342d67f5 Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 2 Oct 2026 17:35:50 -0500 Subject: [PATCH 09/16] Code standards --- src/js/_enqueues/admin/common.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index 04aefbe3aadf9..16eba6465c966 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1516,7 +1516,7 @@ $( function() { /** * Moves the admin notices into an `aside` element after the first `h1`. - * Prior to 7.2.0, admin notices were only moved after the first `h1`; + * Prior to 7.2.0, admin notices were only moved after the first `h1`; * The `aside` wrapper was added in 7.2.0. * * @since 7.2.0 @@ -1525,17 +1525,17 @@ $( function() { */ function moveAdminNotices() { /* - * The `.below-h2` class is here just for backward compatibility with plugins - * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. - * If '.wp-header-end' is found, append the notices after it otherwise - * after the first h1 or h2 heading found within the main content. - */ + * The `.below-h2` class is here just for backward compatibility with plugins + * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. + * If '.wp-header-end' is found, append the notices after it otherwise + * after the first h1 or h2 heading found within the main content. + */ if ( ! $headerEnd.length ) { $headerEnd = $( '.wrap h1, .wrap h2' ).first(); } - var $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); + const $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); if ( $notices.length ) { - var $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); + const $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); $notices.appendTo( $wpAdminNotices ); } } From 13b034cfc69bd7733aaa6a72953ad42b99bc9674 Mon Sep 17 00:00:00 2001 From: Peter Wilson <git-commit-by@peterwilson.cc> Date: Wed, 7 Oct 2026 15:11:36 +1100 Subject: [PATCH 10/16] Include id and data-id in test expectations. --- .../tests/functions/wpGetAdminNotice.php | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php index 87343479a2670..6267e438f7695 100644 --- a/tests/phpunit/tests/functions/wpGetAdminNotice.php +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -21,6 +21,11 @@ class Tests_Functions_WpGetAdminNotice extends WP_UnitTestCase { * @param string $expected The expected admin notice markup. */ public function test_should_return_admin_notice( $message, $args, $expected ) { + // Replace placeholder %%ID%% in expected string, required for test isolation. + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + ++$next_id; + $expected = str_replace( '%%ID%%', $next_id, $expected ); + $this->assertEqualHTML( $expected, wp_get_admin_notice( $message, $args ) ); } @@ -34,7 +39,7 @@ public function data_should_return_admin_notice() { 'defaults' => array( 'message' => 'A notice with defaults.', 'args' => array(), - 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with defaults.</p></div>', ), 'an empty message (used for templates)' => array( 'message' => '', @@ -44,7 +49,7 @@ public function data_should_return_admin_notice() { 'id' => 'message', 'additional_classes' => array( 'inline', 'hidden' ), ), - 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', ), 'an empty message (used for templates) without paragraph wrapping' => array( 'message' => '', @@ -55,56 +60,56 @@ public function data_should_return_admin_notice() { 'additional_classes' => array( 'inline', 'hidden' ), 'paragraph_wrap' => false, ), - 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"></div>', ), 'an "error" notice' => array( 'message' => 'An "error" notice.', 'args' => array( 'type' => 'error', ), - 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-error"><p>An "error" notice.</p></div>', ), 'a "success" notice' => array( 'message' => 'A "success" notice.', 'args' => array( 'type' => 'success', ), - 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-success"><p>A "success" notice.</p></div>', ), 'a "warning" notice' => array( 'message' => 'A "warning" notice.', 'args' => array( 'type' => 'warning', ), - 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning"><p>A "warning" notice.</p></div>', ), 'an "info" notice' => array( 'message' => 'An "info" notice.', 'args' => array( 'type' => 'info', ), - 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-info"><p>An "info" notice.</p></div>', ), 'a type that already starts with "notice-"' => array( 'message' => 'A type that already starts with "notice-".', 'args' => array( 'type' => 'notice-info', ), - 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', ), 'a dismissible notice' => array( 'message' => 'A dismissible notice.', 'args' => array( 'dismissible' => true, ), - 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice is-dismissible"><p>A dismissible notice.</p></div>', ), 'no type and an ID' => array( 'message' => 'A notice with an ID.', 'args' => array( 'id' => 'message', ), - 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice"><p>A notice with an ID.</p></div>', ), 'a type and an ID' => array( 'message' => 'A warning notice with an ID.', @@ -112,14 +117,14 @@ public function data_should_return_admin_notice() { 'type' => 'warning', 'id' => 'message', ), - 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', ), 'no type and additional classes' => array( 'message' => 'A notice with additional classes.', 'args' => array( 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice error notice-alt"><p>A notice with additional classes.</p></div>', ), 'a type and additional classes' => array( 'message' => 'A warning notice with additional classes.', @@ -127,7 +132,7 @@ public function data_should_return_admin_notice() { 'type' => 'warning', 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', ), 'a dismissible notice with a type and additional classes' => array( 'message' => 'A dismissible warning notice with a type and additional classes.', @@ -136,49 +141,49 @@ public function data_should_return_admin_notice() { 'dismissible' => true, 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', ), 'a notice without paragraph wrapping' => array( 'message' => '<span>A notice without paragraph wrapping.</span>', 'args' => array( 'paragraph_wrap' => false, ), - 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><span>A notice without paragraph wrapping.</span></div>', ), 'an unsafe type' => array( 'message' => 'A notice with an unsafe type.', 'args' => array( 'type' => '"><script>alert("Howdy,admin!");</script>', ), - 'expected' => '<div class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>', ), 'an unsafe ID' => array( 'message' => 'A notice with an unsafe ID.', 'args' => array( 'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice', ), - 'expected' => '<div id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>', ), 'unsafe additional classes' => array( 'message' => 'A notice with unsafe additional classes.', 'args' => array( 'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ), ), - 'expected' => '<div class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>', ), 'a type that is not a string' => array( 'message' => 'A notice with a type that is not a string.', 'args' => array( 'type' => array(), ), - 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with a type that is not a string.</p></div>', ), 'a type with only empty space' => array( 'message' => 'A notice with a type with only empty space.', 'args' => array( 'type' => " \t\r\n", ), - 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with a type with only empty space.</p></div>', ), 'an ID that is not a string' => array( 'message' => 'A notice with an ID that is not a string.', @@ -192,49 +197,49 @@ public function data_should_return_admin_notice() { 'args' => array( 'id' => " \t\r\n", ), - 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with an ID with only empty space.</p></div>', ), 'dismissible as a truthy value rather than (bool) true' => array( 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', 'args' => array( 'dismissible' => 1, ), - 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', ), 'additional classes that are not an array' => array( 'message' => 'A notice with additional classes that are not an array.', 'args' => array( 'additional_classes' => 'class-1 class-2 class-3', ), - 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with additional classes that are not an array.</p></div>', ), 'additional attribute with a value' => array( 'message' => 'A notice with an additional attribute with a value.', 'args' => array( 'attributes' => array( 'aria-live' => 'assertive' ), ), - 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', ), 'additional hidden attribute' => array( 'message' => 'A notice with the hidden attribute.', 'args' => array( 'attributes' => array( 'hidden' => true ), ), - 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" hidden><p>A notice with the hidden attribute.</p></div>', ), 'additional attribute no associative keys' => array( 'message' => 'A notice with a boolean attribute without an associative key.', 'args' => array( 'attributes' => array( 'hidden' ), ), - 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', ), 'additional attribute with role' => array( 'message' => 'A notice with an additional attribute role.', 'args' => array( 'attributes' => array( 'role' => 'alert' ), ), - 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', ), 'multiple additional attributes' => array( 'message' => 'A notice with multiple additional attributes.', @@ -244,14 +249,14 @@ public function data_should_return_admin_notice() { 'data-test' => -1, ), ), - 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', ), 'data attribute with unsafe value' => array( 'message' => 'A notice with an additional attribute with an unsafe value.', 'args' => array( 'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ), ), - 'expected' => '<div class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', ), 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', @@ -264,14 +269,14 @@ public function data_should_return_admin_notice() { 'hidden', ), ), - 'expected' => '<div class="notice" role="alert" disabled="disabled" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', + 'expected' => '<div id="wp-admin-notice-%%ID%%" class="notice" role="alert" disabled="disabled" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', ), 'paragraph wrapping as a falsy value rather than (bool) false' => array( 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', 'args' => array( 'paragraph_wrap' => 0, ), - 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', ), ); } @@ -285,8 +290,14 @@ public function data_should_return_admin_notice() { * @expectedIncorrectUsage wp_get_admin_notice */ public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { - $this->assertSame( - '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', + // Replace placeholder %%ID%% in expected string, required for test isolation. + $expected = '<div id="wp-admin-notice-%%ID%%" data-id="wp-admin-notice-%%ID%%" class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>'; + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + ++$next_id; + $expected = str_replace( '%%ID%%', $next_id, $expected ); + + $this->assertEqualHTML( + $expected, wp_get_admin_notice( 'A type containing spaces.', array( 'type' => 'first second third fourth' ) From 47c2c5a1eda43fadecf839f28042d6a69bdd4978 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Wed, 7 Oct 2026 12:33:22 +0200 Subject: [PATCH 11/16] Fix alignment. --- tests/phpunit/tests/functions/wpGetAdminNotice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php index 6267e438f7695..422cbb6da38de 100644 --- a/tests/phpunit/tests/functions/wpGetAdminNotice.php +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -292,7 +292,7 @@ public function data_should_return_admin_notice() { public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { // Replace placeholder %%ID%% in expected string, required for test isolation. $expected = '<div id="wp-admin-notice-%%ID%%" data-id="wp-admin-notice-%%ID%%" class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>'; - $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); ++$next_id; $expected = str_replace( '%%ID%%', $next_id, $expected ); From 3fe0e388b2a870c151c57dcb56bad29fc1975c59 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Wed, 7 Oct 2026 13:02:40 +0200 Subject: [PATCH 12/16] Adjust test. --- .../phpunit/tests/functions/wpAdminNotice.php | 75 +++++++++++-------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 3fb5391dead3e..7ca63c29c5c53 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -21,6 +21,11 @@ class Tests_Functions_WpAdminNotice extends WP_UnitTestCase { * @param string $expected The expected admin notice markup. */ public function test_should_output_admin_notice( $message, $args, $expected ) { + // Replace placeholder %%ID%% in expected string, required for test isolation. + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + ++$next_id; + $expected = str_replace( '%%ID%%', $next_id, $expected ); + ob_start(); wp_admin_notice( $message, $args ); $actual = ob_get_clean(); @@ -38,7 +43,7 @@ public function data_should_output_admin_notice() { 'defaults' => array( 'message' => 'A notice with defaults.', 'args' => array(), - 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with defaults.</p></div>', ), 'an empty message (used for templates)' => array( 'message' => '', @@ -48,7 +53,7 @@ public function data_should_output_admin_notice() { 'id' => 'message', 'additional_classes' => array( 'inline', 'hidden' ), ), - 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', ), 'an empty message (used for templates) without paragraph wrapping' => array( 'message' => '', @@ -59,56 +64,56 @@ public function data_should_output_admin_notice() { 'additional_classes' => array( 'inline', 'hidden' ), 'paragraph_wrap' => false, ), - 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"></div>', ), 'an "error" notice' => array( 'message' => 'An "error" notice.', 'args' => array( 'type' => 'error', ), - 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-error"><p>An "error" notice.</p></div>', ), 'a "success" notice' => array( 'message' => 'A "success" notice.', 'args' => array( 'type' => 'success', ), - 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-success"><p>A "success" notice.</p></div>', ), 'a "warning" notice' => array( 'message' => 'A "warning" notice.', 'args' => array( 'type' => 'warning', ), - 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning"><p>A "warning" notice.</p></div>', ), 'an "info" notice' => array( 'message' => 'An "info" notice.', 'args' => array( 'type' => 'info', ), - 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-info"><p>An "info" notice.</p></div>', ), 'a type that already starts with "notice-"' => array( 'message' => 'A type that already starts with "notice-".', 'args' => array( 'type' => 'notice-info', ), - 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', ), 'a dismissible notice' => array( 'message' => 'A dismissible notice.', 'args' => array( 'dismissible' => true, ), - 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice is-dismissible"><p>A dismissible notice.</p></div>', ), 'no type and an ID' => array( 'message' => 'A notice with an ID.', 'args' => array( 'id' => 'message', ), - 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice"><p>A notice with an ID.</p></div>', ), 'a type and an ID' => array( 'message' => 'A warning notice with an ID.', @@ -116,14 +121,14 @@ public function data_should_output_admin_notice() { 'type' => 'warning', 'id' => 'message', ), - 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', ), 'no type and additional classes' => array( 'message' => 'A notice with additional classes.', 'args' => array( 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice error notice-alt"><p>A notice with additional classes.</p></div>', ), 'a type and additional classes' => array( 'message' => 'A warning notice with additional classes.', @@ -131,7 +136,7 @@ public function data_should_output_admin_notice() { 'type' => 'warning', 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', ), 'a dismissible notice with a type and additional classes' => array( 'message' => 'A dismissible warning notice with a type and additional classes.', @@ -140,49 +145,49 @@ public function data_should_output_admin_notice() { 'dismissible' => true, 'additional_classes' => array( 'error', 'notice-alt' ), ), - 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', ), 'a notice without paragraph wrapping' => array( 'message' => '<span>A notice without paragraph wrapping.</span>', 'args' => array( 'paragraph_wrap' => false, ), - 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><span>A notice without paragraph wrapping.</span></div>', ), 'an unsafe type' => array( 'message' => 'A notice with an unsafe type.', 'args' => array( 'type' => '"><script>alert("Howdy,admin!");</script>', ), - 'expected' => '<div class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>', ), 'an unsafe ID' => array( 'message' => 'A notice with an unsafe ID.', 'args' => array( 'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice', ), - 'expected' => '<div id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>', ), 'unsafe additional classes' => array( 'message' => 'A notice with unsafe additional classes.', 'args' => array( 'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ), ), - 'expected' => '<div class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>', ), 'a type that is not a string' => array( 'message' => 'A notice with a type that is not a string.', 'args' => array( 'type' => array(), ), - 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with a type that is not a string.</p></div>', ), 'a type with only empty space' => array( 'message' => 'A notice with a type with only empty space.', 'args' => array( 'type' => " \t\r\n", ), - 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with a type with only empty space.</p></div>', ), 'an ID that is not a string' => array( 'message' => 'A notice with an ID that is not a string.', @@ -196,49 +201,49 @@ public function data_should_output_admin_notice() { 'args' => array( 'id' => " \t\r\n", ), - 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with an ID with only empty space.</p></div>', ), 'dismissible as a truthy value rather than (bool) true' => array( 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', 'args' => array( 'dismissible' => 1, ), - 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', ), 'additional classes that are not an array' => array( 'message' => 'A notice with additional classes that are not an array.', 'args' => array( 'additional_classes' => 'class-1 class-2 class-3', ), - 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with additional classes that are not an array.</p></div>', ), 'additional attribute with a value' => array( 'message' => 'A notice with an additional attribute with a value.', 'args' => array( 'attributes' => array( 'aria-live' => 'assertive' ), ), - 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', ), 'additional hidden attribute' => array( 'message' => 'A notice with the hidden attribute.', 'args' => array( 'attributes' => array( 'hidden' => true ), ), - 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" hidden><p>A notice with the hidden attribute.</p></div>', ), 'additional attribute no associative keys' => array( 'message' => 'A notice with a boolean attribute without an associative key.', 'args' => array( 'attributes' => array( 'hidden' ), ), - 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', ), 'additional attribute with role' => array( 'message' => 'A notice with an additional attribute role.', 'args' => array( 'attributes' => array( 'role' => 'alert' ), ), - 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', ), 'multiple additional attributes' => array( 'message' => 'A notice with multiple additional attributes.', @@ -248,21 +253,21 @@ public function data_should_output_admin_notice() { 'data-test' => -1, ), ), - 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', ), 'data attribute with unsafe value' => array( 'message' => 'A notice with an additional attribute with an unsafe value.', 'args' => array( 'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ), ), - 'expected' => '<div class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', ), 'additional invalid attribute' => array( 'message' => 'A notice with an additional attribute that is invalid.', 'args' => array( 'attributes' => array( 'not-valid' => 'not-valid' ), ), - 'expected' => '<div class="notice"><p>A notice with an additional attribute that is invalid.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with an additional attribute that is invalid.</p></div>', ), 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', @@ -275,16 +280,17 @@ public function data_should_output_admin_notice() { 'hidden', ), ), - 'expected' => '<div class="notice" role="alert" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', + 'expected' => '<div id="wp-admin-notice-%%ID%%" class="notice" role="alert" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', ), 'paragraph wrapping as a falsy value rather than (bool) false' => array( 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', 'args' => array( 'paragraph_wrap' => 0, ), - 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', ), ); + } /** @@ -295,6 +301,9 @@ public function data_should_output_admin_notice() { * @expectedIncorrectUsage wp_get_admin_notice */ public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + ++$next_id; + ob_start(); wp_admin_notice( 'A type containing spaces.', @@ -303,7 +312,7 @@ public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() $actual = ob_get_clean(); $this->assertSame( - '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', + '<div data-id="wp-admin-notice-' . $next_id . '" id="wp-admin-notice-' . $next_id . '" class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', $actual ); } From 129d943956923e14fa42cea0aca4da23bc26aa9e Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Wed, 7 Oct 2026 13:41:17 +0200 Subject: [PATCH 13/16] Fix CS. --- tests/phpunit/tests/functions/wpAdminNotice.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 7ca63c29c5c53..1367e39eff2df 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -290,7 +290,6 @@ public function data_should_output_admin_notice() { 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', ), ); - } /** From 1136028b74870c5413f612b4a66075f847c30a44 Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 9 Oct 2026 21:46:08 -0500 Subject: [PATCH 14/16] If the passed ID not a string, fallback to generated ID. --- src/wp-includes/functions.php | 3 +++ tests/phpunit/tests/functions/wpAdminNotice.php | 2 +- tests/phpunit/tests/functions/wpGetAdminNotice.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 731e72706d76d..c07a670d763d7 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9361,6 +9361,9 @@ function wp_get_admin_notice( $message, $args = array() ) { } else { $html_builder->set_attribute( 'id', $unique_id ); } + } else { + $html_builder->set_attribute( 'data-id', $unique_id ); + $html_builder->set_attribute( 'id', $unique_id ); } if ( is_string( $args['type'] ) ) { diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 1367e39eff2df..253e8fe9b1ba0 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -194,7 +194,7 @@ public function data_should_output_admin_notice() { 'args' => array( 'id' => array( 'message' ), ), - 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with an ID that is not a string.</p></div>', ), 'an ID with only empty space' => array( 'message' => 'A notice with an ID with only empty space.', diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php index 422cbb6da38de..58219d3fbb9cf 100644 --- a/tests/phpunit/tests/functions/wpGetAdminNotice.php +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -190,7 +190,7 @@ public function data_should_return_admin_notice() { 'args' => array( 'id' => array( 'message' ), ), - 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>', + 'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with an ID that is not a string.</p></div>', ), 'an ID with only empty space' => array( 'message' => 'A notice with an ID with only empty space.', From 86eec42d4468d4634d2850715ecb737095438e79 Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 9 Oct 2026 22:01:54 -0500 Subject: [PATCH 15/16] Add @since versioning to collector. --- .../class-wp-admin-notice-collector.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php index 91a383072516e..0d29cd9404d7a 100644 --- a/src/wp-includes/class-wp-admin-notice-collector.php +++ b/src/wp-includes/class-wp-admin-notice-collector.php @@ -17,7 +17,7 @@ * `ob_start()` callback replaces the placeholder with the final count once * the buffer is flushed. * - * @since x.x.x + * @since 7.2.0 */ class WP_Admin_Notice_Collector { @@ -25,7 +25,7 @@ class WP_Admin_Notice_Collector { * Placeholder inserted into the admin title, replaced with the * actual notice count once it is known. * - * @since x.x.x + * @since 7.2.0 * @var string */ const TITLE_PLACEHOLDER = '%%wp_admin_notice_count%%'; @@ -33,7 +33,7 @@ class WP_Admin_Notice_Collector { /** * Notices captured during the current request. * - * @since x.x.x + * @since 7.2.0 * @var array[] */ protected static $notices = array(); @@ -41,7 +41,7 @@ class WP_Admin_Notice_Collector { /** * Whether the collector's hooks have already been registered. * - * @since x.x.x + * @since 7.2.0 * @var bool */ protected static $hooked = false; @@ -49,7 +49,7 @@ class WP_Admin_Notice_Collector { /** * Registers the hooks used to collect notices and to annotate the admin title. * - * @since x.x.x + * @since 7.2.0 */ public static function init() { if ( self::$hooked ) { @@ -72,7 +72,7 @@ public static function init() { * * Hooked to the {@see 'wp_admin_notice_markup'} filter. * - * @since x.x.x + * @since 7.2.0 * * @param string $markup The HTML markup for the admin notice. * @param string $message The message for the admin notice. @@ -94,7 +94,7 @@ public static function capture_notice( $markup, $message, $args ) { /** * Returns all notices captured during the current request. * - * @since x.x.x + * @since 7.2.0 * * @return array[] Array of captured notices. Each entry contains the * notice's 'markup', 'message', and 'args'. @@ -106,7 +106,7 @@ public static function get_notices() { /** * Returns the number of notices captured during the current request. * - * @since x.x.x + * @since 7.2.0 * * @return int Notice count. */ @@ -117,7 +117,7 @@ public static function get_notice_count() { /** * Clears all previously captured notices. * - * @since x.x.x + * @since 7.2.0 */ public static function reset() { self::$notices = array(); @@ -129,7 +129,7 @@ public static function reset() { * The real count isn't known yet at this point, since notices are usually * produced later in the page. inject_notice_count() resolves it. * - * @since x.x.x + * @since 7.2.0 * * @param string $admin_title The page title, with extra context added. * @param string $title The original page title. @@ -145,7 +145,7 @@ public static function add_title_placeholder( $admin_title, $title ) { * Used as an `ob_start()` callback so it runs once the full page - including * any notices printed after `<title>` - has been buffered. * - * @since x.x.x + * @since 7.2.0 * * @param string $buffer The full buffered page output. * @return string The page output with the placeholder resolved. From 4221e09bd0ac9ec68a13b02271dad7e7a5ead40c Mon Sep 17 00:00:00 2001 From: Joe Dolson <design@joedolson.com> Date: Fri, 9 Oct 2026 22:18:47 -0500 Subject: [PATCH 16/16] Order of arguments is wrong --- tests/phpunit/tests/functions/wpAdminNotice.php | 2 +- tests/phpunit/tests/functions/wpGetAdminNotice.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 253e8fe9b1ba0..239c5a1b2d2e6 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -311,7 +311,7 @@ public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() $actual = ob_get_clean(); $this->assertSame( - '<div data-id="wp-admin-notice-' . $next_id . '" id="wp-admin-notice-' . $next_id . '" class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', + '<div class="notice notice-first second third fourth" data-id="wp-admin-notice-' . $next_id . '" id="wp-admin-notice-' . $next_id . '"><p>A type containing spaces.</p></div>', $actual ); } diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php index 58219d3fbb9cf..5bb7b8f748f6b 100644 --- a/tests/phpunit/tests/functions/wpGetAdminNotice.php +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -291,7 +291,7 @@ public function data_should_return_admin_notice() { */ public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { // Replace placeholder %%ID%% in expected string, required for test isolation. - $expected = '<div id="wp-admin-notice-%%ID%%" data-id="wp-admin-notice-%%ID%%" class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>'; + $expected = '<div class="notice notice-first second third fourth" id="wp-admin-notice-%%ID%%" data-id="wp-admin-notice-%%ID%%"><p>A type containing spaces.</p></div>'; $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); ++$next_id; $expected = str_replace( '%%ID%%', $next_id, $expected );