diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php
index 5d3a98965b..9632540f98 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationActionsTest.php
@@ -31,7 +31,7 @@ class ModerationActionsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
- protected $defaultTheme = 'classy';
+ protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
@@ -81,16 +81,14 @@ public function testNodeStatusActions($action, $bundle, $warning_appears, $start
if ($warning_appears) {
if ($action == 'node_publish_action') {
- $this->assertSession()
- ->elementContains('css', '.messages--warning', node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly published.');
+ $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly published.', 'warning');
}
else {
- $this->assertSession()
- ->elementContains('css', '.messages--warning', node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly unpublished.');
+ $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly unpublished.', 'warning');
}
}
else {
- $this->assertSession()->elementNotExists('css', '.messages--warning');
+ $this->assertSession()->statusMessageNotExists('warning');
}
// Ensure after the action has run, the node matches the expected status.
diff --git a/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php
index 6b7b7f894e..25361b4674 100644
--- a/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php
+++ b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php
@@ -108,17 +108,16 @@ public function testUploadFileExceedingMaximumFileSize() {
$page->attachFileToField("files[field_file_0]", $this->fileSystem->realpath($invalid_file));
// An error message should appear informing the user that the file exceeded
- // the maximum file size.
- $this->assertSession()->waitForElement('css', '.messages--error');
- // The error message includes the actual file size limit which depends on
- // the current environment, so we check for a part of the message.
- $this->assertSession()->pageTextContains('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size');
+ // the maximum file size. The error message includes the actual file size
+ // limit which depends on the current environment, so we check for a part
+ // of the message.
+ $this->assertSession()->statusMessageContainsAfterWait('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size', 'error');
// Now upload a valid file and check that the error message disappears.
$valid_file = $this->generateFile('not_exceeding_post_max_size', 8, 8);
$page->attachFileToField("files[field_file_0]", $this->fileSystem->realpath($valid_file));
$this->assertSession()->waitForElement('named', ['id_or_name', 'field_file_0_remove_button']);
- $this->assertSession()->elementNotExists('css', '.messages--error');
+ $this->assertSession()->statusMessageNotExistsAfterWait('error');
}
}
diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
index 10d812e26e..03f44fec4b 100644
--- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
+++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
@@ -146,6 +146,27 @@ public function messengerServiceTest() {
return [];
}
+ /**
+ * Sets messages for testing the WebAssert methods related to messages.
+ *
+ * @return array
+ * Empty array, we just need the messages.
+ */
+ public function statusMessagesForAssertions(): array {
+ // Add a simple message of each type.
+ $this->messenger->addMessage('My Status Message', 'status');
+ $this->messenger->addMessage('My Error Message', 'error');
+ $this->messenger->addMessage('My Warning Message', 'warning');
+
+ // Add messages with special characters and/or markup.
+ $this->messenger->addStatus('This has " in the middle');
+ $this->messenger->addStatus('This has \' in the middle');
+ $this->messenger->addStatus('Thismarkup will be escaped.');
+ $this->messenger->addStatus('Peaches & cream');
+
+ return [];
+ }
+
/**
* Controller to return $_GET['destination'] for testing.
*
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index 800a289c7c..59b5d9116e 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -21,6 +21,14 @@ system_test.messenger_service:
requirements:
_access: 'TRUE'
+system_test.status_messages_for_assertions:
+ path: '/system-test/status-messages-for-assertions'
+ defaults:
+ _title: 'Set various message to test status message assertion methods'
+ _controller: '\Drupal\system_test\Controller\SystemTestController::statusMessagesForAssertions'
+ requirements:
+ _access: 'TRUE'
+
system_test.main_content_fallback:
path: '/system-test/main-content-fallback'
defaults:
diff --git a/core/modules/system/tests/src/Functional/Bootstrap/DrupalMessengerServiceTest.php b/core/modules/system/tests/src/Functional/Bootstrap/DrupalMessengerServiceTest.php
index 72dcca53be..a0d51126a2 100644
--- a/core/modules/system/tests/src/Functional/Bootstrap/DrupalMessengerServiceTest.php
+++ b/core/modules/system/tests/src/Functional/Bootstrap/DrupalMessengerServiceTest.php
@@ -4,6 +4,7 @@
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
+use PHPUnit\Framework\AssertionFailedError;
/**
* Tests the Messenger service.
@@ -61,4 +62,91 @@ public function testDrupalMessengerService() {
$assert->pageTextContains('system_test_preinstall_module called');
}
+ /**
+ * Tests assertion methods in WebAssert related to status messages.
+ */
+ public function testStatusMessageAssertions(): void {
+ $this->drupalGet(Url::fromRoute('system_test.status_messages_for_assertions'));
+
+ // Use the simple messages to test basic functionality.
+ // Test WebAssert::statusMessagesExists().
+ $this->assertSession()->statusMessageExists();
+ $this->assertSession()->statusMessageExists('status');
+ $this->assertSession()->statusMessageExists('error');
+ $this->assertSession()->statusMessageExists('warning');
+
+ // WebAssert::statusMessageContains().
+ $this->assertSession()->statusMessageContains('My Status Message');
+ $this->assertSession()->statusMessageContains('My Error Message');
+ $this->assertSession()->statusMessageContains('My Warning Message');
+ // Test partial match.
+ $this->assertSession()->statusMessageContains('My Status');
+ // Test with second arg.
+ $this->assertSession()->statusMessageContains('My Status Message', 'status');
+ $this->assertSession()->statusMessageContains('My Error Message', 'error');
+ $this->assertSession()->statusMessageContains('My Warning Message', 'warning');
+
+ // Test WebAssert::statusMessageNotContains().
+ $this->assertSession()->statusMessageNotContains('My Status Message is fake');
+ $this->assertSession()->statusMessageNotContains('My Status Message is fake', 'status');
+ $this->assertSession()->statusMessageNotContains('My Error Message', 'status');
+ $this->assertSession()->statusMessageNotContains('My Status Message', 'error');
+
+ // Check that special characters get handled correctly.
+ $this->assertSession()->statusMessageContains('This has " in the middle');
+ $this->assertSession()->statusMessageContains('This has \' in the middle');
+ $this->assertSession()->statusMessageContains('Thismarkup will be escaped');
+ $this->assertSession()->statusMessageContains('Peaches & cream');
+ $this->assertSession()->statusMessageNotContains('Peaches & cream');
+
+ // Go to a new route that only has messages of type 'status'.
+ $this->drupalGet(Url::fromRoute('system_test.messenger_service'));
+ // Test WebAssert::statusMessageNotExists().
+ $this->assertSession()->statusMessageNotExists('error');
+ $this->assertSession()->statusMessageNotExists('warning');
+
+ // Perform a few assertions that should fail. We can only call
+ // TestCase::expectException() once per test, so we make a few
+ // try/catch blocks.
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageContains('This message is not real');
+ }
+ catch (AssertionFailedError $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageNotContains('markup');
+ }
+ catch (AssertionFailedError $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageExists('error');
+ }
+ catch (AssertionFailedError $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageNotExists();
+ }
+ catch (AssertionFailedError $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ // Tests passing a bad status type.
+ $this->expectException(\InvalidArgumentException::class);
+ $this->assertSession()->statusMessageExists('not a valid type');
+ }
+
}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
index 03c6b3af15..9b8a6f3468 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MessageCommandTest.php
@@ -3,6 +3,7 @@
namespace Drupal\FunctionalJavascriptTests\Ajax;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\ExpectationFailedException;
/**
* Tests adding messages via AJAX command.
@@ -76,6 +77,94 @@ public function testMessageCommand() {
}
}
+ /**
+ * Tests methods in JsWebAssert related to status messages.
+ */
+ public function testJsStatusMessageAssertions(): void {
+ $page = $this->getSession()->getPage();
+
+ $this->drupalGet('ajax-test/message');
+
+ $page->pressButton('Make Message In Default Location');
+ $this->assertSession()->statusMessageContainsAfterWait('I am a message in the default location.');
+
+ $page->pressButton('Make Message In Alternate Location');
+ $this->assertSession()->statusMessageContainsAfterWait('I am a message in an alternate location.', 'status');
+
+ $page->pressButton('Make Warning Message');
+ $this->assertSession()->statusMessageContainsAfterWait('I am a warning message in the default location.', 'warning');
+
+ // Reload and test some negative assertions.
+ $this->drupalGet('ajax-test/message');
+
+ $page->pressButton('Make Message In Default Location');
+ // Use message that is not on page.
+ $this->assertSession()->statusMessageNotContainsAfterWait('This is not a real message');
+
+ $page->pressButton('Make Message In Alternate Location');
+ // Use message that exists but has the wrong type.
+ $this->assertSession()->statusMessageNotContainsAfterWait('I am a message in an alternate location.', 'warning');
+
+ // Test partial match.
+ $page->pressButton('Make Warning Message');
+ $this->assertSession()->statusMessageContainsAfterWait('I am a warning');
+
+ // One more reload to try with different arg combinations.
+ $this->drupalGet('ajax-test/message');
+
+ $page->pressButton('Make Message In Default Location');
+ $this->assertSession()->statusMessageExistsAfterWait();
+
+ $page->pressButton('Make Message In Alternate Location');
+ $this->assertSession()->statusMessageNotExistsAfterWait('error');
+
+ $page->pressButton('Make Warning Message');
+ $this->assertSession()->statusMessageExistsAfterWait('warning');
+
+ // Perform a few assertions that should fail. We can only call
+ // TestCase::expectException() once per test, so we make a few
+ // try/catch blocks.
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageContainsAfterWait('Not a real message');
+ }
+ catch (ExpectationFailedException $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageNotContainsAfterWait('I am a warning');
+ }
+ catch (ExpectationFailedException $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageExistsAfterWait('error');
+ }
+ catch (ExpectationFailedException $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ $expected_failure_occurred = FALSE;
+ try {
+ $this->assertSession()->statusMessageNotExistsAfterWait('warning');
+ }
+ catch (ExpectationFailedException $e) {
+ $expected_failure_occurred = TRUE;
+ }
+ $this->assertTrue($expected_failure_occurred);
+
+ // Tests passing a bad status type.
+ $this->expectException(\InvalidArgumentException::class);
+ $this->assertSession()->statusMessageExistsAfterWait('not a valid type');
+ }
+
/**
* Asserts that a message of the expected type appears.
*
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
index e9e1d9cdec..f3dd65b9be 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -8,6 +8,9 @@
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Drupal\Tests\WebAssert;
+use PHPUnit\Framework\Assert;
+use PHPUnit\Framework\Constraint\IsNull;
+use PHPUnit\Framework\Constraint\LogicalNot;
use WebDriver\Exception;
use WebDriver\Exception\CurlExec;
@@ -523,4 +526,137 @@ public static function isExceptionNotClickable(Exception $exception): bool {
return (bool) preg_match('/not (clickable|interactable|visible)/', $exception->getMessage());
}
+ /**
+ * Asserts that a status message exists after wait.
+ *
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageExistsAfterWait(string $type = NULL): void {
+ $selector = $this->buildJavascriptStatusMessageSelector('', $type);
+ $status_message_element = $this->waitForElement('xpath', $selector);
+ if ($type) {
+ $failure_message = sprintf('A status message of type "%s" does not appear on this page, but it should.', $type);
+ }
+ else {
+ $failure_message = 'A status message does not appear on this page, but it should.';
+ }
+ // There is no Assert::isNotNull() method, so we make our own constraint.
+ $constraint = new LogicalNot(new IsNull());
+ Assert::assertThat($status_message_element, $constraint, $failure_message);
+ }
+
+ /**
+ * Asserts that a status message does not exist after wait.
+ *
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageNotExistsAfterWait(string $type = NULL): void {
+ $selector = $this->buildJavascriptStatusMessageSelector('', $type);
+ $status_message_element = $this->waitForElement('xpath', $selector);
+ if ($type) {
+ $failure_message = sprintf('A status message of type "%s" appears on this page, but it should not.', $type);
+ }
+ else {
+ $failure_message = 'A status message appears on this page, but it should not.';
+ }
+ Assert::assertThat($status_message_element, Assert::isNull(), $failure_message);
+ }
+
+ /**
+ * Asserts that a status message containing given string exists after wait.
+ *
+ * @param string $message
+ * The partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageContainsAfterWait(string $message, string $type = NULL): void {
+ $selector = $this->buildJavascriptStatusMessageSelector($message, $type);
+ $status_message_element = $this->waitForElement('xpath', $selector);
+ if ($type) {
+ $failure_message = sprintf('A status message of type "%s" containing "%s" does not appear on this page, but it should.', $type, $message);
+ }
+ else {
+ $failure_message = sprintf('A status message containing "%s" does not appear on this page, but it should.', $type, $message);
+ }
+ // There is no Assert::isNotNull() method, so we make our own constraint.
+ $constraint = new LogicalNot(new IsNull());
+ Assert::assertThat($status_message_element, $constraint, $failure_message);
+ }
+
+ /**
+ * Asserts that no status message containing given string exists after wait.
+ *
+ * @param string $message
+ * The partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageNotContainsAfterWait(string $message, string $type = NULL): void {
+ $selector = $this->buildJavascriptStatusMessageSelector($message, $type);
+ $status_message_element = $this->waitForElement('xpath', $selector);
+ if ($type) {
+ $failure_message = sprintf('A status message of type "%s" containing "%s" appears on this page, but it should not.', $type, $message);
+ }
+ else {
+ $failure_message = sprintf('A status message containing "%s" appears on this page, but it should not.', $message);
+ }
+ Assert::assertThat($status_message_element, Assert::isNull(), $failure_message);
+ }
+
+ /**
+ * Builds a xpath selector for a message with given type and text.
+ *
+ * The selector is designed to work with the Drupal.theme.message
+ * template defined in message.js in addition to status-messages.html.twig
+ * in the system module.
+ *
+ * @param string|null $message
+ * The optional message or partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ *
+ * @return string
+ * The xpath selector for the message.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown when $type is not an allowed type.
+ */
+ private function buildJavascriptStatusMessageSelector(string $message = NULL, string $type = NULL): string {
+ $allowed_types = [
+ 'status',
+ 'error',
+ 'warning',
+ NULL,
+ ];
+ if (!in_array($type, $allowed_types, TRUE)) {
+ throw new \InvalidArgumentException(sprintf("If a status message type is specified, the allowed values are 'status', 'error', 'warning'. The value provided was '%s'.", $type));
+ }
+
+ if ($type) {
+ $class = 'messages--' . $type;
+ }
+ else {
+ $class = 'messages__wrapper';
+ }
+
+ if ($message) {
+ $js_selector = $this->buildXPathQuery('//div[contains(@class, :class) and contains(., :message)]', [
+ ':class' => $class,
+ ':message' => $message,
+ ]);
+ }
+ else {
+ $js_selector = $this->buildXPathQuery('//div[contains(@class, :class)]', [
+ ':class' => $class,
+ ]);
+ }
+
+ // We select based on WebAssert::buildStatusMessageSelector() or the
+ // js_selector we have just built.
+ return $this->buildStatusMessageSelector($message, $type) . ' | ' . $js_selector;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/WebAssert.php b/core/tests/Drupal/Tests/WebAssert.php
index a5d80a6e3c..4e2c0c0707 100644
--- a/core/tests/Drupal/Tests/WebAssert.php
+++ b/core/tests/Drupal/Tests/WebAssert.php
@@ -1125,4 +1125,136 @@ public function checkboxNotChecked($field, TraversableElement $container = NULL)
return parent::checkboxNotChecked($field, $container);
}
+ /**
+ * Asserts that a status message exists.
+ *
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageExists(string $type = NULL): void {
+ $selector = $this->buildStatusMessageSelector('', $type);
+ try {
+ $this->elementExists('xpath', $selector);
+ }
+ catch (ExpectationException $e) {
+ Assert::fail($e->getMessage());
+ }
+ }
+
+ /**
+ * Asserts that a status message does not exist.
+ *
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageNotExists(string $type = NULL): void {
+ $selector = $this->buildStatusMessageSelector('', $type);
+ try {
+ $this->elementNotExists('xpath', $selector);
+ }
+ catch (ExpectationException $e) {
+ Assert::fail($e->getMessage());
+ }
+ }
+
+ /**
+ * Asserts that a status message containing a given string exists.
+ *
+ * @param string $message
+ * The partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageContains(string $message, string $type = NULL): void {
+ $selector = $this->buildStatusMessageSelector($message, $type);
+ try {
+ $this->elementExists('xpath', $selector);
+ }
+ catch (ExpectationException $e) {
+ Assert::fail($e->getMessage());
+ }
+ }
+
+ /**
+ * Asserts that a status message containing a given string does not exist.
+ *
+ * @param string $message
+ * The partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ */
+ public function statusMessageNotContains(string $message, string $type = NULL): void {
+ $selector = $this->buildStatusMessageSelector($message, $type);
+ try {
+ $this->elementNotExists('xpath', $selector);
+ }
+ catch (ExpectationException $e) {
+ Assert::fail($e->getMessage());
+ }
+ }
+
+ /**
+ * Builds a xpath selector for a message with given type and text.
+ *
+ * The selector is designed to work with the status-messages.html.twig
+ * template in the system module.
+ *
+ * See Drupal\Core\Render\Element\StatusMessages for aria label definition.
+ *
+ * @param string|null $message
+ * The optional message or partial message to assert.
+ * @param string|null $type
+ * The optional message type: status, error, or warning.
+ *
+ * @return string
+ * The xpath selector for the message.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown when $type is not an allowed type.
+ */
+ protected function buildStatusMessageSelector(string $message = NULL, string $type = NULL): string {
+ $allowed_types = [
+ 'status',
+ 'error',
+ 'warning',
+ NULL,
+ ];
+ if (!in_array($type, $allowed_types, TRUE)) {
+ throw new \InvalidArgumentException(sprintf("If a status message type is specified, the allowed values are 'status', 'error', 'warning'. The value provided was '%s'.", $type));
+ }
+ $selector = '//div[@data-drupal-messages]';
+ $aria_label = NULL;
+ switch ($type) {
+ case 'status':
+ $aria_label = 'Status message';
+ break;
+
+ case 'error':
+ $aria_label = 'Error message';
+ break;
+
+ case 'warning':
+ $aria_label = 'Warning message';
+ }
+
+ if ($message && $aria_label) {
+ $selector = $this->buildXPathQuery($selector . '//div[contains(@aria-label, :aria_label) and contains(., :message)]', [
+ ':aria_label' => $aria_label,
+ ':message' => $message,
+ ]);
+ }
+ elseif ($message) {
+ $selector = $this->buildXPathQuery($selector . '//div[contains(., :message)]', [
+ ':message' => $message,
+ ]);
+ }
+ elseif ($aria_label) {
+ $selector = $this->buildXPathQuery($selector . '//div[@aria-label=:aria_label]', [
+ ':aria_label' => $aria_label,
+ ]);
+ }
+
+ return $selector;
+ }
+
}