diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index 3de9447879f5e..b044b722c7a3c 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1109,11 +1109,46 @@ $( function() { $button.find( '.screen-reader-text' ).text( __( 'Dismiss this notice.' ) ); $button.on( 'click.wp-dismiss-notice', function( event ) { event.preventDefault(); - $el.fadeTo( 100, 0, function() { - $el.slideUp( 100, function() { - $el.remove(); + + var $dismiss_data = { action: 'dismiss-notice' }, + $slug = $el.data( 'slug' ), + $expiration = $el.data( 'expiration' ); + + if ( ! $slug ) { + $el.fadeTo( 100, 0, function() { + $el.slideUp( 100, function() { + $el.remove(); + }); }); - }); + } else { + $dismiss_data.slug = $slug; + + if ( $expiration ) { + $dismiss_data.expiration = $expiration; + } + + $.post( + ajaxurl, + $dismiss_data + ).always( function ( response ) { + if ( true === response.success ) { + $el.fadeTo( 100, 0, function() { + $el.slideUp( 100, function() { + $el.remove(); + }); + }); + } else { + var $noticeDismissalFailed = $( '#notice-dismissal-failed' ); + + if ( 0 === $noticeDismissalFailed.length ) { + $el.after( '

' + response.data + '

' ); + } else { + $el.after( $noticeDismissalFailed ); + $noticeDismissalFailed.find( 'p' ).innerHTML = response.data; + } + } + } ); + } }); $el.append( $button ); @@ -1733,7 +1768,7 @@ $( function() { setTimeout( function() { var focusIsInToggle = $.contains( toggleButton, focusedElement ); var focusIsInSidebar = $.contains( sidebar, focusedElement ); - + if ( ! focusIsInToggle && ! focusIsInSidebar ) { $( toggleButton ).trigger( 'click.wp-responsive' ); } diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index fb191100299ce..0f18c1ebfd172 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -141,6 +141,7 @@ 'health-check-get-sizes', 'toggle-auto-updates', 'send-password-reset', + 'dismiss-notice', ); // Deprecated. diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 69f5fd469ca94..ef0dbd1d746ed 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -5610,3 +5610,40 @@ function wp_ajax_send_password_reset() { wp_send_json_error( $results->get_error_message() ); } } + +/** + * Handles dismissing a notice via AJAX. + * + * @since 6.5.0 + */ +function wp_ajax_dismiss_notice() { + check_ajax_referer( 'dismiss-notice', 'nonce' ); + + if ( ! isset( $_POST['slug'] ) ) { + wp_send_json_error( __( 'Failed to dismiss notice: The notice does not have a slug.' ) ); + } + + $slug = trim( sanitize_text_field( $_POST['slug'] ) ); + if ( '' === $slug ) { + wp_send_json_error( __( "Failed to dismiss notice: The notice's slug must not be an empty string." ) ); + } + + $expiration = 0; + if ( isset( $_POST['expiration'] ) ) { + if ( ! is_numeric( $_POST['expiration'] ) ) { + wp_send_json_error( __( 'Failed to dismiss notice: The expiration time must be a number of seconds.' ) ); + } + + $expiration = (int) $_POST['expiration']; + + if ( 0 > $_POST['expiration'] ) { + wp_send_json_error( __( 'Failed to dismiss notice: The expiration time must be greater than or equal to 0.' ) ); + } + } + + if ( false === set_site_transient( "wp_admin_notice_dismissed_{$slug}", 1, $expiration ) ) { + wp_send_json_error( __( 'Failed to dismiss notice: The notice could not be dismissed.' ) ); + } + + wp_send_json_success( __( 'The notice was successfully dismissed.' ) ); +} diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index cb490ee1764fa..6449ef8e3a909 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8756,14 +8756,24 @@ function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) { * @param array $args { * Optional. An array of arguments for the admin notice. Default empty array. * - * @type string $type Optional. The type of admin notice. - * For example, 'error', 'success', 'warning', 'info'. - * Default empty string. - * @type bool $dismissible Optional. Whether the admin notice is dismissible. Default false. - * @type string $id Optional. The value of the admin notice's ID attribute. Default empty string. - * @type string[] $additional_classes Optional. A string array of class names. Default empty array. - * @type string[] $attributes Optional. Additional attributes for the notice div. Default empty array. - * @type bool $paragraph_wrap Optional. Whether to wrap the message in paragraph tags. Default true. + * @type string $type Optional. The type of admin notice. + * For example, 'error', 'success', 'warning', 'info'. + * Default empty string. + * @type bool|array $dismissible { + * Optional. Whether the admin notice is dismissible. Default false. + * + * If false, the notice is not dismissible. + * If true, the notice is dismissible until the next page load. + * If an array, the notice will be dismissed either permanently, + * or until the specified expiration has occurred. + * + * @type string $slug The slug of the notice. Used to store a transient when the notice is dismissed. + * @type int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). + * } + * @type string $id Optional. The value of the admin notice's ID attribute. Default empty string. + * @type string[] $additional_classes Optional. A string array of class names. Default empty array. + * @type string[] $attributes Optional. Additional attributes for the notice div. Default empty array. + * @type bool $paragraph_wrap Optional. Whether to wrap the message in paragraph tags. Default true. * } * @return string The markup for an admin notice. */ @@ -8822,6 +8832,106 @@ function wp_get_admin_notice( $message, $args = array() ) { if ( true === $args['dismissible'] ) { $classes .= ' is-dismissible'; + } elseif ( is_array( $args['dismissible'] ) ) { + // The "slug" key is required. + if ( ! isset( $args['dismissible']['slug'] ) || ! is_string( $args['dismissible']['slug'] ) ) { + wp_trigger_error( + __FUNCTION__, + sprintf( + /* translators: 1: The "slug" key, 2: The "dismissible" key. */ + __( 'The "%1$s" key in the "%2$s" array must be a string.' ), + 'slug', + 'dismissible' + ) + ); + } else { + $slug = trim( $args['dismissible']['slug'] ); + if ( '' === $slug ) { + wp_trigger_error( + __FUNCTION__, + sprintf( + /* translators: 1: The "slug" key, 2: The "dismissible" key. */ + __( 'The "%1$s" key in the "%2$s" array must be a non-empty string.' ), + 'slug', + 'dismissible' + ) + ); + } else { + // Add a slug data attribute so a transient can be saved when the notice is dismissed. + $args['attributes']['data-slug'] = $slug; + } + + /* + * If the notice is still dismissed, return early with + * empty notice markup so that nothing can be output. + */ + if ( 1 === (int) get_site_transient( "wp_admin_notice_dismissed_{$slug}" ) ) { + return ''; + } + + /* + * The "expiration" key is optional. + * + * `isset()` is not used because it will not catch `null` values, which are invalid. + */ + if ( array_key_exists( 'expiration', $args['dismissible'] ) ) { + if ( ! is_int( $args['dismissible']['expiration'] ) ) { + /* + * Unset the slug data attribute so that the notice appears on the next page load, + * allowing for corrections to be made without needing to delete the notice's transient. + * + * Without this, the notice would be permanently dismissed. + */ + unset( $args['attributes']['data-slug'] ); + + wp_trigger_error( + __FUNCTION__, + sprintf( + /* translators: 1: The "expiration" key, 2: The "dismissible" key. */ + __( 'The "%1$s" key in the "%2$s" array must be an integer.' ), + 'expiration', + 'dismissible' + ) + ); + } else { + $expiration = (int) $args['dismissible']['expiration']; + if ( 0 > $expiration ) { + /* + * Unset the slug data attribute so that the notice appears on the next page load, + * allowing for corrections to be made without needing to delete the notice's transient. + * + * Without this, the notice would be permanently dismissed. + */ + unset( $args['attributes']['data-slug'] ); + + wp_trigger_error( + __FUNCTION__, + sprintf( + /* translators: 1: The "expiration" key, 2: The "dismissible" key. */ + __( 'The "%1$s" key in the "%2$s" array must be greater than or equal to 0.' ), + 'expiration', + 'dismissible' + ) + ); + } else { + // Add an expiration data attribute so the notice can be dismissed for a specified duration. + $args['attributes']['data-expiration'] = $expiration; + } + } + } + + /* + * By only adding the HTML class when everything is considered valid, + * this means invalid values will result in a notice that cannot be dismissed. + * + * Even if error reporting is disabled for some reason, the developer will know + * immediately that something has gone wrong. + */ + if ( isset( $args['attributes']['data-slug'] ) ) { + $args['attributues']['data-nonce'] = wp_create_nonce( 'dismiss-notice' ); + $classes .= ' is-dismissible'; + } + } } if ( is_array( $args['additional_classes'] ) && ! empty( $args['additional_classes'] ) ) { diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php index 0478c10900c37..52ea476ac94e4 100644 --- a/tests/phpunit/includes/testcase-ajax.php +++ b/tests/phpunit/includes/testcase-ajax.php @@ -113,6 +113,7 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { 'get-post-thumbnail-html', 'wp-privacy-export-personal-data', 'wp-privacy-erase-personal-data', + 'dismiss-notice', ); public static function set_up_before_class() { diff --git a/tests/phpunit/tests/ajax/wpAjaxDismissNotice.php b/tests/phpunit/tests/ajax/wpAjaxDismissNotice.php new file mode 100644 index 0000000000000..b2edb1b24ec93 --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxDismissNotice.php @@ -0,0 +1,412 @@ +_setRole( 'administrator' ); + + // Set up a default request. + $_POST['nonce'] = wp_create_nonce( 'dismiss-notice' ); + $_POST['slug'] = $slug; + + if ( 'skip' !== $expiration ) { + $_POST['expiration'] = $expiration; + } + + // Make the request. + try { + $this->_handleAjax( 'dismiss-notice' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $this->assertTrue( + $response['success'], + 'The notice was not dismissed.' + ); + + $this->assertSame( + 'The notice was successfully dismissed.', + $response['data'], + 'An unexpected JSON success message was received.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_dismiss_notice() { + return array( + 'no expiration provided (should be permanent)' => array( + 'slug' => 'mynotice-dismiss-forever-default', + 'expiration' => 'skip', + ), + 'an expiration of 0 seconds (permanent)' => array( + 'slug' => 'mynotice-dismiss-forever-specified', + 'expiration' => 0, + ), + 'an expiration of 30 days' => array( + 'slug' => 'mynotice-dismiss-for-30-days', + 'expiration' => 30 * DAY_IN_SECONDS, + ), + 'a (bool) true slug' => array( + 'slug' => true, + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'an (int) 1 slug' => array( + 'slug' => 1, + 'expiration' => 0, + ), + 'an (int) 0 slug' => array( + 'slug' => 0, + 'expiration' => 0, + ), + 'a (float) 1.0 slug' => array( + 'slug' => 1.0, + 'expiration' => 0, + ), + 'a (float) 0.0 slug' => array( + 'slug' => 0.0, + 'expiration' => 0, + ), + 'a NULL "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => null, + ), + 'a (float) 1.0 "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => 1.0, + ), + 'a (float) 0.0 "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => 0.0, + ), + 'a (string) "1" "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => '1', + ), + 'a (string) "0" "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => '0', + ), + 'a NAN "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => NAN, + ), + 'an INF "expiration" value' => array( + 'slug' => 'mynotice', + 'expiration' => INF, + ), + ); + } + + /** + * Tests that a notice with an invalid slug is not dismissed. + * + * @ticket + * + * @dataProvider data_should_not_dismiss_notice_with_invalid_slug + * + * @param mixed $slug The slug of the notice. + * 'skip' to skip adding the 'slug' request parameter. + * @param int|string $expiration Time until expiration in seconds. 0 = no expiration (permanent). + * 'skip' to skip adding the 'expiration' request parameter. + * @param string $expected The expected JSON error. + */ + public function test_should_not_dismiss_notice_with_invalid_slug( $slug, $expiration, $expected ) { + // Become an administrator. + $this->_setRole( 'administrator' ); + + // Set up a default request. + $_POST['nonce'] = wp_create_nonce( 'dismiss-notice' ); + + if ( 'skip' !== $slug ) { + $_POST['slug'] = $slug; + } + + if ( 'skip' !== $expiration ) { + $_POST['expiration'] = $expiration; + } + + // Make the request. + try { + $this->_handleAjax( 'dismiss-notice' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $this->assertFalse( + $response['success'], + 'The notice was dismissed.' + ); + + $this->assertSame( + $expected, + $response['data'], + 'An unexpected JSON error message was received.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_dismiss_notice_with_invalid_slug() { + return array( + 'an empty "dismissible" array' => array( + 'slug' => 'skip', + 'expiration' => 'skip', + 'expected' => 'Failed to dismiss notice: The notice does not have a slug.', + ), + 'no "slug" key in the "dismissible" array' => array( + 'slug' => 'skip', + 'expiration' => 0, + 'expected' => 'Failed to dismiss notice: The notice does not have a slug.', + ), + 'a NULL "slug" key in the "dismissible" array' => array( + 'slug' => null, + 'expiration' => 0, + 'expected' => 'Failed to dismiss notice: The notice does not have a slug.', + ), + 'a (bool) false "slug" key in the "dismissible" array' => array( + 'slug' => false, + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'an empty array "slug" key in the "dismissible" array' => array( + 'slug' => array(), + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'a populated array "slug" key in the "dismissible" array' => array( + 'slug' => array( 'mynotice-dismiss-forever' ), + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'an object "slug" key in the "dismissible" array' => array( + 'slug' => new stdClass(), + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'an empty string "slug" key in the "dismissible" array' => array( + 'slug' => '', + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + 'a "slug" key containing only space in the "dismissible" array' => array( + 'slug' => " \r\t\n", + 'expiration' => 0, + 'expected' => "Failed to dismiss notice: The notice's slug must not be an empty string.", + ), + ); + } + + /** + * Tests that a notice with an invalid slug is not dismissed. + * + * @ticket + * + * @dataProvider data_should_not_dismiss_notice_with_invalid_expiration + * + * @param mixed $expiration Time until expiration in seconds. 0 = no expiration (permanent). + * @param string $expected The expected JSON error. + */ + public function test_should_not_dismiss_notice_with_invalid_expiration( $expiration, $expected ) { + // Become an administrator. + $this->_setRole( 'administrator' ); + + // Set up a default request. + $_POST['nonce'] = wp_create_nonce( 'dismiss-notice' ); + $_POST['slug'] = 'mynotice'; + $_POST['expiration'] = $expiration; + + // Make the request. + try { + $this->_handleAjax( 'dismiss-notice' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $this->assertFalse( + $response['success'], + 'The notice was dismissed.' + ); + + $this->assertSame( + $expected, + $response['data'], + 'An unexpected JSON error message was received.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_dismiss_notice_with_invalid_expiration() { + return array( + 'a (bool) false "expiration" value' => array( + 'expiration' => false, + 'expected' => 'Failed to dismiss notice: The expiration time must be a number of seconds.', + ), + 'a (bool) true "expiration" value' => array( + 'expiration' => true, + 'expected' => 'Failed to dismiss notice: The expiration time must be a number of seconds.', + ), + 'an empty array "expiration" value' => array( + 'expiration' => array(), + 'expected' => 'Failed to dismiss notice: The expiration time must be a number of seconds.', + ), + 'a populated array "expiration" value' => array( + 'expiration' => array( 1 ), + 'expected' => 'Failed to dismiss notice: The expiration time must be a number of seconds.', + ), + 'an object "expiration" value' => array( + 'expiration' => new stdClass(), + 'expected' => 'Failed to dismiss notice: The expiration time must be a number of seconds.', + ), + 'a (string) "-1" "expiration" value' => array( + 'expiration' => '-1', + 'expected' => 'Failed to dismiss notice: The expiration time must be greater than or equal to 0.', + ), + 'a negative "expiration" value' => array( + 'expiration' => -1, + 'expected' => 'Failed to dismiss notice: The expiration time must be greater than or equal to 0.', + ), + ); + } + + /** + * Tests that a notice with an invalid nonce is not dismissed. + * + * @ticket + * + * @dataProvider data_should_not_dismiss_notice_with_invalid_nonce + * + * @param string $nonce The nonce for the request. + * 'skip' to skip adding the request parameter. + */ + public function test_should_not_dismiss_notice_with_invalid_nonce( $nonce ) { + // Become an administrator. + $this->_setRole( 'administrator' ); + + // Set up a default request. + if ( 'skip' !== $nonce ) { + $_POST['nonce'] = $nonce; + } + + $_POST['slug'] = 'mynotice'; + $_POST['expiration'] = 0; + + // Make the request. + $this->expectException( 'WPAjaxDieStopException', 'The request did not die.' ); + $this->expectExceptionMessage( '-1', 'An unexpected exception was thrown.' ); + $this->_handleAjax( 'dismiss-notice' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_dismiss_notice_with_invalid_nonce() { + return array( + 'no nonce' => array( + 'nonce' => 'skip', + ), + 'nonce for another action' => array( + 'nonce' => wp_create_nonce( 'my-other-action' ), + ), + ); + } + + /** + * Tests that a notice is not dismissed for a non-privileged user. + * + * @ticket + */ + public function test_should_not_dismiss_notice_for_a_non_privileged_user() { + // Set up a default request. + $_POST['nonce'] = wp_create_nonce( 'dismiss-notice' ); + $_POST['slug'] = 'mynotice'; + $_POST['expiration'] = 0; + + // Make the request. + $this->expectException( 'WPAjaxDieContinueException', 'The request did not die and continue.' ); + $this->expectExceptionMessage( '', 'An unexpected exception was thrown.' ); + $this->_handleAjax( 'dismiss-notice' ); + } + + /** + * Tests that a notice is not dismissed that is already dismissed. + * + * @ticket + */ + public function test_should_not_dismiss_notice_that_is_already_dismissed() { + // Dismiss the notice. + set_site_transient( 'wp_admin_notice_dismissed_mynotice', 1 ); + + // Set up a default request. + $_POST['nonce'] = wp_create_nonce( 'dismiss-notice' ); + $_POST['slug'] = 'mynotice'; + $_POST['expiration'] = 0; + + // Make the request. + try { + $this->_handleAjax( 'dismiss-notice' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Get the response. + $response = json_decode( $this->_last_response, true ); + + $this->assertFalse( + $response['success'], + 'The notice was dismissed.' + ); + + $this->assertSame( + 'Failed to dismiss notice: The notice could not be dismissed.', + $response['data'], + 'An unexpected JSON error message was received.' + ); + } +} diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index fd0ef6c44f2cf..917d3a2603a65 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -1,326 +1,666 @@ -assertSame( $expected, $actual ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_output_admin_notice() { - return array( - 'defaults' => array( - 'message' => 'A notice with defaults.', - 'args' => array(), - 'expected' => '

A notice with defaults.

', - ), - 'an empty message (used for templates)' => array( - 'message' => '', - 'args' => array( - 'type' => 'error', - 'dismissible' => true, - 'id' => 'message', - 'additional_classes' => array( 'inline', 'hidden' ), - ), - 'expected' => '', - ), - 'an empty message (used for templates) without paragraph wrapping' => array( - 'message' => '', - 'args' => array( - 'type' => 'error', - 'dismissible' => true, - 'id' => 'message', - 'additional_classes' => array( 'inline', 'hidden' ), - 'paragraph_wrap' => false, - ), - 'expected' => '', - ), - 'an "error" notice' => array( - 'message' => 'An "error" notice.', - 'args' => array( - 'type' => 'error', - ), - 'expected' => '

An "error" notice.

', - ), - 'a "success" notice' => array( - 'message' => 'A "success" notice.', - 'args' => array( - 'type' => 'success', - ), - 'expected' => '

A "success" notice.

', - ), - 'a "warning" notice' => array( - 'message' => 'A "warning" notice.', - 'args' => array( - 'type' => 'warning', - ), - 'expected' => '

A "warning" notice.

', - ), - 'an "info" notice' => array( - 'message' => 'An "info" notice.', - 'args' => array( - 'type' => 'info', - ), - 'expected' => '

An "info" notice.

', - ), - 'a type that already starts with "notice-"' => array( - 'message' => 'A type that already starts with "notice-".', - 'args' => array( - 'type' => 'notice-info', - ), - 'expected' => '

A type that already starts with "notice-".

', - ), - 'a dismissible notice' => array( - 'message' => 'A dismissible notice.', - 'args' => array( - 'dismissible' => true, - ), - 'expected' => '

A dismissible notice.

', - ), - 'no type and an ID' => array( - 'message' => 'A notice with an ID.', - 'args' => array( - 'id' => 'message', - ), - 'expected' => '

A notice with an ID.

', - ), - 'a type and an ID' => array( - 'message' => 'A warning notice with an ID.', - 'args' => array( - 'type' => 'warning', - 'id' => 'message', - ), - 'expected' => '

A warning notice with an ID.

', - ), - 'no type and additional classes' => array( - 'message' => 'A notice with additional classes.', - 'args' => array( - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A notice with additional classes.

', - ), - 'a type and additional classes' => array( - 'message' => 'A warning notice with additional classes.', - 'args' => array( - 'type' => 'warning', - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A warning notice with additional classes.

', - ), - 'a dismissible notice with a type and additional classes' => array( - 'message' => 'A dismissible warning notice with a type and additional classes.', - 'args' => array( - 'type' => 'warning', - 'dismissible' => true, - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A dismissible warning notice with a type and additional classes.

', - ), - 'a notice without paragraph wrapping' => array( - 'message' => 'A notice without paragraph wrapping.', - 'args' => array( - 'paragraph_wrap' => false, - ), - 'expected' => '
A notice without paragraph wrapping.
', - ), - 'an unsafe type' => array( - 'message' => 'A notice with an unsafe type.', - 'args' => array( - 'type' => '">', - ), - 'expected' => '
alert("Howdy,admin!");">

A notice with an unsafe type.

', - ), - 'an unsafe ID' => array( - 'message' => 'A notice with an unsafe ID.', - 'args' => array( - 'id' => '">
alert( "Howdy, admin!" );

A notice with an unsafe ID.

', - ), - 'unsafe additional classes' => array( - 'message' => 'A notice with unsafe additional classes.', - 'args' => array( - 'additional_classes' => array( '">
alert( "Howdy, admin!" );

A notice with unsafe additional classes.

', - ), - 'a type that is not a string' => array( - 'message' => 'A notice with a type that is not a string.', - 'args' => array( - 'type' => array(), - ), - 'expected' => '

A notice with a type that is not a string.

', - ), - 'a type with only empty space' => array( - 'message' => 'A notice with a type with only empty space.', - 'args' => array( - 'type' => " \t\r\n", - ), - 'expected' => '

A notice with a type with only empty space.

', - ), - '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' => '

A notice with an ID that is not a string.

', - ), - 'an ID with only empty space' => array( - 'message' => 'A notice with an ID with only empty space.', - 'args' => array( - 'id' => " \t\r\n", - ), - 'expected' => '

A notice with an ID with only empty space.

', - ), - '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' => '

A notice with dismissible as a truthy value rather than (bool) true.

', - ), - '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' => '

A notice with additional classes that are not an array.

', - ), - 'additional attribute with a value' => array( - 'message' => 'A notice with an additional attribute with a value.', - 'args' => array( - 'attributes' => array( 'aria-live' => 'assertive' ), - ), - 'expected' => '

A notice with an additional attribute with a value.

', - ), - 'additional hidden attribute' => array( - 'message' => 'A notice with the hidden attribute.', - 'args' => array( - 'attributes' => array( 'hidden' => true ), - ), - 'expected' => '', - ), - 'additional attribute no associative keys' => array( - 'message' => 'A notice with a boolean attribute without an associative key.', - 'args' => array( - 'attributes' => array( 'hidden' ), - ), - 'expected' => '', - ), - 'additional attribute with role' => array( - 'message' => 'A notice with an additional attribute role.', - 'args' => array( - 'attributes' => array( 'role' => 'alert' ), - ), - 'expected' => '', - ), - 'multiple additional attributes' => array( - 'message' => 'A notice with multiple additional attributes.', - 'args' => array( - 'attributes' => array( - 'role' => 'alert', - 'data-test' => -1, - ), - ), - 'expected' => '', - ), - 'data attribute with unsafe value' => array( - 'message' => 'A notice with an additional attribute with an unsafe value.', - 'args' => array( - 'attributes' => array( 'data-unsafe' => '' ), - ), - 'expected' => '

A notice with an additional attribute with an unsafe value.

', - ), - 'additional invalid attribute' => array( - 'message' => 'A notice with an additional attribute that is invalid.', - 'args' => array( - 'attributes' => array( 'not-valid' => 'not-valid' ), - ), - 'expected' => '

A notice with an additional attribute that is invalid.

', - ), - 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( - 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', - 'args' => array( - 'attributes' => array( - 'role' => 'alert', - 'disabled' => 'disabled', - 'data-name' => 'my-name', - 'data-id' => 1, - 'hidden', - ), - ), - 'expected' => '', - ), - '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' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', - ), - ); - } - - /** - * Tests that `_doing_it_wrong()` is thrown when a 'type' containing spaces is passed. - * - * @ticket 57791 - * - * @expectedIncorrectUsage wp_get_admin_notice - */ - public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { - ob_start(); - wp_admin_notice( - 'A type containing spaces.', - array( 'type' => 'first second third fourth' ) - ); - $actual = ob_get_clean(); - - $this->assertSame( - '

A type containing spaces.

', - $actual - ); - } - - /** - * Tests that `wp_admin_notice()` fires the 'wp_admin_notice' action. - * - * @ticket 57791 - */ - public function test_should_fire_wp_admin_notice_action() { - $action = new MockAction(); - add_action( 'wp_admin_notice', array( $action, 'action' ) ); - - ob_start(); - wp_admin_notice( 'A notice.', array( 'type' => 'success' ) ); - ob_end_clean(); - - $this->assertSame( 1, $action->get_call_count() ); - } -} +assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_output_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '

A notice with defaults.

', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '

An "error" notice.

', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '

A "success" notice.

', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '

A "warning" notice.

', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '

An "info" notice.

', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '

A type that already starts with "notice-".

', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '

A dismissible notice.

', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '

A notice with an ID.

', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '

A warning notice with an ID.

', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A notice with additional classes.

', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A warning notice with additional classes.

', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A dismissible warning notice with a type and additional classes.

', + ), + 'a notice without paragraph wrapping' => array( + 'message' => 'A notice without paragraph wrapping.', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '
A notice without paragraph wrapping.
', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '">', + ), + 'expected' => '
alert("Howdy,admin!");">

A notice with an unsafe type.

', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '">
alert( "Howdy, admin!" );

A notice with an unsafe ID.

', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '">
alert( "Howdy, admin!" );

A notice with unsafe additional classes.

', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '

A notice with a type that is not a string.

', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '

A notice with a type with only empty space.

', + ), + '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' => '

A notice with an ID that is not a string.

', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '

A notice with an ID with only empty space.

', + ), + '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' => '

A notice with dismissible as a truthy value rather than (bool) true.

', + ), + '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' => '

A notice with additional classes that are not an array.

', + ), + 'additional attribute with a value' => array( + 'message' => 'A notice with an additional attribute with a value.', + 'args' => array( + 'attributes' => array( 'aria-live' => 'assertive' ), + ), + 'expected' => '

A notice with an additional attribute with a value.

', + ), + 'additional hidden attribute' => array( + 'message' => 'A notice with the hidden attribute.', + 'args' => array( + 'attributes' => array( 'hidden' => true ), + ), + 'expected' => '', + ), + 'additional attribute no associative keys' => array( + 'message' => 'A notice with a boolean attribute without an associative key.', + 'args' => array( + 'attributes' => array( 'hidden' ), + ), + 'expected' => '', + ), + 'additional attribute with role' => array( + 'message' => 'A notice with an additional attribute role.', + 'args' => array( + 'attributes' => array( 'role' => 'alert' ), + ), + 'expected' => '', + ), + 'multiple additional attributes' => array( + 'message' => 'A notice with multiple additional attributes.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'data-test' => -1, + ), + ), + 'expected' => '', + ), + 'data attribute with unsafe value' => array( + 'message' => 'A notice with an additional attribute with an unsafe value.', + 'args' => array( + 'attributes' => array( 'data-unsafe' => '' ), + ), + 'expected' => '

A notice with an additional attribute with an unsafe value.

', + ), + 'additional invalid attribute' => array( + 'message' => 'A notice with an additional attribute that is invalid.', + 'args' => array( + 'attributes' => array( 'not-valid' => 'not-valid' ), + ), + 'expected' => '

A notice with an additional attribute that is invalid.

', + ), + 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( + 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'disabled' => 'disabled', + 'data-name' => 'my-name', + 'data-id' => 1, + 'hidden', + ), + ), + 'expected' => '', + ), + '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' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', + ), + 'a notice that should be dismissed permanently' => array( + 'message' => 'A notice that should be dismissed permanently.', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-permanently', + ), + ), + 'expected' => '

A notice that should be dismissed permanently.

', + ), + 'a notice that should be dismissed for 30 days' => array( + 'message' => 'A notice that should be dismissed for 30 days.', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-for-30-days', + 'expiration' => 30 * DAY_IN_SECONDS, + ), + ), + 'expected' => '

A notice that should be dismissed for 30 days.

', + ), + ); + } + + /** + * Tests that `_doing_it_wrong()` is thrown when a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + ob_start(); + wp_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ); + $actual = ob_get_clean(); + + $this->assertSame( + '

A type containing spaces.

', + $actual + ); + } + + /** + * Tests that `wp_admin_notice()` fires the 'wp_admin_notice' action. + * + * @ticket 57791 + */ + public function test_should_fire_wp_admin_notice_action() { + $action = new MockAction(); + add_action( 'wp_admin_notice', array( $action, 'action' ) ); + + ob_start(); + wp_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + ob_end_clean(); + + $this->assertSame( 1, $action->get_call_count() ); + } + + + /** + * Tests that `wp_admin_notice()` outputs an empty string for a notice that is still dismissed. + * + * @ticket + * + * @dataProvider data_notices_with_dismissible_array + * + * @param array $dismissible The value for the dismissible array. + */ + public function test_should_output_empty_string_for_a_notice_that_is_still_dismissed( $dismissible ) { + // The notice is still dismissed. + set_site_transient( 'wp_admin_notice_dismissed_' . $dismissible['slug'], 1 ); + + ob_start(); + wp_admin_notice( + 'A notice that is still dismissed.', + array( + 'dismissible' => $dismissible, + ) + ); + $actual = ob_get_clean(); + + $this->assertSame( '', $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_notices_with_dismissible_array() { + return array( + 'a permanently dismissed notice (slug only, no expiration provided)' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-forever', + ), + ), + 'a notice dismissed for 30 days' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-for-30-days', + 'expiration' => 30 * DAY_IN_SECONDS, + ), + ), + ); + } + + /** + * Tests that `wp_admin_notice()` triggers an error. + * + * @ticket + * + * @dataProvider data_should_trigger_error_for_an_invalid_dismissible_slug + * @dataProvider data_should_trigger_error_for_invalid_dismissible_expiration + * + * @param string $message The message. + * @param array $args Arguments for the admin notice. + * @param string $expected_markup The expected admin notice markup. + * @param string $expected_error The expected error message. + */ + public function test_should_trigger_error( $message, $args, $expected_markup, $expected_error ) { + // Ensure no previous errors exist. + error_clear_last(); + + // Backup the error reporting value. + $original_error_reporting = error_reporting(); + + // Suppress E_USER_NOTICE. + error_reporting( E_ALL & ~E_USER_NOTICE ); + + ob_start(); + wp_admin_notice( $message, $args ); + $actual = ob_get_clean(); + $last_error = error_get_last(); + + // Reset error reporting. + error_reporting( $original_error_reporting ); + + $this->assertSame( $expected_markup, $actual ); + $this->assertIsArray( $last_error, 'An error was not triggered.' ); + $this->assertSame( E_USER_NOTICE, $last_error['type'], 'The error was not a notice.' ); + $this->assertSame( $last_error['message'], 'wp_get_admin_notice(): ' . $expected_error, 'The wrong error message was sent.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error_for_an_invalid_dismissible_slug() { + return array( + 'an empty "dismissible" array' => array( + 'message' => 'an empty "dismissible" array', + 'args' => array( + 'dismissible' => array(), + ), + 'expected_markup' => '

an empty "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'no "slug" key in the "dismissible" array' => array( + 'message' => 'no "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'expiration' => 30 * DAY_IN_SECONDS ), + ), + 'expected_markup' => '

no "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a NULL "slug" key in the "dismissible" array' => array( + 'message' => 'a NULL "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => null ), + ), + 'expected_markup' => '

a NULL "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a (bool) false "slug" key in the "dismissible" array' => array( + 'message' => 'a (bool) false "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => false ), + ), + 'expected_markup' => '

a (bool) false "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a (bool) true "slug" key in the "dismissible" array' => array( + 'message' => 'a (bool) true "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => true ), + ), + 'expected_markup' => '

