Make WordPress Core

Changeset 64282


Ignore:
Timestamp:
10/10/2026 02:18:37 PM (3 days ago)
Author:
joedolson
Message:

Admin: Admin notice accessibility improvements.

Capture all admin notices with message content generated via wp_get_admin_notices(). Wrap all captured notices inside an aside so notices are identified within a navigable landmark. Add information about the number of notices produced on a page in the title element of the page.

Ensures users have awareness on page entry whether there are notices present, how many there are, and provides screen readers with a mechanism to locate them as a group. Inline notices are counted in the total, but not contained within the landmark.

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

Props afercia, psykro, ryokuhi, raaaahman, alexstine, sergeybiryukov, oglekler, marybaum, swissspidy, davidbaumwald, sanket.parmar, sabernhardt, peterwilsoncc, williampatton, im3dabasia1, audrasjb, khokansardar, ugyensupport, joedolson.
Fixes #50486.

Location:
trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/admin/common.js

    r64133 r64282  
    10761076        }
    10771077
    1078         /*
    1079          * The `.below-h2` class is here just for backward compatibility with plugins
    1080          * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570.
    1081          * If '.wp-header-end' is found, append the notices after it otherwise
    1082          * after the first h1 or h2 heading found within the main content.
    1083          */
    1084         if ( ! $headerEnd.length ) {
    1085                 $headerEnd = $( '.wrap h1, .wrap h2' ).first();
    1086         }
    1087         $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd );
    1088 
    10891078        /**
    10901079         * Makes notices dismissible.
    … …  
    15251514                input.on('change', toggleUploadButton);
    15261515        })();
     1516
     1517        /**
     1518         * Moves the admin notices into an `aside` element after the first `h1`.
     1519         * Prior to 7.2.0, admin notices were only moved after the first `h1`;
     1520         * The `aside` wrapper was added in 7.2.0.
     1521         *
     1522         * @since 7.2.0
     1523         *
     1524         * @return {void}
     1525         */
     1526        function moveAdminNotices() {
     1527                /*
     1528                 * The `.below-h2` class is here just for backward compatibility with plugins
     1529                 * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570.
     1530                 * If '.wp-header-end' is found, append the notices after it otherwise
     1531                 * after the first h1 or h2 heading found within the main content.
     1532                 */
     1533                if ( ! $headerEnd.length ) {
     1534                        $headerEnd = $( '.wrap h1, .wrap h2' ).first();
     1535                }
     1536                const $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' );
     1537                if ( $notices.length ) {
     1538                        const $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd );
     1539                        $notices.appendTo( $wpAdminNotices );
     1540                }
     1541        }
    15271542
    15281543        /**
    … …  
    21282143
    21292144        window.wpResponsive.init();
     2145        moveAdminNotices();
    21302146        setPinMenu();
    21312147        setMenuState();
  • trunk/src/wp-admin/admin.php

    r64120 r64282  
    101101
    102102require_once ABSPATH . 'wp-admin/includes/admin.php';
     103
     104WP_Admin_Notice_Collector::init();
    103105
    104106auth_redirect();
  • trunk/src/wp-includes/functions.php

    r64253 r64282  
    93439343        $args = apply_filters( 'wp_admin_notice_args', $args, $message );
    93449344
     9345        // Unique ID is intended for internal use, and is not a filterable argument.
     9346        $unique_id         = wp_unique_prefixed_id( 'wp-admin-notice-' );
     9347        $args['notice_id'] = $unique_id;
     9348
    93459349        $wrap_with_p  = false !== $args['paragraph_wrap'];
    93469350        $wrap_opener  = $wrap_with_p ? '<p>' : '';
    … …  
    93529356                $trimmed_id = trim( $args['id'] );
    93539357
     9358                $html_builder->set_attribute( 'data-id', $unique_id );
    93549359                if ( '' !== $trimmed_id ) {
    93559360                        $html_builder->set_attribute( 'id', $trimmed_id );
    9356                 }
     9361                } else {
     9362                        $html_builder->set_attribute( 'id', $unique_id );
     9363                }
     9364        } else {
     9365                $html_builder->set_attribute( 'data-id', $unique_id );
     9366                $html_builder->set_attribute( 'id', $unique_id );
    93579367        }
    93589368
  • trunk/src/wp-settings.php

    r63361 r64282  
    116116require ABSPATH . WPINC . '/meta.php';
    117117require ABSPATH . WPINC . '/functions.php';
     118require ABSPATH . WPINC . '/class-wp-admin-notice-collector.php';
    118119require ABSPATH . WPINC . '/class-wp-meta-query.php';
    119120require ABSPATH . WPINC . '/class-wp-matchesmapregex.php';
  • trunk/tests/phpunit/tests/functions/wpAdminNotice.php

    r63380 r64282  
    2222         */
    2323        public function test_should_output_admin_notice( $message, $args, $expected ) {
     24                // Replace placeholder %%ID%% in expected string, required for test isolation.
     25                $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) );
     26                ++$next_id;
     27                $expected = str_replace( '%%ID%%', $next_id, $expected );
     28
    2429                ob_start();
    2530                wp_admin_notice( $message, $args );
    … …  
    3944                                'message'  => 'A notice with defaults.',
    4045                                'args'     => array(),
    41                                 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>',
     46                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with defaults.</p></div>',
    4247                        ),
    4348                        'an empty message (used for templates)'     => array(
    … …  
    4954                                        'additional_classes' => array( 'inline', 'hidden' ),
    5055                                ),
    51                                 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
     56                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
    5257                        ),
    5358                        'an empty message (used for templates) without paragraph wrapping' => array(
    … …  
    6065                                        'paragraph_wrap'     => false,
    6166                                ),
    62                                 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>',
     67                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"></div>',
    6368                        ),
    6469                        'an "error" notice'                         => array(
    … …  
    6772                                        'type' => 'error',
    6873                                ),
    69                                 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>',
     74                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-error"><p>An "error" notice.</p></div>',
    7075                        ),
    7176                        'a "success" notice'                        => array(
    … …  
    7479                                        'type' => 'success',
    7580                                ),
    76                                 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>',
     81                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-success"><p>A "success" notice.</p></div>',
    7782                        ),
    7883                        'a "warning" notice'                        => array(
    … …  
    8186                                        'type' => 'warning',
    8287                                ),
    83                                 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>',
     88                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning"><p>A "warning" notice.</p></div>',
    8489                        ),
    8590                        'an "info" notice'                          => array(
    … …  
    8893                                        'type' => 'info',
    8994                                ),
    90                                 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>',
     95                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-info"><p>An "info" notice.</p></div>',
    9196                        ),
    9297                        'a type that already starts with "notice-"' => array(
    … …  
    95100                                        'type' => 'notice-info',
    96101                                ),
    97                                 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>',
     102                                '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>',
    98103                        ),
    99104                        'a dismissible notice'                      => array(
    … …  
    102107                                        'dismissible' => true,
    103108                                ),
    104                                 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>',
     109                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice is-dismissible"><p>A dismissible notice.</p></div>',
    105110                        ),
    106111                        'no type and an ID'                         => array(
    … …  
    109114                                        'id' => 'message',
    110115                                ),
    111                                 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>',
     116                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice"><p>A notice with an ID.</p></div>',
    112117                        ),
    113118                        'a type and an ID'                          => array(
    … …  
    117122                                        'id'   => 'message',
    118123                                ),
    119                                 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
     124                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
    120125                        ),
    121126                        'no type and additional classes'            => array(
    … …  
    124129                                        'additional_classes' => array( 'error', 'notice-alt' ),
    125130                                ),
    126                                 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>',
     131                                '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>',
    127132                        ),
    128133                        'a type and additional classes'             => array(
    … …  
    132137                                        'additional_classes' => array( 'error', 'notice-alt' ),
    133138                                ),
    134                                 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>',
     139                                '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>',
    135140                        ),
    136141                        'a dismissible notice with a type and additional classes' => array(
    … …  
    141146                                        'additional_classes' => array( 'error', 'notice-alt' ),
    142147                                ),
    143                                 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>',
     148                                '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>',
    144149                        ),
    145150                        'a notice without paragraph wrapping'       => array(
    … …  
    148153                                        'paragraph_wrap' => false,
    149154                                ),
    150                                 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>',
     155                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><span>A notice without paragraph wrapping.</span></div>',
    151156                        ),
    152157                        'an unsafe type'                            => array(
    … …  
    155160                                        'type' => '"><script>alert("Howdy,admin!");</script>',
    156161                                ),
    157                                 'expected' => '<div class="notice notice-&quot;><script>alert(&quot;Howdy,admin!&quot;);</script>"><p>A notice with an unsafe type.</p></div>',
     162                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-&quot;><script>alert(&quot;Howdy,admin!&quot;);</script>"><p>A notice with an unsafe type.</p></div>',
    158163                        ),
    159164                        'an unsafe ID'                              => array(
    … …  
    162167                                        'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice',
    163168                                ),
    164                                 'expected' => '<div id="&quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice" class="notice"><p>A notice with an unsafe ID.</p></div>',
     169                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="&quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice" class="notice"><p>A notice with an unsafe ID.</p></div>',
    165170                        ),
    166171                        'unsafe additional classes'                 => array(
    … …  
    169174                                        'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ),
    170175                                ),
    171                                 'expected' => '<div class="notice &quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice"><p>A notice with unsafe additional classes.</p></div>',
     176                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice &quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice"><p>A notice with unsafe additional classes.</p></div>',
    172177                        ),
    173178                        'a type that is not a string'               => array(
    … …  
    176181                                        'type' => array(),
    177182                                ),
    178                                 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>',
     183                                '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>',
    179184                        ),
    180185                        'a type with only empty space'              => array(
    … …  
    183188                                        'type' => " \t\r\n",
    184189                                ),
    185                                 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>',
     190                                '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>',
    186191                        ),
    187192                        'an ID that is not a string'                => array(
    … …  
    190195                                        'id' => array( 'message' ),
    191196                                ),
    192                                 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>',
     197                                '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>',
    193198                        ),
    194199                        'an ID with only empty space'               => array(
    … …  
    197202                                        'id' => " \t\r\n",
    198203                                ),
    199                                 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>',
     204                                '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>',
    200205                        ),
    201206                        'dismissible as a truthy value rather than (bool) true' => array(
    … …  
    204209                                        'dismissible' => 1,
    205210                                ),
    206                                 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>',
     211                                '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>',
    207212                        ),
    208213                        'additional classes that are not an array'  => array(
    … …  
    211216                                        'additional_classes' => 'class-1 class-2 class-3',
    212217                                ),
    213                                 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>',
     218                                '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>',
    214219                        ),
    215220                        'additional attribute with a value'         => array(
    … …  
    218223                                        'attributes' => array( 'aria-live' => 'assertive' ),
    219224                                ),
    220                                 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>',
     225                                '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>',
    221226                        ),
    222227                        'additional hidden attribute'               => array(
    … …  
    225230                                        'attributes' => array( 'hidden' => true ),
    226231                                ),
    227                                 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>',
     232                                '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>',
    228233                        ),
    229234                        'additional attribute no associative keys'  => array(
    … …  
    232237                                        'attributes' => array( 'hidden' ),
    233238                                ),
    234                                 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>',
     239                                '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>',
    235240                        ),
    236241                        'additional attribute with role'            => array(
    … …  
    239244                                        'attributes' => array( 'role' => 'alert' ),
    240245                                ),
    241                                 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>',
     246                                '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>',
    242247                        ),
    243248                        'multiple additional attributes'            => array(
    … …  
    249254                                        ),
    250255                                ),
    251                                 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>',
     256                                '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>',
    252257                        ),
    253258                        'data attribute with unsafe value'          => array(
    … …  
    256261                                        'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ),
    257262                                ),
    258                                 'expected' => '<div class="notice" data-unsafe="&lt;script&gt;alert( &quot;Howdy, admin!&quot; );&lt;/script&gt;"><p>A notice with an additional attribute with an unsafe value.</p></div>',
     263                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" data-unsafe="&lt;script&gt;alert( &quot;Howdy, admin!&quot; );&lt;/script&gt;"><p>A notice with an additional attribute with an unsafe value.</p></div>',
    259264                        ),
    260265                        'additional invalid attribute'              => array(
    … …  
    263268                                        'attributes' => array( 'not-valid' => 'not-valid' ),
    264269                                ),
    265                                 'expected' => '<div class="notice"><p>A notice with an additional attribute that is invalid.</p></div>',
     270                                '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>',
    266271                        ),
    267272                        'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array(
    … …  
    276281                                        ),
    277282                                ),
    278                                 '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>',
     283                                '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>',
    279284                        ),
    280285                        'paragraph wrapping as a falsy value rather than (bool) false' => array(
    … …  
    283288                                        'paragraph_wrap' => 0,
    284289                                ),
    285                                 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>',
     290                                '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>',
    286291                        ),
    287292                );
    … …  
    296301         */
    297302        public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() {
     303                $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) );
     304                ++$next_id;
     305
    298306                ob_start();
    299307                wp_admin_notice(
    … …  
    304312
    305313                $this->assertSame(
    306                         '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>',
     314                        '<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>',
    307315                        $actual
    308316                );
  • trunk/tests/phpunit/tests/functions/wpGetAdminNotice.php

    r63380 r64282  
    2222         */
    2323        public function test_should_return_admin_notice( $message, $args, $expected ) {
     24                // Replace placeholder %%ID%% in expected string, required for test isolation.
     25                $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) );
     26                ++$next_id;
     27                $expected = str_replace( '%%ID%%', $next_id, $expected );
     28
    2429                $this->assertEqualHTML( $expected, wp_get_admin_notice( $message, $args ) );
    2530        }
    … …  
    3540                                'message'  => 'A notice with defaults.',
    3641                                'args'     => array(),
    37                                 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>',
     42                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><p>A notice with defaults.</p></div>',
    3843                        ),
    3944                        'an empty message (used for templates)'     => array(
    … …  
    4550                                        'additional_classes' => array( 'inline', 'hidden' ),
    4651                                ),
    47                                 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
     52                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>',
    4853                        ),
    4954                        'an empty message (used for templates) without paragraph wrapping' => array(
    … …  
    5661                                        'paragraph_wrap'     => false,
    5762                                ),
    58                                 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>',
     63                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-error is-dismissible inline hidden"></div>',
    5964                        ),
    6065                        'an "error" notice'                         => array(
    … …  
    6368                                        'type' => 'error',
    6469                                ),
    65                                 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>',
     70                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-error"><p>An "error" notice.</p></div>',
    6671                        ),
    6772                        'a "success" notice'                        => array(
    … …  
    7075                                        'type' => 'success',
    7176                                ),
    72                                 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>',
     77                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-success"><p>A "success" notice.</p></div>',
    7378                        ),
    7479                        'a "warning" notice'                        => array(
    … …  
    7782                                        'type' => 'warning',
    7883                                ),
    79                                 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>',
     84                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-warning"><p>A "warning" notice.</p></div>',
    8085                        ),
    8186                        'an "info" notice'                          => array(
    … …  
    8489                                        'type' => 'info',
    8590                                ),
    86                                 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>',
     91                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-info"><p>An "info" notice.</p></div>',
    8792                        ),
    8893                        'a type that already starts with "notice-"' => array(
    … …  
    9196                                        'type' => 'notice-info',
    9297                                ),
    93                                 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>',
     98                                '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>',
    9499                        ),
    95100                        'a dismissible notice'                      => array(
    … …  
    98103                                        'dismissible' => true,
    99104                                ),
    100                                 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>',
     105                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice is-dismissible"><p>A dismissible notice.</p></div>',
    101106                        ),
    102107                        'no type and an ID'                         => array(
    … …  
    105110                                        'id' => 'message',
    106111                                ),
    107                                 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>',
     112                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice"><p>A notice with an ID.</p></div>',
    108113                        ),
    109114                        'a type and an ID'                          => array(
    … …  
    113118                                        'id'   => 'message',
    114119                                ),
    115                                 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
     120                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>',
    116121                        ),
    117122                        'no type and additional classes'            => array(
    … …  
    120125                                        'additional_classes' => array( 'error', 'notice-alt' ),
    121126                                ),
    122                                 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>',
     127                                '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>',
    123128                        ),
    124129                        'a type and additional classes'             => array(
    … …  
    128133                                        'additional_classes' => array( 'error', 'notice-alt' ),
    129134                                ),
    130                                 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>',
     135                                '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>',
    131136                        ),
    132137                        'a dismissible notice with a type and additional classes' => array(
    … …  
    137142                                        'additional_classes' => array( 'error', 'notice-alt' ),
    138143                                ),
    139                                 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>',
     144                                '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>',
    140145                        ),
    141146                        'a notice without paragraph wrapping'       => array(
    … …  
    144149                                        'paragraph_wrap' => false,
    145150                                ),
    146                                 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>',
     151                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice"><span>A notice without paragraph wrapping.</span></div>',
    147152                        ),
    148153                        'an unsafe type'                            => array(
    … …  
    151156                                        'type' => '"><script>alert("Howdy,admin!");</script>',
    152157                                ),
    153                                 'expected' => '<div class="notice notice-&quot;><script>alert(&quot;Howdy,admin!&quot;);</script>"><p>A notice with an unsafe type.</p></div>',
     158                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice notice-&quot;><script>alert(&quot;Howdy,admin!&quot;);</script>"><p>A notice with an unsafe type.</p></div>',
    154159                        ),
    155160                        'an unsafe ID'                              => array(
    … …  
    158163                                        'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice',
    159164                                ),
    160                                 'expected' => '<div id="&quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice" class="notice"><p>A notice with an unsafe ID.</p></div>',
     165                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="&quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice" class="notice"><p>A notice with an unsafe ID.</p></div>',
    161166                        ),
    162167                        'unsafe additional classes'                 => array(
    … …  
    165170                                        'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ),
    166171                                ),
    167                                 'expected' => '<div class="notice &quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice"><p>A notice with unsafe additional classes.</p></div>',
     172                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice &quot;><script>alert( &quot;Howdy, admin!&quot; );</script> <div class=&quot;notice"><p>A notice with unsafe additional classes.</p></div>',
    168173                        ),
    169174                        'a type that is not a string'               => array(
    … …  
    172177                                        'type' => array(),
    173178                                ),
    174                                 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>',
     179                                '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>',
    175180                        ),
    176181                        'a type with only empty space'              => array(
    … …  
    179184                                        'type' => " \t\r\n",
    180185                                ),
    181                                 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>',
     186                                '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>',
    182187                        ),
    183188                        'an ID that is not a string'                => array(
    … …  
    186191                                        'id' => array( 'message' ),
    187192                                ),
    188                                 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>',
     193                                '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>',
    189194                        ),
    190195                        'an ID with only empty space'               => array(
    … …  
    193198                                        'id' => " \t\r\n",
    194199                                ),
    195                                 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>',
     200                                '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>',
    196201                        ),
    197202                        'dismissible as a truthy value rather than (bool) true' => array(
    … …  
    200205                                        'dismissible' => 1,
    201206                                ),
    202                                 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>',
     207                                '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>',
    203208                        ),
    204209                        'additional classes that are not an array'  => array(
    … …  
    207212                                        'additional_classes' => 'class-1 class-2 class-3',
    208213                                ),
    209                                 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>',
     214                                '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>',
    210215                        ),
    211216                        'additional attribute with a value'         => array(
    … …  
    214219                                        'attributes' => array( 'aria-live' => 'assertive' ),
    215220                                ),
    216                                 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>',
     221                                '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>',
    217222                        ),
    218223                        'additional hidden attribute'               => array(
    … …  
    221226                                        'attributes' => array( 'hidden' => true ),
    222227                                ),
    223                                 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>',
     228                                '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>',
    224229                        ),
    225230                        'additional attribute no associative keys'  => array(
    … …  
    228233                                        'attributes' => array( 'hidden' ),
    229234                                ),
    230                                 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>',
     235                                '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>',
    231236                        ),
    232237                        'additional attribute with role'            => array(
    … …  
    235240                                        'attributes' => array( 'role' => 'alert' ),
    236241                                ),
    237                                 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>',
     242                                '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>',
    238243                        ),
    239244                        'multiple additional attributes'            => array(
    … …  
    245250                                        ),
    246251                                ),
    247                                 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>',
     252                                '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>',
    248253                        ),
    249254                        'data attribute with unsafe value'          => array(
    … …  
    252257                                        'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ),
    253258                                ),
    254                                 'expected' => '<div class="notice" data-unsafe="&lt;script&gt;alert( &quot;Howdy, admin!&quot; );&lt;/script&gt;"><p>A notice with an additional attribute with an unsafe value.</p></div>',
     259                                'expected' => '<div data-id="wp-admin-notice-%%ID%%" id="wp-admin-notice-%%ID%%" class="notice" data-unsafe="&lt;script&gt;alert( &quot;Howdy, admin!&quot; );&lt;/script&gt;"><p>A notice with an additional attribute with an unsafe value.</p></div>',
    255260                        ),
    256261                        'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array(
    … …  
    265270                                        ),
    266271                                ),
    267                                 '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>',
     272                                '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>',
    268273                        ),
    269274                        'paragraph wrapping as a falsy value rather than (bool) false' => array(
    … …  
    272277                                        'paragraph_wrap' => 0,
    273278                                ),
    274                                 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>',
     279                                '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>',
    275280                        ),
    276281                );
    … …  
    286291         */
    287292        public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() {
    288                 $this->assertSame(
    289                         '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>',
     293                // Replace placeholder %%ID%% in expected string, required for test isolation.
     294                $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>';
     295                $next_id  = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) );
     296                ++$next_id;
     297                $expected = str_replace( '%%ID%%', $next_id, $expected );
     298
     299                $this->assertEqualHTML(
     300                        $expected,
    290301                        wp_get_admin_notice(
    291302                                'A type containing spaces.',
Note: See TracChangeset for help on using the changeset viewer.