From aeac2967ce9a31345f481fd41050517d3be954d2 Mon Sep 17 00:00:00 2001
From: Lauri Eskola <lauri.eskola@acquia.com>
Date: Fri, 3 Feb 2023 19:13:45 +0200
Subject: [PATCH] =?UTF-8?q?Issue=20#2632750=20by=20Wim=20Leers,=20bnjmnm,?=
 =?UTF-8?q?=20krlucas,=20ronaldtebrake,=20zaporylie,=20mherchel,=20Fabianx?=
 =?UTF-8?q?,=20webchick,=20yogeshchaugule8,=20catch,=20darol100,=20yoroy,?=
 =?UTF-8?q?=20Bojhan,=20G=C3=A1bor=20Hojtsy:=20Interface=20previews/skelet?=
 =?UTF-8?q?on=20screens=20through=20optional=20"preview"=20or=20"placehold?=
 =?UTF-8?q?er"=20templates?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../Core/Render/PlaceholderGenerator.php      |  4 +
 core/lib/Drupal/Core/Render/Renderer.php      |  1 +
 core/modules/big_pipe/big_pipe.module         | 54 ++++++++++++
 core/modules/big_pipe/src/Render/BigPipe.php  | 11 +--
 .../Render/Placeholder/BigPipeStrategy.php    | 17 +++-
 .../big-pipe-interface-preview.html.twig      |  9 ++
 .../big_pipe_bypass_js.info.yml               |  5 ++
 .../big_pipe_bypass_js.module                 | 17 ++++
 .../big_pipe_test/big_pipe_test.routing.yml   |  8 ++
 .../src/BigPipePlaceholderTestCases.php       | 29 +++++-
 .../src/BigPipeTestController.php             | 31 +++++++
 .../tests/src/Functional/BigPipeTest.php      |  2 +-
 .../BigPipePreviewTest.php                    | 63 +++++++++++++
 ...peInterfacePreviewThemeSuggestionsTest.php | 88 +++++++++++++++++++
 ...r-link-builder-renderDisplayName.html.twig |  1 +
 .../umami/js/components/messages/messages.js  | 49 +++++++++++
 .../demo_umami/themes/umami/umami.info.yml    |  3 +-
 .../themes/umami/umami.libraries.yml          |  4 +-
 .../big-pipe-interface-preview.html.twig      |  9 ++
 19 files changed, 390 insertions(+), 15 deletions(-)
 create mode 100644 core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig
 create mode 100644 core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.info.yml
 create mode 100644 core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module
 create mode 100644 core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php
 create mode 100644 core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php
 create mode 100644 core/modules/big_pipe/tests/themes/big_pipe_test_theme/templates/big-pipe-interface-preview--user-toolbar-link-builder-renderDisplayName.html.twig
 create mode 100644 core/profiles/demo_umami/themes/umami/js/components/messages/messages.js
 create mode 100644 core/themes/stable9/templates/content/big-pipe-interface-preview.html.twig

diff --git a/core/lib/Drupal/Core/Render/PlaceholderGenerator.php b/core/lib/Drupal/Core/Render/PlaceholderGenerator.php
index de50f06a4f3d..c00da6ca5321 100644
--- a/core/lib/Drupal/Core/Render/PlaceholderGenerator.php
+++ b/core/lib/Drupal/Core/Render/PlaceholderGenerator.php
@@ -77,6 +77,10 @@ public function createPlaceholder(array $element) {
       '#cache' => TRUE,
     ]);
 
+    if (isset($element['#lazy_builder_preview'])) {
+      $placeholder_render_array['#preview'] = $element['#lazy_builder_preview'];
+    }
+
     // Be sure cache contexts and tags are sorted before serializing them and
     // making hash. Issue #3225328 removes sort from contexts and tags arrays
     // for performances reasons.
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index da19e3281f0e..8b95a5bc0f34 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -325,6 +325,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         '#lazy_builder',
         '#cache',
         '#create_placeholder',