a (bool) true "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an integer "slug" key in the "dismissible" array' => array( + 'message' => 'an integer "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => 1234 ), + ), + 'expected_markup' => '

an integer "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a float "slug" key in the "dismissible" array' => array( + 'message' => 'a float "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => 12.34 ), + ), + 'expected_markup' => '

a float "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an empty array "slug" key in the "dismissible" array' => array( + 'message' => 'an empty array "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => array() ), + ), + 'expected_markup' => '

an empty array "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a populated array "slug" key in the "dismissible" array' => array( + 'message' => 'a populated array "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => array( 'mynotice-dismiss-forever' ) ), + ), + 'expected_markup' => '

a populated array "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an object "slug" key in the "dismissible" array' => array( + 'message' => 'an object "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => new stdClass() ), + ), + 'expected_markup' => '

an object "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an empty string "slug" key in the "dismissible" array' => array( + 'message' => 'an empty string "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => '' ), + ), + 'expected_markup' => '

an empty string "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a non-empty string.', + ), + 'a "slug" key containing only space in the "dismissible" array' => array( + 'message' => 'a "slug" key containing only space in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => " \r\t\n" ), + ), + 'expected_markup' => '

a "slug" key containing only space in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a non-empty string.', + ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error_for_invalid_dismissible_expiration() { + return array( + 'a NULL "expiration" value in the "dismissible" array' => array( + 'message' => 'a null "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-null-expiration', + 'expiration' => null, + ), + ), + 'expected_markup' => '

