diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index b0b78fd52b890..24604a86cdff3 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1075,17 +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(); - } - $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd ); - /** * Makes notices dismissible. * @@ -1525,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(); + } + const $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); + if ( $notices.length ) { + const $wpAdminNotices = $( '' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); + $notices.appendTo( $wpAdminNotices ); + } + } + /** * Pins the menu while distraction-free writing is enabled. * @@ -2127,6 +2142,7 @@ $( function() { }); window.wpResponsive.init(); + moveAdminNotices(); setPinMenu(); setMenuState(); makeNoticesDismissible(); diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 2902c8de209ed..1c88e4a818d67 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-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php new file mode 100644 index 0000000000000..0d29cd9404d7a --- /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 7.2.0 + */ +class WP_Admin_Notice_Collector { + + /** + * Placeholder inserted into the admin title, replaced with the + * actual notice count once it is known. + * + * @since 7.2.0 + * @var string + */ + const TITLE_PLACEHOLDER = '%%wp_admin_notice_count%%'; + + /** + * Notices captured during the current request. + * + * @since 7.2.0 + * @var array[] + */ + protected static $notices = array(); + + /** + * Whether the collector's hooks have already been registered. + * + * @since 7.2.0 + * @var bool + */ + protected static $hooked = false; + + /** + * Registers the hooks used to collect notices and to annotate the admin title. + * + * @since 7.2.0 + */ + 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 7.2.0 + * + * @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[ $args['notice_id'] ] = array( + 'markup' => $markup, + 'message' => $message, + 'args' => $args, + ); + } + + return $markup; + } + + /** + * Returns all notices captured during the current request. + * + * @since 7.2.0 + * + * @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 7.2.0 + * + * @return int Notice count. + */ + public static function get_notice_count() { + return count( self::$notices ); + } + + /** + * Clears all previously captured notices. + * + * @since 7.2.0 + */ + 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 7.2.0 + * + * @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 7.2.0 + * + * @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 b70fff7132abe..c07a670d763d7 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9342,6 +9342,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>' : ''; @@ -9351,9 +9355,15 @@ 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 ); } + } else { + $html_builder->set_attribute( 'data-id', $unique_id ); + $html_builder->set_attribute( 'id', $unique_id ); } if ( is_string( $args['type'] ) ) { 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'; diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 3fb5391dead3e..239c5a1b2d2e6 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,105 +145,105 @@ 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.', '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.', '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,14 +280,14 @@ 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 +300,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 +311,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 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 87343479a2670..5bb7b8f748f6b 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,105 +141,105 @@ 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.', '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.', '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 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 ); + + $this->assertEqualHTML( + $expected, wp_get_admin_notice( 'A type containing spaces.', array( 'type' => 'first second third fourth' )