+        '#lazy_builder_preview',
         // The keys below are not actually supported, but these are added
         // automatically by the Renderer. Adding them as though they are
         // supported allows us to avoid throwing an exception 100% of the time.
diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module
index fa0518ad3d1f..d7bc9e86c621 100644
--- a/core/modules/big_pipe/big_pipe.module
+++ b/core/modules/big_pipe/big_pipe.module
@@ -76,3 +76,57 @@ function big_pipe_page_attachments(array &$page) {
     }
   }
 }
+
+/**
+ * Implements hook_theme().
+ */
+function big_pipe_theme() {
+  return [
+    'big_pipe_interface_preview' => [
+      'variables' => [
+        'callback' => NULL,
+        'arguments' => NULL,
+        'preview' => NULL,
+      ],
+    ],
+  ];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function big_pipe_theme_suggestions_big_pipe_interface_preview(array $variables) {
+  $common_callbacks_simplified_suggestions = [
+    'Drupal_block_BlockViewBuilder__lazyBuilder' => 'block',
+  ];
+
+  $suggestions = [];
+  $suggestion = 'big_pipe_interface_preview';
+  if ($variables['callback']) {
+    $callback = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['callback']);
+    if (is_array($callback)) {
+      $callback = implode('__', $callback);
+    }
+
+    // Use simplified template suggestion, if any.
+    // For example, this simplifies
+    // big-pipe-interface-preview--Drupal-block-BlockViewBuilder--lazyBuilder--<BLOCK ID>.html.twig
+    // to
+    // big-pipe-interface-preview--block--<BLOCK ID>.html.twig
+    if (isset($common_callbacks_simplified_suggestions[$callback])) {
+      $callback = $common_callbacks_simplified_suggestions[$callback];
+    }
+
+    $suggestions[] = $suggestion .= '__' . $callback;
+    if (is_array($variables['arguments'])) {
+      $arguments = preg_replace('/[^a-zA-Z0-9]/', '_', $variables['arguments']);
+      foreach ($arguments as $argument) {
+        if (empty($argument)) {
+          continue;
+        }
+        $suggestions[] = $suggestion . '__' . $argument;
+      }
+    }
+  }
+  return $suggestions;
+}
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index 985502876338..0461890b7143 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -711,14 +711,11 @@ protected function renderPlaceholder($placeholder, array $placeholder_render_arr
    *   only keep the first occurrence.
    */
   protected function getPlaceholderOrder($html, $placeholders) {
-    $fragments = explode('<span data-big-pipe-placeholder-id="', $html);
-    array_shift($fragments);
     $placeholder_ids = [];
-
-    foreach ($fragments as $fragment) {
-      $t = explode('"></span>', $fragment, 2);
-      $placeholder_id = $t[0];
-      $placeholder_ids[] = $placeholder_id;
+    $dom = Html::load($html);
+    $xpath = new \DOMXPath($dom);
+    foreach ($xpath->query('//span[@data-big-pipe-placeholder-id]') as $node) {
+      $placeholder_ids[] = Html::escape($node->getAttribute('data-big-pipe-placeholder-id'));
     }
     $placeholder_ids = array_unique($placeholder_ids);
 
diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
index 74a031506cdb..6219a1fddca6 100644
--- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
+++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
@@ -198,8 +198,23 @@ protected static function placeholderIsAttributeSafe($placeholder) {
   protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
     $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
 
+    $interface_preview = [];
+    if (isset($placeholder_render_array['#lazy_builder'])) {
+      $interface_preview = [
+        '#theme' => 'big_pipe_interface_preview',
+        '#callback' => $placeholder_render_array['#lazy_builder'][0],
+        '#arguments' => $placeholder_render_array['#lazy_builder'][1],
+      ];
+      if (isset($placeholder_render_array['#preview'])) {
+        $interface_preview['#preview'] = $placeholder_render_array['#preview'];
+        unset($placeholder_render_array['#preview']);
+      }
+    }
+
     return [
-      '#markup' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></span>',
+      '#prefix' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '">',
+      'interface_preview' => $interface_preview,
+      '#suffix' => '</span>',
       '#cache' => [
         'max-age' => 0,
         'contexts' => [
diff --git a/core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig b/core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig
new file mode 100644
index 000000000000..06a2d13c11bd
--- /dev/null
+++ b/core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig
@@ -0,0 +1,9 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a BigPipe JS placeholder interface preview.
+ *
+ * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy::createBigPipeJsPlaceholder()
+ */
+#}
+{{ preview }}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.info.yml b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.info.yml
new file mode 100644
index 000000000000..0278a7b5a64e
--- /dev/null
+++ b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.info.yml
@@ -0,0 +1,5 @@
+name: 'BigPipe bypass JS'
+type: module
+description: 'Prevents the loading of Big Pipe JavaScript. Used for testing preview templates.'
+package: Testing
+version: VERSION
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module
new file mode 100644
index 000000000000..3cba4c13c3c8
--- /dev/null
+++ b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Provides a way to bypass Big Pipe JavaScript.
+ */
+
+/**
+ * Implements hook_library_info_alter().
+ *
+ * Disables Big Pipe JavaScript by removing the js file from the library.
+ */
+function big_pipe_bypass_js_library_info_alter(&$libraries, $extension) {
+  if ($extension === 'big_pipe') {
+    unset($libraries['big_pipe']['js']);
+  }
+}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml b/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
index 152bf7ac8951..8edbcec487b4 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
@@ -23,3 +23,11 @@ big_pipe_test_multi_occurrence:
     _title: 'BigPipe test multiple occurrences of the same placeholder'
   requirements:
     _access: 'TRUE'
+
+big_pipe_test_preview:
+  path: '/big_pipe_test_preview'
+  defaults:
+    _controller: '\Drupal\big_pipe_test\BigPipeTestController::placeholderPreview'
+    _title: 'Test placeholder previews'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
index 22d1da8f1ee9..b19d01872cee 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
@@ -3,6 +3,7 @@
 /**
  * @file
  */
+// cSpell:ignore Vxezb
 
 namespace Drupal\big_pipe_test;
 
@@ -58,7 +59,13 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
     );
     $status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
     $status_messages->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
+      '#prefix' => '<span data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA">',
+      'interface_preview' => [
+        '#theme' => 'big_pipe_interface_preview',
+        '#callback' => 'Drupal\Core\Render\Element\StatusMessages::renderMessages',
+        '#arguments' => [NULL],
+      ],
+      '#suffix' => '</span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
@@ -199,7 +206,9 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
     );
     $current_time->bigPipePlaceholderId = 'timecurrent-timetime';
     $current_time->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="timecurrent-timetime"></span>',
+      '#prefix' => '<span data-big-pipe-placeholder-id="timecurrent-timetime">',
+      'interface_preview' => [],
+      '#suffix' => '</span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
@@ -247,7 +256,13 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
     );
     $exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU';
     $exception->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></span>',
+      '#prefix' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU">',
+      'interface_preview' => [
+        '#theme' => 'big_pipe_interface_preview',
+        '#callback' => '\Drupal\big_pipe_test\BigPipeTestController::exception',
+        '#arguments' => ['llamas', 'suck'],
+      ],
+      '#suffix' => '</span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
@@ -289,7 +304,13 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
     );
     $embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=' . $token;
     $embedded_response_exception->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=' . $token . '"></span>',
+      '#prefix' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU">',
+      'interface_preview' => [
+        '#theme' => 'big_pipe_interface_preview',
+        '#callback' => '\Drupal\big_pipe_test\BigPipeTestController::responseException',
+        '#arguments' => [],
+      ],
+      '#suffix' => '</span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
index 0b36f1f8b268..6e6ea88f6a9b 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
@@ -82,6 +82,37 @@ public function multiOccurrence() {
     ];
   }
 
+  /**
+   * A page with placeholder preview.
+   *
+   * @return array[]
+   */
+  public function placeholderPreview() {
+    return [
+      'user_container' => [
+        '#type' => 'container',
+        '#attributes' => ['id' => 'placeholder-preview-twig-container'],
+        'user' => [
+          '#lazy_builder' => ['user.toolbar_link_builder:renderDisplayName', []],
+          '#create_placeholder' => TRUE,
+        ],
+      ],
+      'user_links_container' => [
+        '#type' => 'container',
+        '#attributes' => ['id' => 'placeholder-render-array-container'],
+        'user_links' => [
+          '#lazy_builder' => [static::class . '::helloOrYarhar', []],
+          '#create_placeholder' => TRUE,
+          '#lazy_builder_preview' => [
+            '#attributes' => ['id' => 'render-array-preview'],
+            '#type' => 'container',
+            '#markup' => 'There is a lamb and there is a puppy',
+          ],
+        ],
+      ],
+    ];
+  }
+
   /**
    * #lazy_builder callback; builds <time> markup with current time.
    *
diff --git a/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php b/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php
index 33900bdea47f..246cd47a64d9 100644
--- a/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php
+++ b/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php
@@ -370,7 +370,7 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde
     $placeholder_replacement_positions = [];
     foreach ($expected_big_pipe_placeholders as $big_pipe_placeholder_id => $expected_ajax_response) {
       // Verify expected placeholder.
-      $expected_placeholder_html = '<span data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '"></span>';
+      $expected_placeholder_html = '<span data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '">';
       $this->assertSession()->responseContains($expected_placeholder_html);
       $pos = strpos($this->getSession()->getPage()->getContent(), $expected_placeholder_html);
       $placeholder_positions[$pos] = $big_pipe_placeholder_id;
diff --git a/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php
new file mode 100644
index 000000000000..1ff07ecbd1f7
--- /dev/null
+++ b/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipePreviewTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Tests\big_pipe\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests placeholder preview functionality.
+ *
+ * @group big_pipe
+ */
+class BigPipePreviewTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'big_pipe',
+    'user',
+    'big_pipe_bypass_js',
+    'big_pipe_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'big_pipe_test_theme';
+
+  /**
+   * Test preview functionality within placeholders.
+   */
+  public function testLazyLoaderPreview() {
+    $user = $this->drupalCreateUser([]);
+    $display_name = $user->getDisplayName();
+    $this->drupalLogin($user);
+
+    $this->drupalGet('big_pipe_test_preview');
+
+    // This test begins with the big_pipe_bypass_js module enabled, which blocks
+    // Big Pipe's JavaScript from loading. Without that JavaScript, the
+    // placeholder and previews are not replaced and we can reliably test their
+    // presence.
+    $this->assertSession()->elementExists('css', '#placeholder-preview-twig-container [data-big-pipe-placeholder-id] > .i-am-taking-up-space');
+    $this->assertSession()->elementTextEquals('css', '#placeholder-preview-twig-container [data-big-pipe-placeholder-id] > .i-am-taking-up-space', 'LOOK AT ME I AM CONSUMING SPACE FOR LATER');
+    $this->assertSession()->elementTextNotContains('css', '#placeholder-preview-twig-container', $display_name);
+
+    $this->assertSession()->pageTextContains('There is a lamb and there is a puppy');
+    $this->assertSession()->elementTextEquals('css', '#placeholder-render-array-container [data-big-pipe-placeholder-id] > #render-array-preview', 'There is a lamb and there is a puppy');
+    $this->assertSession()->elementTextNotContains('css', '#placeholder-render-array-container', 'Yarhar llamas forever!');
+
+    // Uninstall big_pipe_bypass_js.
+    \Drupal::service('module_installer')->uninstall(['big_pipe_bypass_js']);
+    $this->rebuildAll();
+    $this->drupalGet('big_pipe_test_preview');
+    $this->assertSession()->waitForElementRemoved('css', '[data-big-pipe-placeholder-id]', 20000);
+    $this->assertSession()->elementTextContains('css', '#placeholder-preview-twig-container', $display_name);
+    $this->assertSession()->pageTextNotContains('LOOK AT ME I AM CONSUMING SPACE FOR LATER');
+
+    $this->assertSession()->elementTextContains('css', '#placeholder-render-array-container marquee', 'Yarhar llamas forever!');
+    $this->assertSession()->pageTextNotContains('There is a lamb and there is a puppy');
+  }
+
+}
diff --git a/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php b/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php
new file mode 100644
index 000000000000..9064a6074a23
--- /dev/null
+++ b/core/modules/big_pipe/tests/src/Kernel/BigPipeInterfacePreviewThemeSuggestionsTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\Tests\big_pipe\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\block\Entity\Block;
+
+/**
+ * Tests the big_pipe_theme_suggestions_big_pipe_interface_preview() function.
+ *
+ * @group big_pipe
+ */
+class BigPipeInterfacePreviewThemeSuggestionsTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['block', 'big_pipe', 'system'];
+
+  /**
+   * The block being tested.
+   *
+   * @var \Drupal\block\Entity\BlockInterface
+   */
+  protected $block;
+
+  /**
+   * The block storage.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
+   */
+  protected $controller;
+
+  /**
+   * The block view builder.
+   *
+   * @var \Drupal\block\BlockViewBuilder
+   */
+  protected $blockViewBuilder;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->controller = $this->container
+      ->get('entity_type.manager')
+      ->getStorage('block');
+
+    $this->blockViewBuilder = $this->container
+      ->get('entity_type.manager')
+      ->getViewBuilder('block');
+  }
+
+  /**
+   * Tests template suggestions from big_pipe_theme_suggestions_big_pipe_interface_preview().
+   */
+  public function testBigPipeThemeHookSuggestions() {
+    $entity = $this->controller->create([
+      'id' => 'test_block1',
+      'theme' => 'stark',
+      'plugin' => 'test_html',
+    ]);
+    $entity->save();
+
+    // Test the rendering of a block.
+    $block = Block::load('test_block1');
+    // Using the BlockViewBuilder we will be able to get a lovely
+    // #lazy_builder callback assigned.
+    $build = $this->blockViewBuilder->view($block);
+
+    $variables = [];
+    // In turn this is what createBigPipeJsPlaceholder() uses to
+    // build the BigPipe JS placeholder render array which is used as input
+    // for big_pipe_theme_suggestions_big_pipe_interface_preview().
+    $variables['callback'] = $build['#lazy_builder'][0];
+    $variables['arguments'] = $build['#lazy_builder'][1];
+    $suggestions = big_pipe_theme_suggestions_big_pipe_interface_preview($variables);
+    $suggested_id = preg_replace('/[^a-zA-Z0-9]/', '_', $block->id());
+    $this->assertSame([
+      'big_pipe_interface_preview__block',
+      'big_pipe_interface_preview__block__' . $suggested_id,
+      'big_pipe_interface_preview__block__full',
+    ], $suggestions);
+  }
+
+}
diff --git a/core/modules/big_pipe/tests/themes/big_pipe_test_theme/templates/big-pipe-interface-preview--user-toolbar-link-builder-renderDisplayName.html.twig b/core/modules/big_pipe/tests/themes/big_pipe_test_theme/templates/big-pipe-interface-preview--user-toolbar-link-builder-renderDisplayName.html.twig
new file mode 100644
index 000000000000..22b185a88d85
--- /dev/null
+++ b/core/modules/big_pipe/tests/themes/big_pipe_test_theme/templates/big-pipe-interface-preview--user-toolbar-link-builder-renderDisplayName.html.twig
@@ -0,0 +1 @@
+<span class="i-am-taking-up-space">LOOK AT ME I AM CONSUMING SPACE FOR LATER</span>
diff --git a/core/profiles/demo_umami/themes/umami/js/components/messages/messages.js b/core/profiles/demo_umami/themes/umami/js/components/messages/messages.js
new file mode 100644
index 000000000000..6f56fd345427
--- /dev/null
+++ b/core/profiles/demo_umami/themes/umami/js/components/messages/messages.js
@@ -0,0 +1,49 @@
+/**
+ * @file
+ * Message template overrides.
+ */
+
+((Drupal) => {
+  /**
+   * Overrides message theme function.
+   *
+   * @param {object} message
+   *   The message object.
+   * @param {string} message.text
+   *   The message text.
+   * @param {object} options
+   *   The message context.
+   * @param {string} options.type
+   *   The message type.
+   * @param {string} options.id
+   *   ID of the message, for reference.
+   *
+   * @return {HTMLElement}
+   *   A DOM Node.
+   */
+  Drupal.theme.message = ({ text }, { type, id }) => {
+    const messagesTypes = Drupal.Message.getMessageTypeLabels();
+    const messageWrapper = document.createElement('div');
+
+    messageWrapper.setAttribute('class', `messages messages--${type}`);
+    messageWrapper.setAttribute(
+      'role',
+      type === 'error' || type === 'warning' ? 'alert' : 'status',
+    );
+    messageWrapper.setAttribute('data-drupal-message-id', id);
+    messageWrapper.setAttribute('data-drupal-message-type', type);
+
+    messageWrapper.innerHTML = `
+    <div class="messages__content container">
+      <h2 class="visually-hidden">
+        ${messagesTypes[type]}
+      </h2>
+      <span class="messages__item">
+        ${text}
+      </span>
+    </div>
+  `;
+
+    return messageWrapper;
+  };
+})(Drupal);
diff --git a/core/profiles/demo_umami/themes/umami/umami.info.yml b/core/profiles/demo_umami/themes/umami/umami.info.yml
index 35fe076ff522..1e615dea31ab 100644
--- a/core/profiles/demo_umami/themes/umami/umami.info.yml
+++ b/core/profiles/demo_umami/themes/umami/umami.info.yml
@@ -7,7 +7,6 @@ libraries:
   - umami/classy.base
   - core/normalize
   - umami/global