a null "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a (bool) false "expiration" value in the "dismissible" array' => array( + 'message' => 'a (bool) false "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-false-expiration', + 'expiration' => false, + ), + ), + 'expected_markup' => '

a (bool) false "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a (bool) true "expiration" value in the "dismissible" array' => array( + 'message' => 'a (bool) true "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-true-expiration', + 'expiration' => true, + ), + ), + 'expected_markup' => '

a (bool) true "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an empty array "expiration" value in the "dismissible" array' => array( + 'message' => 'an empty array "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-empty-array-expiration', + 'expiration' => array(), + ), + ), + 'expected_markup' => '

an empty array "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a populated array "expiration" value in the "dismissible" array' => array( + 'message' => 'a populated array "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-populated-array-expiration', + 'expiration' => array( 30 * DAY_IN_SECONDS ), + ), + ), + 'expected_markup' => '

a populated array "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an object "expiration" value in the "dismissible" array' => array( + 'message' => 'an object "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-object-expiration', + 'expiration' => array( 30 * DAY_IN_SECONDS ), + ), + ), + 'expected_markup' => '

an object "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a float "expiration" value in the "dismissible" array' => array( + 'message' => 'a float "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-float-expiration', + 'expiration' => 30.0, + ), + ), + 'expected_markup' => '

