diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 5da51e8..cc3fd71 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -233,6 +233,15 @@ drupal.machine-name: - core/drupalSettings - core/drupal.form +drupal.message: + version: VERSION + js: + misc/message.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupal.debounce + drupal.progress: version: VERSION js: diff --git a/core/lib/Drupal/Core/Render/Element/StatusMessages.php b/core/lib/Drupal/Core/Render/Element/StatusMessages.php index 530457f..cd3bf52 100644 --- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php @@ -75,19 +75,17 @@ public static function generatePlaceholder(array $element) { public static function renderMessages($type) { $render = []; $messages = drupal_get_messages($type); - if ($messages) { - // Render the messages. - $render = [ - '#theme' => 'status_messages', - // @todo Improve when https://www.drupal.org/node/2278383 lands. - '#message_list' => $messages, - '#status_headings' => [ - 'status' => t('Status message'), - 'error' => t('Error message'), - 'warning' => t('Warning message'), - ], - ]; - } + // Render the messages. + $render = [ + '#theme' => 'status_messages', + // @todo Improve when https://www.drupal.org/node/2278383 lands. + '#message_list' => $messages, + '#status_headings' => [ + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ], + ]; return $render; } diff --git a/core/misc/announce.js b/core/misc/announce.js index acf850a..cd1ee5b 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -50,21 +50,23 @@ /** * Concatenates announcements to a single string; appends to the live region. */ - function announce() { + function processAnnounce() { var text = []; var priority = 'polite'; var announcement; - // Create an array of announcement strings to be joined and appended to the - // aria live region. - var il = announcements.length; - for (var i = 0; i < il; i++) { - announcement = announcements.pop(); - text.unshift(announcement.text); - // If any of the announcements has a priority of assertive then the group - // of joined announcements will have this priority. - if (announcement.priority === 'assertive') { - priority = 'assertive'; + if (announcements.length) { + // Create an array of announcement strings to be joined and appended to + // the aria live region. + var il = announcements.length; + for (var i = 0; i < il; i++) { + announcement = announcements.pop(); + text.unshift(announcement.text); + // If any of the announcements has a priority of assertive then the + // group of joined announcements will have this priority. + if (announcement.priority === 'assertive') { + priority = 'assertive'; + } } } @@ -81,8 +83,13 @@ // The live text area is updated. Allow the AT to announce the text. liveElement.setAttribute('aria-busy', 'false'); } + } + // 200 ms is right at the cusp where humans notice a pause, so we will wait + // at most this much time before the set of queued announcements is read. + var debouncedProcessAnnounce = debounce(processAnnounce, 200); + /** * Triggers audio UAs to read the supplied text. * @@ -96,25 +103,24 @@ * * @param {string} text * A string to be read by the UA. - * @param {string} [priority='polite'] + * @param {string} priority * A string to indicate the priority of the message. Can be either * 'polite' or 'assertive'. * - * @return {function} - * The return of the call to debounce. - * * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops */ Drupal.announce = function (text, priority) { - // Save the text and priority into a closure variable. Multiple simultaneous - // announcements will be concatenated and read in sequence. - announcements.push({ - text: text, - priority: priority - }); - // Immediately invoke the function that debounce returns. 200 ms is right at - // the cusp where humans notice a pause, so we will wait - // at most this much time before the set of queued announcements is read. - return (debounce(announce, 200)()); + if (typeof text === 'string') { + // Save the text and priority into a closure variable. Multiple + // simultaneous announcements will be concatenated and read in sequence. + announcements.push({ + text: text, + priority: priority + }); + debouncedProcessAnnounce(announcements); + } + else { + throw new Error(Drupal.t('"text" passed Drupal.announce must be a string.')); + } }; }(Drupal, Drupal.debounce)); diff --git a/core/misc/message.js b/core/misc/message.js new file mode 100644 index 0000000..867bf67 --- /dev/null +++ b/core/misc/message.js @@ -0,0 +1,111 @@ +/** + * @file + * Message API. + */ +(function ($, Drupal, debounce) { + + 'use strict'; + + var messages = []; + var messagesElement; + var debouncedProcessMessages; + var messageWrapperSelector = '[data-drupal-messages]'; + + /** + * Builds a div element with the aria-live attribute and attaches it to the + * DOM. + */ + Drupal.behaviors.drupalMessage = { + attach: function () { + if (!messagesElement) { + var $messagesWrapper = $(messageWrapperSelector); + if (!$messagesWrapper.length) { + throw new Error(Drupal.t('There is no element with a [data-drupal-messages] attribute')); + } + messagesElement = $messagesWrapper[0]; + } + } + }; + + // Debounce the function in case Drupal.message() is used in a loop. + debouncedProcessMessages = debounce(processMessages, 100); + + /** + * Displays all queued messages in one pass instead of one after the other. + * @see debouncedProcessMessages + */ + function processMessages() { + var text = []; + + messages.reverse().forEach(function (message) { + text.unshift(Drupal.theme('message', message)); + }); + if (text.length) { + messagesElement.innerHTML += text.join(''); + } + // Reset the messages array after the messages are processed. + messages = []; + + } + + Drupal.message = {}; + + /** + * Displays a message on the page. + * + * @param {string} message + * The message to display + * @param {string} type + * Message type, can be either 'status', 'error' or 'warning'. + * Default to 'status' + * @param {string} context + * The context of the message, used for removing messages again. + */ + Drupal.message.add = function (message, type, context) { + if (typeof message === 'string') { + // Save the text and priority into a closure variable. Multiple + // simultaneous announcements will be concatenated and read in sequence. + messages.push({ + context: context || 'default', + text: message, + type: type || 'status' + }); + debouncedProcessMessages(); + } + }; + + /** + * Removes messages from the page. + * + * @param {string} context + * The message context to remove + * @param {string} type + * Message type, can be either 'status', 'error' or 'warning'. + */ + Drupal.message.remove = function (context, type) { + var selector = '.js-messages-context--' + (context || 'default'); + if (type) { + selector = '.messages--' + (type || 'status') + selector; + } + $(selector).remove(); + }; + + /** + * Theme function for a message. + * + * @param {object} message + * The message object. + * @param {string} message.text + * The message text. + * @param {string} message.type + * The message type. + * @param {string} message.context + * The message context. + * @return {string} + * A string representing a DOM fragment. + */ + Drupal.theme.message = function (message) { + return '
' + message.text + '
'; + }; + +}(jQuery, Drupal, Drupal.debounce)); diff --git a/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php b/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php index 69ec86f..ea8affa 100644 --- a/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php +++ b/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php @@ -88,33 +88,14 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf if ($container && $user) { $status_messages->embeddedAjaxResponseCommands = [ [ - 'command' => 'settings', - 'settings' => [ - 'ajaxPageState' => [ - 'theme' => 'classy', - 'libraries' => 'big_pipe/big_pipe,classy/base,classy/messages,core/drupal.active-link,core/html5shiv,core/normalize,system/base', - ], - 'pluralDelimiter' => PluralTranslatableMarkup::DELIMITER, - 'user' => [ - 'uid' => '1', - 'permissionsHash' => $container->get('user_permissions_hash_generator')->generate($user), - ], - ], - 'merge' => TRUE, - ], - [ - 'command' => 'add_css', - 'data' => '' . "\n" - ], - [ 'command' => 'insert', 'method' => 'replaceWith', 'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]', - 'data' => "\n" . ' ' . "\n ", + 'data' => '
' . "\n" . ' ' . "\n" . '
' . "\n", 'settings' => NULL, ], ]; - $status_messages->embeddedHtmlResponse = '' . "\n" . "\n" . ' ' . "\n \n"; + $status_messages->embeddedHtmlResponse = '
' . "\n" . ' ' . "\n" . '
' . "\n\n"; } diff --git a/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php new file mode 100644 index 0000000..c389e79 --- /dev/null +++ b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php @@ -0,0 +1,31 @@ +processNormalizedValues($normalized); + } + +} diff --git a/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php new file mode 100644 index 0000000..c9a6290 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php @@ -0,0 +1,44 @@ +config('serialization.settings')->get('bc_timestamp_normalizer_unix')) { + return ['value' => $timestamp]; + } + + // Otherwise, format the date string to the same that + // \Drupal\serialization\Normalizer\TimestampItemNormalizer will produce. + $date = new \DateTime(); + $date->setTimestamp($timestamp); + $date->setTimezone(new \DateTimeZone('UTC')); + + // Format is also added to the expected return values. + return [ + 'value' => $date->format(\DateTime::RFC3339), + 'format' => \DateTime::RFC3339, + ]; + } + +} diff --git a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php new file mode 100644 index 0000000..2fb956b --- /dev/null +++ b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php @@ -0,0 +1,85 @@ + 'U', + 'ISO 8601' => \DateTime::ISO8601, + 'RFC 3339' => \DateTime::RFC3339, + ]; + + /** + * Processes normalized timestamp values to add a formatted date and format. + * + * @param array $data + * @return array + */ + protected function processNormalizedValues(array $data) { + // Use a RFC 3339 timestamp with the time zone set to UTC to replace the + // timestamp value. + $date = new \DateTime(); + $date->setTimestamp($data['value']); + $date->setTimezone(new \DateTimeZone('UTC')); + $data['value'] = $date->format(\DateTime::RFC3339); + // 'format' is not a property on TimestampItem fields. This is present to + // assist consumers of this data. + $data['format'] = \DateTime::RFC3339; + + return $data; + } + + /** + * {@inheritdoc} + */ + protected function constructValue($data, $context) { + // Loop through the allowed formats and create a TimestampItem from the + // input data if it matches the defined pattern. Since the formats are + // unambiguous (i.e., they reference an absolute time with a defined time + // zone), only one will ever match. + $timezone = new \DateTimeZone('UTC'); + + // First check for a provided format. + if (!empty($data['format']) && in_array($data['format'], $this->allowedFormats)) { + $date = \DateTime::createFromFormat($data['format'], $data['value'], $timezone); + return ['value' => $date->getTimestamp()]; + } + // Otherwise, loop through formats. + else { + foreach ($this->allowedFormats as $format) { + if (($date = \DateTime::createFromFormat($format, $data['value'], $timezone)) !== FALSE) { + return ['value' => $date->getTimestamp()]; + } + } + } + + $format_strings = []; + + foreach ($this->allowedFormats as $label => $format) { + $format_strings[] = "\"$format\" ($label)"; + } + + $formats = implode(', ', $format_strings); + throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data['value'], $formats)); + } + +} diff --git a/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php new file mode 100644 index 0000000..8f6d8ad --- /dev/null +++ b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php @@ -0,0 +1,44 @@ +processNormalizedValues($data); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = NULL, array $context = []) { + if (empty($data['value'])) { + throw new InvalidArgumentException('No "value" attribute present'); + } + + return parent::denormalize($data, $class, $format, $context); + } + + +} diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php new file mode 100644 index 0000000..9470949 --- /dev/null +++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php @@ -0,0 +1,146 @@ +normalizer = new TimestampItemNormalizer(); + } + + /** + * @covers ::supportsNormalization + */ + public function testSupportsNormalization() { + $timestamp_item = $this->createTimestampItemProphecy(); + $this->assertTrue($this->normalizer->supportsNormalization($timestamp_item->reveal())); + + $entity_ref_item = $this->prophesize(EntityReferenceItem::class); + $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal())); + } + + /** + * @covers ::supportsDenormalization + */ + public function testSupportsDenormalization() { + $timestamp_item = $this->createTimestampItemProphecy(); + $this->assertTrue($this->normalizer->supportsDenormalization($timestamp_item->reveal(), TimestampItem::class)); + + $entity_ref_item = $this->prophesize(EntityReferenceItem::class); + $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal(), TimestampItem::class)); + } + + /** + * Tests the normalize function. + * + * @covers ::normalize + */ + public function testNormalize() { + $expected = ['value' => '2016-11-06T09:02:00+00:00', 'format' => \DateTime::RFC3339]; + + $timestamp_item = $this->createTimestampItemProphecy(); + $timestamp_item->getIterator() + ->willReturn(new \ArrayIterator(['value' => 1478422920])); + + $serializer = new Serializer(); + $this->normalizer->setSerializer($serializer); + + $normalized = $this->normalizer->normalize($timestamp_item->reveal()); + $this->assertSame($expected, $normalized); + } + + /** + * Tests the denormalize function with good data. + * + * @covers ::denormalize + * @dataProvider providerTestDenormalizeValidFormats + */ + public function testDenormalizeValidFormats($value, $expected) { + $normalized = ['value' => $value]; + + $timestamp_item = $this->createTimestampItemProphecy(); + // The field item should be set with the expected timestamp. + $timestamp_item->setValue(['value' => $expected]) + ->shouldBeCalled(); + + $context = ['target_instance' => $timestamp_item->reveal()]; + + $denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context); + $this->assertTrue($denormalized instanceof TimestampItem); + } + + /** + * Data provider for testDenormalizeValidFormats. + * + * @return array + */ + public function providerTestDenormalizeValidFormats() { + $expected_stamp = 1478422920; + + $data = []; + + $data['U'] = [$expected_stamp, $expected_stamp]; + $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp]; + $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp]; + + return $data; + } + + /** + * Tests the denormalize function with bad data. + * + * @covers ::denormalize + */ + public function testDenormalizeException() { + $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).'); + + $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()]; + + $normalized = ['value' => '2016/11/06 09:02am GMT']; + $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context); + } + + /** + * Creates a Timestamp Item prophecy. + * + * @return \Prophecy\Prophecy\ObjectProphecy + */ + protected function createTimestampItemProphecy() { + $timestamp_item = $this->prophesize(TimestampItem::class); + $timestamp_item->getParent() + ->willReturn(TRUE); + + return $timestamp_item; + } + +} diff --git a/core/modules/system/templates/status-messages.html.twig b/core/modules/system/templates/status-messages.html.twig index 73bd9b7..e8f64e0 100644 --- a/core/modules/system/templates/status-messages.html.twig +++ b/core/modules/system/templates/status-messages.html.twig @@ -23,25 +23,27 @@ * @ingroup themeable */ #} +
{% for type, messages in message_list %}
{% if type == 'error' %}
{% endif %} - {% if status_headings[type] %} -

