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; + } + }