a float "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a numeric string "expiration" value in the "dismissible" array' => array( + 'message' => 'a numeric string "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-numeric-string-expiration', + 'expiration' => '30', + ), + ), + 'expected_markup' => '

a numeric string "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a NAN "expiration" value in the "dismissible" array' => array( + 'message' => 'a NAN "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-nan-expiration', + 'expiration' => NAN, + ), + ), + 'expected_markup' => '

a NAN "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an INF "expiration" value in the "dismissible" array' => array( + 'message' => 'an INF "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-inf-expiration', + 'expiration' => INF, + ), + ), + 'expected_markup' => '

an INF "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a negative "expiration" value in the "dismissible" array' => array( + 'message' => 'a negative "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-negative-expiration', + 'expiration' => -1, + ), + ), + 'expected_markup' => '

a negative "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be greater than or equal to 0.', + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php index 2aacdba7fed8d..9cfd4f5739378 100644 --- a/tests/phpunit/tests/functions/wpGetAdminNotice.php +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -1,326 +1,688 @@ -assertSame( $expected, wp_get_admin_notice( $message, $args ) ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_return_admin_notice() { - return array( - 'defaults' => array( - 'message' => 'A notice with defaults.', - 'args' => array(), - 'expected' => '

A notice with defaults.

', - ), - 'an empty message (used for templates)' => array( - 'message' => '', - 'args' => array( - 'type' => 'error', - 'dismissible' => true, - 'id' => 'message', - 'additional_classes' => array( 'inline', 'hidden' ), - ), - 'expected' => '', - ), - 'an empty message (used for templates) without paragraph wrapping' => array( - 'message' => '', - 'args' => array( - 'type' => 'error', - 'dismissible' => true, - 'id' => 'message', - 'additional_classes' => array( 'inline', 'hidden' ), - 'paragraph_wrap' => false, - ), - 'expected' => '', - ), - 'an "error" notice' => array( - 'message' => 'An "error" notice.', - 'args' => array( - 'type' => 'error', - ), - 'expected' => '