-  - umami/messages
 
 libraries-override:
   layout_builder/twocol_section:
@@ -24,6 +23,8 @@ libraries-override:
         layouts/fourcol_section/fourcol_section.css: layouts/fourcol_section/fourcol_section.css
 
 libraries-extend:
+  core/drupal.message:
+    - umami/messages
   tour/tour-styling:
     - umami/demo-umami-tour
   core/drupal.dialog:
diff --git a/core/profiles/demo_umami/themes/umami/umami.libraries.yml b/core/profiles/demo_umami/themes/umami/umami.libraries.yml
index db84ec743d97..934d3f5e8258 100644
--- a/core/profiles/demo_umami/themes/umami/umami.libraries.yml
+++ b/core/profiles/demo_umami/themes/umami/umami.libraries.yml
@@ -56,7 +56,9 @@ global:
 messages:
   css:
     component:
-      css/components/messages/messages.css: { weight: -10 }
+      css/components/messages/messages.css: {}
+  js:
+    js/components/messages/messages.js: {}
 
 more-link:
   css:
diff --git a/core/themes/stable9/templates/content/big-pipe-interface-preview.html.twig b/core/themes/stable9/templates/content/big-pipe-interface-preview.html.twig
new file mode 100644
index 000000000000..edafd6ef8d74
--- /dev/null
+++ b/core/themes/stable9/templates/content/big-pipe-interface-preview.html.twig
@@ -0,0 +1,9 @@
+{#
+/**
+ * @file
+ * Theme override for a BigPipe JS placeholder interface preview.
+ *
+ * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy::createBigPipeJsPlaceholder()
+ */
+#}
+{{ preview }}
-- 
GitLab