{{ status_headings[type] }}

- {% endif %} - {% if messages|length > 1 %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% else %} - {{ messages|first }} - {% endif %} + {% if status_headings[type] %} +

{{ status_headings[type] }}

+ {% endif %} + {% if messages|length > 1 %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% else %} + {{ messages|first }} + {% endif %} {% if type == 'error' %}
{% endif %}
{% endfor %} +
diff --git a/core/modules/system/tests/modules/js_message_test/js/js_message_test.js b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js new file mode 100644 index 0000000..b38a1c2 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js @@ -0,0 +1,40 @@ +/** + * @file + * Testing behavior for JSMessageTest. + */ + +(function ($, message) { + + 'use strict'; + + /** + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Add click listeners that show and remove links with context and type. + */ + Drupal.behaviors.js_message_test = { + attach: function (context) { + $('.show-link').once('show-msg').on('click', function (e) { + e.preventDefault(); + var type = e.currentTarget.getAttribute('data-type'); + var context = e.currentTarget.getAttribute('data-context'); + message.add('Msg-' + context + '-' + type, type, context); + }); + $('.remove-link').once('remove-msg').on('click', function (e) { + e.preventDefault(); + var type = e.currentTarget.getAttribute('data-type'); + var context = e.currentTarget.getAttribute('data-context'); + message.remove(context, type); + }); + $('.show-multiple').once('show-msg').on('click', function (e) { + e.preventDefault(); + for (var i = 0; i < 10; i++) { + message.add('Msg-' + i, 'status', 'context-' + i); + } + + }); + } + }; + +})(jQuery, Drupal.message); diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml new file mode 100644 index 0000000..e8bc73b --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml @@ -0,0 +1,6 @@ +name: 'JS Message test module' +type: module +description: 'Module for the JSMessageTest test.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml new file mode 100644 index 0000000..53da0f9 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml @@ -0,0 +1,7 @@ +show_message: + version: VERSION + js: + js/js_message_test.js: {} + dependencies: + - core/drupal.message + - core/jquery.once diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml new file mode 100644 index 0000000..4c325e8 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml @@ -0,0 +1,7 @@ +js_message_test.links: + path: '/js_message_test_link' + defaults: + _controller: '\Drupal\js_message_test\Controller\JSMessageTestController::messageLinks' + _title: 'JsMessageLinks' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php new file mode 100644 index 0000000..239d4ef --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php @@ -0,0 +1,71 @@ + "Show-$context-$type", + 'url' => Url::fromRoute('js_message_test.links'), + 'attributes' => [ + 'id' => "show-$context-$type", + 'data-context' => $context, + 'data-type' => $type, + 'class' => ['show-link'], + ], + ]; + $links["remove-$context-$type"] = [ + 'title' => "Remove-$context-$type", + 'url' => Url::fromRoute('js_message_test.links'), + 'attributes' => [ + 'id' => "remove-$context-$type", + 'data-context' => $context, + 'data-type' => $type, + 'class' => ['remove-link'], + ], + ]; + } + $links["remove-$context"] = [ + 'title' => "Remove-$context-all", + 'url' => Url::fromRoute('js_message_test.links'), + 'attributes' => [ + 'id' => "remove-$context", + 'data-context' => $context, + 'class' => ['remove-link'], + ], + ]; + } + $links['show-multi'] = [ + 'title' => "Show Multiple", + 'url' => Url::fromRoute('js_message_test.links'), + 'attributes' => [ + 'class' => ['show-multiple'], + ], + ]; + return [ + '#theme' => 'links', + '#links' => $links, + '#attached' => [ + 'library' => [ + 'js_message_test/show_message', + ], + ], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php new file mode 100644 index 0000000..c218f41 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php @@ -0,0 +1,143 @@ +assertSession(); + $this->drupalGet('js_message_test_link'); + $web_assert->elementExists('css', '[data-drupal-messages]'); + + foreach ($this->msgContexts as $context) { + foreach ($this->msgTypes as $type) { + $this->clickLink("Show-$context-$type"); + $selector = ".messages.messages--$type.js-messages.js-messages-context--$context"; + $msg_element = $web_assert->waitForElementVisible('css', $selector); + $this->assertNotEmpty($msg_element, "Message element visible: $selector"); + $web_assert->elementContains('css', $selector, "Msg-$context-$type"); + // Click all remove links except the one that will remove the message. + $this->clickAllRemoveLinksExcept($context, $type); + // Confirm the message was not removed. + $web_assert->elementContains('css', $selector, "Msg-$context-$type"); + // Click the remove links that should remove the message. + $this->clickLink("Remove-$context-$type"); + $web_assert->elementNotExists('css', $selector); + } + } + + // Test removing all messages of a specific context. + $this->clickLink("Show-context1-error"); + $this->clickLink("Show-context2-error"); + $this->clickLink("Show-context1-warning"); + $this->clickLink("Show-context2-warning"); + $this->waitForMessageElement('warning', 'context2'); + $this->assertCurrentMessages([ + 'Msg-context1-error', + 'Msg-context2-error', + 'Msg-context1-warning', + 'Msg-context2-warning', + ]); + $this->clickLink('Remove-context1-all'); + $web_assert->assertWaitOnAjaxRequest(); + $this->assertCurrentMessages([ + 'Msg-context2-error', + 'Msg-context2-warning', + ]); + $this->clickLink('Remove-context2-all'); + $this->assertCurrentMessages([]); + + // Test adding multiple messages at once. + // @see processMessages() + $this->clickLink('Show Multiple'); + $this->waitForMessageElement('status', 'context-9'); + + $current_messages = []; + for ($i = 0; $i < 10; $i++) { + $current_messages[] = "Msg-$i"; + } + $this->assertCurrentMessages($current_messages); + } + + /** + * Clicks all remove message links except for combination specified. + * + * @param string $exclude_context + * The message context to exclude. + * @param string $exclude_type + * The message type to exclude. + */ + protected function clickAllRemoveLinksExcept($exclude_context, $exclude_type) { + foreach ($this->msgContexts as $context) { + foreach ($this->msgTypes as $type) { + if ($context !== $exclude_context || $type !== $exclude_type) { + $this->clickLink("Remove-$context-$type"); + } + } + } + } + + /** + * Asserts that currently shown messages match expected messages. + * + * @param array $expected_messages + * Expected messages. + */ + protected function assertCurrentMessages(array $expected_messages) { + $current_messages = []; + if ($message_divs = $this->getSession()->getPage()->findAll('css', '.messages')) { + foreach ($message_divs as $message_div) { + /** @var \Behat\Mink\Element\NodeElement $message_div */ + $current_messages[] = $message_div->getText(); + } + } + $this->assertEquals($expected_messages, $current_messages); + } + + /** + * Waits for an element message to be visible. + * + * @param string $type + * The type of the message. + * @param string $context + * The context of the message. + */ + protected function waitForMessageElement($type, $context) { + $last_msg_element = $this->assertSession()->waitForElementVisible('css', ".messages.messages--$type.js-messages.js-messages-context--$context"); + $this->assertNotEmpty($last_msg_element, "Message element visible."); + } + +} diff --git a/core/themes/classy/templates/misc/status-messages.html.twig b/core/themes/classy/templates/misc/status-messages.html.twig index bc8fd10..0d059f3 100644 --- a/core/themes/classy/templates/misc/status-messages.html.twig +++ b/core/themes/classy/templates/misc/status-messages.html.twig @@ -21,7 +21,7 @@ * - class: HTML classes. */ #} -{{ attach_library('classy/messages') }} +
{% block messages %} {% for type, messages in message_list %} {% @@ -54,3 +54,4 @@ {% set attributes = attributes.removeClass(classes) %} {% endfor %} {% endblock messages %} +