An "error" notice.

', - ), - 'a "success" notice' => array( - 'message' => 'A "success" notice.', - 'args' => array( - 'type' => 'success', - ), - 'expected' => '

A "success" notice.

', - ), - 'a "warning" notice' => array( - 'message' => 'A "warning" notice.', - 'args' => array( - 'type' => 'warning', - ), - 'expected' => '

A "warning" notice.

', - ), - 'an "info" notice' => array( - 'message' => 'An "info" notice.', - 'args' => array( - 'type' => 'info', - ), - 'expected' => '

An "info" notice.

', - ), - 'a type that already starts with "notice-"' => array( - 'message' => 'A type that already starts with "notice-".', - 'args' => array( - 'type' => 'notice-info', - ), - 'expected' => '

A type that already starts with "notice-".

', - ), - 'a dismissible notice' => array( - 'message' => 'A dismissible notice.', - 'args' => array( - 'dismissible' => true, - ), - 'expected' => '

A dismissible notice.

', - ), - 'no type and an ID' => array( - 'message' => 'A notice with an ID.', - 'args' => array( - 'id' => 'message', - ), - 'expected' => '

A notice with an ID.

', - ), - 'a type and an ID' => array( - 'message' => 'A warning notice with an ID.', - 'args' => array( - 'type' => 'warning', - 'id' => 'message', - ), - 'expected' => '

A warning notice with an ID.

', - ), - 'no type and additional classes' => array( - 'message' => 'A notice with additional classes.', - 'args' => array( - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A notice with additional classes.

', - ), - 'a type and additional classes' => array( - 'message' => 'A warning notice with additional classes.', - 'args' => array( - 'type' => 'warning', - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A warning notice with additional classes.

', - ), - 'a dismissible notice with a type and additional classes' => array( - 'message' => 'A dismissible warning notice with a type and additional classes.', - 'args' => array( - 'type' => 'warning', - 'dismissible' => true, - 'additional_classes' => array( 'error', 'notice-alt' ), - ), - 'expected' => '

A dismissible warning notice with a type and additional classes.

', - ), - 'a notice without paragraph wrapping' => array( - 'message' => 'A notice without paragraph wrapping.', - 'args' => array( - 'paragraph_wrap' => false, - ), - 'expected' => '
A notice without paragraph wrapping.
', - ), - 'an unsafe type' => array( - 'message' => 'A notice with an unsafe type.', - 'args' => array( - 'type' => '">', - ), - 'expected' => '
">

A notice with an unsafe type.

', - ), - 'an unsafe ID' => array( - 'message' => 'A notice with an unsafe ID.', - 'args' => array( - 'id' => '">

A notice with an unsafe ID.

', - ), - 'unsafe additional classes' => array( - 'message' => 'A notice with unsafe additional classes.', - 'args' => array( - 'additional_classes' => array( '">

A notice with unsafe additional classes.

', - ), - 'a type that is not a string' => array( - 'message' => 'A notice with a type that is not a string.', - 'args' => array( - 'type' => array(), - ), - 'expected' => '

A notice with a type that is not a string.

', - ), - 'a type with only empty space' => array( - 'message' => 'A notice with a type with only empty space.', - 'args' => array( - 'type' => " \t\r\n", - ), - 'expected' => '

A notice with a type with only empty space.

', - ), - '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' => '

A notice with an ID that is not a string.

', - ), - 'an ID with only empty space' => array( - 'message' => 'A notice with an ID with only empty space.', - 'args' => array( - 'id' => " \t\r\n", - ), - 'expected' => '

A notice with an ID with only empty space.

', - ), - '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' => '

A notice with dismissible as a truthy value rather than (bool) true.

', - ), - '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' => '

A notice with additional classes that are not an array.

', - ), - 'additional attribute with a value' => array( - 'message' => 'A notice with an additional attribute with a value.', - 'args' => array( - 'attributes' => array( 'aria-live' => 'assertive' ), - ), - 'expected' => '

A notice with an additional attribute with a value.

', - ), - 'additional hidden attribute' => array( - 'message' => 'A notice with the hidden attribute.', - 'args' => array( - 'attributes' => array( 'hidden' => true ), - ), - 'expected' => '', - ), - 'additional attribute no associative keys' => array( - 'message' => 'A notice with a boolean attribute without an associative key.', - 'args' => array( - 'attributes' => array( 'hidden' ), - ), - 'expected' => '', - ), - 'additional attribute with role' => array( - 'message' => 'A notice with an additional attribute role.', - 'args' => array( - 'attributes' => array( 'role' => 'alert' ), - ), - 'expected' => '', - ), - 'multiple additional attributes' => array( - 'message' => 'A notice with multiple additional attributes.', - 'args' => array( - 'attributes' => array( - 'role' => 'alert', - 'data-test' => -1, - ), - ), - 'expected' => '', - ), - 'data attribute with unsafe value' => array( - 'message' => 'A notice with an additional attribute with an unsafe value.', - 'args' => array( - 'attributes' => array( 'data-unsafe' => '' ), - ), - 'expected' => '

A notice with an additional attribute with an unsafe value.

', - ), - 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( - 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', - 'args' => array( - 'attributes' => array( - 'role' => 'alert', - 'disabled' => 'disabled', - 'data-name' => 'my-name', - 'data-id' => 1, - 'hidden', - ), - ), - 'expected' => '', - ), - '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' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', - ), - ); - } - - /** - * Tests that `wp_get_admin_notice()` throws a `_doing_it_wrong()` when - * a 'type' containing spaces is passed. - * - * @ticket 57791 - * - * @expectedIncorrectUsage wp_get_admin_notice - */ - public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { - $this->assertSame( - '

A type containing spaces.

', - wp_get_admin_notice( - 'A type containing spaces.', - array( 'type' => 'first second third fourth' ) - ) - ); - } - - /** - * Tests that `wp_get_admin_notice()` applies filters. - * - * @ticket 57791 - * - * @dataProvider data_should_apply_filters - * - * @param string $hook_name The name of the filter hook. - */ - public function test_should_apply_filters( $hook_name ) { - $filter = new MockAction(); - add_filter( $hook_name, array( $filter, 'filter' ) ); - - wp_get_admin_notice( 'A notice.', array( 'type' => 'success' ) ); - - $this->assertSame( 1, $filter->get_call_count() ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_apply_filters() { - return array( - 'wp_admin_notice_args' => array( 'hook_name' => 'wp_admin_notice_args' ), - 'wp_admin_notice_markup' => array( 'hook_name' => 'wp_admin_notice_markup' ), - ); - } -} +assertSame( $expected, wp_get_admin_notice( $message, $args ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_return_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '

A notice with defaults.

', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '

An "error" notice.

', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '

A "success" notice.

', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '

A "warning" notice.

', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '

An "info" notice.

', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '

A type that already starts with "notice-".

', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '

A dismissible notice.

', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '

A notice with an ID.

', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '

A warning notice with an ID.

', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A notice with additional classes.

', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A warning notice with additional classes.

', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '

A dismissible warning notice with a type and additional classes.

', + ), + 'a notice without paragraph wrapping' => array( + 'message' => 'A notice without paragraph wrapping.', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '
A notice without paragraph wrapping.
', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '">', + ), + 'expected' => '
">

A notice with an unsafe type.

', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '">

A notice with an unsafe ID.

', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '">

A notice with unsafe additional classes.

', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '

A notice with a type that is not a string.

', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '

A notice with a type with only empty space.

', + ), + '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' => '

A notice with an ID that is not a string.

', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '

A notice with an ID with only empty space.

', + ), + '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' => '

A notice with dismissible as a truthy value rather than (bool) true.

', + ), + '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' => '

A notice with additional classes that are not an array.

', + ), + 'additional attribute with a value' => array( + 'message' => 'A notice with an additional attribute with a value.', + 'args' => array( + 'attributes' => array( 'aria-live' => 'assertive' ), + ), + 'expected' => '

A notice with an additional attribute with a value.

', + ), + 'additional hidden attribute' => array( + 'message' => 'A notice with the hidden attribute.', + 'args' => array( + 'attributes' => array( 'hidden' => true ), + ), + 'expected' => '', + ), + 'additional attribute no associative keys' => array( + 'message' => 'A notice with a boolean attribute without an associative key.', + 'args' => array( + 'attributes' => array( 'hidden' ), + ), + 'expected' => '', + ), + 'additional attribute with role' => array( + 'message' => 'A notice with an additional attribute role.', + 'args' => array( + 'attributes' => array( 'role' => 'alert' ), + ), + 'expected' => '', + ), + 'multiple additional attributes' => array( + 'message' => 'A notice with multiple additional attributes.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'data-test' => -1, + ), + ), + 'expected' => '', + ), + 'data attribute with unsafe value' => array( + 'message' => 'A notice with an additional attribute with an unsafe value.', + 'args' => array( + 'attributes' => array( 'data-unsafe' => '' ), + ), + 'expected' => '

A notice with an additional attribute with an unsafe value.

', + ), + 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( + 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'disabled' => 'disabled', + 'data-name' => 'my-name', + 'data-id' => 1, + 'hidden', + ), + ), + 'expected' => '', + ), + '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' => '

A notice with paragraph wrapping as a falsy value rather than (bool) false.

', + ), + 'a notice that should be dismissed permanently' => array( + 'message' => 'A notice that should be dismissed permanently.', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-permanently', + ), + ), + 'expected' => '

A notice that should be dismissed permanently.

', + ), + 'a notice that should be dismissed for 30 days' => array( + 'message' => 'A notice that should be dismissed for 30 days.', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-for-30-days', + 'expiration' => 30 * DAY_IN_SECONDS, + ), + ), + 'expected' => '

A notice that should be dismissed for 30 days.

', + ), + ); + } + + /** + * Tests that `wp_get_admin_notice()` throws a `_doing_it_wrong()` when + * a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + $this->assertSame( + '

A type containing spaces.

', + wp_get_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ) + ); + } + + /** + * Tests that `wp_get_admin_notice()` applies filters. + * + * @ticket 57791 + * + * @dataProvider data_should_apply_filters + * + * @param string $hook_name The name of the filter hook. + */ + public function test_should_apply_filters( $hook_name ) { + $filter = new MockAction(); + add_filter( $hook_name, array( $filter, 'filter' ) ); + + wp_get_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_apply_filters() { + return array( + 'wp_admin_notice_args' => array( 'hook_name' => 'wp_admin_notice_args' ), + 'wp_admin_notice_markup' => array( 'hook_name' => 'wp_admin_notice_markup' ), + ); + } + + /** + * Tests that `wp_get_admin_notice()` returns an empty string for a notice that is still dismissed. + * + * @ticket + * + * @dataProvider data_notices_with_dismissible_array + * + * @param array $dismissible The value for the dismissible array. + */ + public function test_should_return_empty_string_for_a_notice_that_is_still_dismissed( $dismissible ) { + // The notice is still dismissed. + set_site_transient( 'wp_admin_notice_dismissed_' . $dismissible['slug'], 1 ); + + $this->assertSame( + '', + wp_get_admin_notice( + 'A notice that is still dismissed.', + array( + 'dismissible' => $dismissible, + ) + ) + ); + } + + /** + * Tests that `wp_get_admin_notice()` does not apply markup filters for a notice that is still dismissed. + * + * @ticket + * + * @dataProvider data_notices_with_dismissible_array + * + * @param array $dismissible The value for the dismissible array. + */ + public function test_should_not_apply_markup_filters_for_a_notice_that_is_still_dismissed( $dismissible ) { + $filter = new MockAction(); + add_filter( 'wp_admin_notice_markup', array( $filter, 'filter' ) ); + + // The notice is still dismissed. + set_site_transient( 'wp_admin_notice_dismissed_' . $dismissible['slug'], 1 ); + + wp_get_admin_notice( + 'A notice that is still dismissed.', + array( + 'dismissible' => $dismissible, + ) + ); + + $this->assertSame( 0, $filter->get_call_count() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_notices_with_dismissible_array() { + return array( + 'a permanently dismissed notice (slug only, no expiration provided)' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-forever', + ), + ), + 'a notice dismissed for 30 days' => array( + 'dismissible' => array( + 'slug' => 'mynotice-dismiss-for-30-days', + 'expiration' => 30 * DAY_IN_SECONDS, + ), + ), + ); + } + + /** + * Tests that `wp_get_admin_notice()` triggers an error. + * + * @ticket + * + * @dataProvider data_should_trigger_error_for_an_invalid_dismissible_slug + * @dataProvider data_should_trigger_error_for_invalid_dismissible_expiration + * + * @param string $message The message. + * @param array $args Arguments for the admin notice. + * @param string $expected_markup The expected admin notice markup. + * @param string $expected_error The expected error message. + */ + public function test_should_trigger_error( $message, $args, $expected_markup, $expected_error ) { + // Ensure no previous errors exist. + error_clear_last(); + + // Backup the error reporting value. + $original_error_reporting = error_reporting(); + + // Suppress E_USER_NOTICE. + error_reporting( E_ALL & ~E_USER_NOTICE ); + + $actual = wp_get_admin_notice( $message, $args ); + $last_error = error_get_last(); + + // Reset error reporting. + error_reporting( $original_error_reporting ); + + $this->assertSame( $expected_markup, $actual ); + $this->assertIsArray( $last_error, 'An error was not triggered.' ); + $this->assertSame( E_USER_NOTICE, $last_error['type'], 'The error was not a notice.' ); + $this->assertSame( $last_error['message'], 'wp_get_admin_notice(): ' . $expected_error, 'The wrong error message was sent.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error_for_an_invalid_dismissible_slug() { + return array( + 'an empty "dismissible" array' => array( + 'message' => 'an empty "dismissible" array', + 'args' => array( + 'dismissible' => array(), + ), + 'expected_markup' => '

an empty "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'no "slug" key in the "dismissible" array' => array( + 'message' => 'no "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'expiration' => 30 * DAY_IN_SECONDS ), + ), + 'expected_markup' => '

no "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a NULL "slug" key in the "dismissible" array' => array( + 'message' => 'a NULL "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => null ), + ), + 'expected_markup' => '

a NULL "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a (bool) false "slug" key in the "dismissible" array' => array( + 'message' => 'a (bool) false "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => false ), + ), + 'expected_markup' => '

a (bool) false "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a (bool) true "slug" key in the "dismissible" array' => array( + 'message' => 'a (bool) true "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => true ), + ), + 'expected_markup' => '

a (bool) true "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an integer "slug" key in the "dismissible" array' => array( + 'message' => 'an integer "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => 1234 ), + ), + 'expected_markup' => '

an integer "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a float "slug" key in the "dismissible" array' => array( + 'message' => 'a float "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => 12.34 ), + ), + 'expected_markup' => '

a float "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an empty array "slug" key in the "dismissible" array' => array( + 'message' => 'an empty array "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => array() ), + ), + 'expected_markup' => '

an empty array "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'a populated array "slug" key in the "dismissible" array' => array( + 'message' => 'a populated array "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => array( 'mynotice-dismiss-forever' ) ), + ), + 'expected_markup' => '

a populated array "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an object "slug" key in the "dismissible" array' => array( + 'message' => 'an object "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => new stdClass() ), + ), + 'expected_markup' => '

an object "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a string.', + ), + 'an empty string "slug" key in the "dismissible" array' => array( + 'message' => 'an empty string "slug" key in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => '' ), + ), + 'expected_markup' => '

an empty string "slug" key in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a non-empty string.', + ), + 'a "slug" key containing only space in the "dismissible" array' => array( + 'message' => 'a "slug" key containing only space in the "dismissible" array', + 'args' => array( + 'dismissible' => array( 'slug' => " \r\t\n" ), + ), + 'expected_markup' => '

a "slug" key containing only space in the "dismissible" array

', + 'expected_error' => 'The "slug" key in the "dismissible" array must be a non-empty string.', + ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error_for_invalid_dismissible_expiration() { + return array( + 'a NULL "expiration" value in the "dismissible" array' => array( + 'message' => 'a null "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-null-expiration', + 'expiration' => null, + ), + ), + 'expected_markup' => '

a null "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a (bool) false "expiration" value in the "dismissible" array' => array( + 'message' => 'a (bool) false "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-false-expiration', + 'expiration' => false, + ), + ), + 'expected_markup' => '

a (bool) false "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a (bool) true "expiration" value in the "dismissible" array' => array( + 'message' => 'a (bool) true "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-true-expiration', + 'expiration' => true, + ), + ), + 'expected_markup' => '

a (bool) true "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an empty array "expiration" value in the "dismissible" array' => array( + 'message' => 'an empty array "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-empty-array-expiration', + 'expiration' => array(), + ), + ), + 'expected_markup' => '

an empty array "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a populated array "expiration" value in the "dismissible" array' => array( + 'message' => 'a populated array "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-populated-array-expiration', + 'expiration' => array( 30 * DAY_IN_SECONDS ), + ), + ), + 'expected_markup' => '

a populated array "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an object "expiration" value in the "dismissible" array' => array( + 'message' => 'an object "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-object-expiration', + 'expiration' => array( 30 * DAY_IN_SECONDS ), + ), + ), + 'expected_markup' => '

an object "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a float "expiration" value in the "dismissible" array' => array( + 'message' => 'a float "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-float-expiration', + 'expiration' => 30.0, + ), + ), + 'expected_markup' => '

a float "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a numeric string "expiration" value in the "dismissible" array' => array( + 'message' => 'a numeric string "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-numeric-string-expiration', + 'expiration' => '30', + ), + ), + 'expected_markup' => '

a numeric string "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a NAN "expiration" value in the "dismissible" array' => array( + 'message' => 'a NAN "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-nan-expiration', + 'expiration' => NAN, + ), + ), + 'expected_markup' => '

a NAN "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'an INF "expiration" value in the "dismissible" array' => array( + 'message' => 'an INF "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-inf-expiration', + 'expiration' => INF, + ), + ), + 'expected_markup' => '

an INF "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be an integer.', + ), + 'a negative "expiration" value in the "dismissible" array' => array( + 'message' => 'a negative "expiration" value in the "dismissible" array', + 'args' => array( + 'dismissible' => array( + 'slug' => 'mynotice-negative-expiration', + 'expiration' => -1, + ), + ), + 'expected_markup' => '

a negative "expiration" value in the "dismissible" array

', + 'expected_error' => 'The "expiration" key in the "dismissible" array must be greater than or equal to 0.', + ), + ); + } +}