Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_bypass_js\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for big_pipe_bypass_js.
*/
class BigPipeBypassJsHooks {
/**
* Implements hook_library_info_alter().
*
* Disables Big Pipe JavaScript by removing the js file from the library.
*/
#[Hook('library_info_alter')]
public function libraryInfoAlter(&$libraries, $extension): void {
if ($extension === 'big_pipe') {
unset($libraries['big_pipe']['js']);
}
}
}

View File

@ -0,0 +1,5 @@
name: 'BigPipe messages test'
type: module
description: 'Forces the messages placeholder to go via the big pipe strategy for testing purposes.'
package: Testing
version: VERSION

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_messages_test\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Security\Attribute\TrustedCallback;
/**
* Hook implementations for big_pipe_test.
*/
class BigPipeMessagesHooks {
/**
* Implements hook_element_info_alter().
*/
#[Hook('element_info_alter')]
public function elementInfoAlter(array &$info): void {
$info['status_messages']['#pre_render'][] = static::class . '::preRenderMessages';
}
/**
* Pre render callback.
*
* Removes #placeholder_strategy from the messages element to force the
* messages placeholder to go via the big pipe strategy for testing purposes.
*/
#[TrustedCallback]
public static function preRenderMessages(array $element): array {
if (isset($element['#attached']['placeholders'])) {
$key = key($element['#attached']['placeholders']);
unset($element['#attached']['placeholders'][$key]['#placeholder_strategy_denylist']);
}
if (isset($element['messages']['#attached']['placeholders'])) {
$key = key($element['messages']['#attached']['placeholders']);
unset($element['messages']['#attached']['placeholders'][$key]['#placeholder_strategy_denylist']);
}
return $element;
}
}

View File

@ -0,0 +1,5 @@
name: 'BigPipe regression test'
type: module
description: 'Support module for BigPipe regression testing.'
package: Testing
version: VERSION

View File

@ -0,0 +1,28 @@
big_pipe_regression_test.2678662:
path: '/big_pipe_regression_test/2678662'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::regression2678662'
requirements:
_access: 'TRUE'
big_pipe_regression_test.2802923:
path: '/big_pipe_regression_test/2802923'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::regression2802923'
requirements:
_access: 'TRUE'
big_pipe_test_large_content:
path: '/big_pipe_test_large_content'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::largeContent'
_title: 'BigPipe test large content'
requirements:
_access: 'TRUE'
big_pipe_test_multiple_replacements:
path: '/big_pipe_test_multiple_replacements'
defaults:
_controller: '\Drupal\big_pipe_regression_test\BigPipeRegressionTestController::multipleReplacements'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_regression_test;
use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\Component\Utility\Random;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Controller for BigPipe regression tests.
*/
class BigPipeRegressionTestController implements TrustedCallbackInterface {
const MARKER_2678662 = '<script>var hitsTheFloor = "</body>";</script>';
const PLACEHOLDER_COUNT = 2000;
/**
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testMultipleBodies_2678662()
*/
public function regression2678662() {
return [
'#markup' => BigPipeMarkup::create(self::MARKER_2678662),
];
}
/**
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testMultipleBodies_2678662()
*/
public function regression2802923() {
return [
'#prefix' => BigPipeMarkup::create('<p>Hi, my train will arrive at '),
'time' => [
'#lazy_builder' => [static::class . '::currentTime', []],
'#create_placeholder' => TRUE,
],
'#suffix' => BigPipeMarkup::create(' — will I still be able to catch the connection to the center?</p>'),
];
}
/**
* A page with large content.
*
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testBigPipeLargeContent
*/
public function largeContent() {
return [
'item1' => [
'#lazy_builder' => [static::class . '::largeContentBuilder', []],
'#create_placeholder' => TRUE,
],
];
}
/**
* A page with multiple nodes.
*
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testMultipleReplacements
*/
public function multipleReplacements() {
$build = [];
foreach (range(1, self::PLACEHOLDER_COUNT) as $length) {
$build[] = [
'#lazy_builder' => [static::class . '::renderRandomSentence', [$length]],
'#create_placeholder' => TRUE,
];
}
return $build;
}
/**
* Renders large content.
*
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testBigPipeLargeContent
*/
public static function largeContentBuilder() {
return [
'#theme' => 'big_pipe_test_large_content',
'#cache' => ['max-age' => 0],
];
}
/**
* Render API callback: Builds <time> markup with current time.
*
* This function is assigned as a #lazy_builder callback.
*
* @return array
* Render array with a <time> markup with current time and cache settings.
*/
public static function currentTime() {
return [
'#markup' => '<time datetime="' . date('Y-m-d', time()) . '"></time>',
'#cache' => ['max-age' => 0],
];
}
/**
* Renders a random length sentence.
*
* @param int $length
* The sentence length.
*
* @return array
* Render array.
*/
public static function renderRandomSentence(int $length): array {
return ['#cache' => ['max-age' => 0], '#markup' => (new Random())->sentences($length)];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['currentTime', 'largeContentBuilder', 'renderRandomSentence'];
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_regression_test\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for big_pipe_regression_test.
*/
class BigPipeRegressionTestHooks {
/**
* Implements hook_theme().
*
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testBigPipeLargeContent
*/
#[Hook('theme')]
public function theme() : array {
return ['big_pipe_test_large_content' => ['variables' => []]];
}
}

View File

@ -0,0 +1,6 @@
<div id="big-pipe-large-content">
{% for i in 0..130000 %}
boing
{% endfor %}
</div>

View File

@ -0,0 +1,5 @@
name: 'BigPipe test'
type: module
description: 'Support module for BigPipe testing.'
package: Testing
version: VERSION

View File

@ -0,0 +1,49 @@
big_pipe_test:
path: '/big_pipe_test'
defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::test'
_title: 'BigPipe test'
requirements:
_access: 'TRUE'
no_big_pipe:
path: '/no_big_pipe'
defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::nope'
_title: '_no_big_pipe route option test'
options:
_no_big_pipe: TRUE
requirements:
_access: 'TRUE'
big_pipe_test_multi_occurrence:
path: '/big_pipe_test_multi_occurrence'
defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::multiOccurrence'
_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'
big_pipe_test_trusted_redirect:
path: '/big_pipe_test_trusted_redirect'
defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::trustedRedirectLazyBuilder'
_title: 'BigPipe test trusted redirect'
requirements:
_access: 'TRUE'
big_pipe_test_untrusted_redirect:
path: '/big_pipe_test_untrusted_redirect'
defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::untrustedRedirectLazyBuilder'
_title: 'BigPipe test untrusted redirect'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,5 @@
services:
_defaults:
autoconfigure: true
big_pipe_test_subscriber:
class: Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber

View File

@ -0,0 +1,511 @@
<?php
declare(strict_types=1);
// cspell:ignore divpiggydiv timecurrent timetime
namespace Drupal\big_pipe_test;
use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* BigPipe placeholder test cases for use in both unit and integration tests.
*
* - Unit test:
* \Drupal\Tests\big_pipe\Unit\Render\Placeholder\BigPipeStrategyTest
* - Integration test for BigPipe with JS on:
* \Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipe()
* - Integration test for BigPipe with JS off:
* \Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipeNoJs()
*/
class BigPipePlaceholderTestCases {
/**
* Gets all BigPipe placeholder test cases.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface|null $container
* Optional. Necessary to get the embedded AJAX/HTML responses.
* @param \Drupal\Core\Session\AccountInterface|null $user
* Optional. Necessary to get the embedded AJAX/HTML responses.
*
* @return \Drupal\big_pipe_test\BigPipePlaceholderTestCase[]
* An array of placeholder test cases.
*/
public static function cases(?ContainerInterface $container = NULL, ?AccountInterface $user = NULL) {
// Define the two types of cacheability that we expect to see. These will be
// used in the expectations.
$cacheability_depends_on_session_only = [
'max-age' => 0,
'contexts' => ['session.exists'],
];
$cacheability_depends_on_session_and_nojs_cookie = [
'max-age' => 0,
'contexts' => ['session.exists', 'cookies:big_pipe_nojs'],
];
// 1. Real-world example of HTML placeholder.
$status_messages = new BigPipePlaceholderTestCase(
['#type' => 'status_messages'],
// cspell:disable-next-line
'<drupal-render-placeholder callback="Drupal\Core\Render\Element\StatusMessages::renderMessages" arguments="0" token="_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></drupal-render-placeholder>',
[
'#lazy_builder' => [
'Drupal\Core\Render\Element\StatusMessages::renderMessages',
[NULL],
],
]
);
// cspell:disable-next-line
$status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
$status_messages->bigPipePlaceholderRenderArray = [
// cspell:disable-next-line
'#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'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
// cspell:disable-next-line
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args%5B0%5D&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => TRUE,
],
],
'big_pipe_placeholders' => [
// cspell:disable-next-line
'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => $status_messages->placeholderRenderArray,
],
],
];
// cspell:disable-next-line
$status_messages->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>';
$status_messages->bigPipeNoJsPlaceholderRenderArray = [
// cspell:disable-next-line
'#markup' => '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
// cspell:disable-next-line
'<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>' => $status_messages->placeholderRenderArray,
],
],
];
if ($container && $user) {
$status_messages->embeddedAjaxResponseCommands = [
[
'command' => 'insert',
'method' => 'replaceWith',
// cspell:disable-next-line
'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args%5B0%5D&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]',
'data' => '<div data-drupal-messages>' . "\n" . ' <div role="contentinfo" aria-label="Status message">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n" . '</div>' . "\n",
'settings' => NULL,
],
];
$status_messages->embeddedHtmlResponse = '<div data-drupal-messages-fallback class="hidden"></div><div data-drupal-messages>' . "\n" . ' <div role="contentinfo" aria-label="Status message">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n" . '</div>' . "\n";
}
// 2. Real-world example of HTML attribute value placeholder: form action.
$form_action = new BigPipePlaceholderTestCase(
$container ? $container->get('form_builder')->getForm('Drupal\big_pipe_test\Form\BigPipeTestForm') : [],
'form_action_cc611e1d',
[
'#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []],
]
);
$form_action->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d';
$form_action->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => 'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d',
'#cache' => $cacheability_depends_on_session_only,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d' => $form_action->placeholderRenderArray,
],
],
];
if ($container) {
$form_action->embeddedHtmlResponse = '<form class="big-pipe-test-form" data-drupal-selector="big-pipe-test-form" action="' . base_path() . 'big_pipe_test"';
}
// 3. Real-world example of HTML attribute value subset placeholder: CSRF
// token in link.
$csrf_token = new BigPipePlaceholderTestCase(
[
'#title' => 'Link with CSRF token',
'#type' => 'link',
'#url' => Url::fromRoute('system.theme_set_default'),
],
'e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e',
[
'#lazy_builder' => [
'route_processor_csrf:renderPlaceholderCsrfToken',
['admin/config/user-interface/shortcut/manage/default/add-link-inline'],
],
]
);
$csrf_token->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e';
$csrf_token->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => 'big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e',
'#cache' => $cacheability_depends_on_session_only,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e' => $csrf_token->placeholderRenderArray,
],
],
];
if ($container) {
$csrf_token->embeddedHtmlResponse = $container->get('csrf_token')->get('admin/appearance/default');
}
// 4. Edge case: custom string to be considered as a placeholder that
// happens to not be valid HTML.
$hello = new BigPipePlaceholderTestCase(
[
'#markup' => BigPipeMarkup::create('<hello'),
'#attached' => [
'placeholders' => [
'<hello' => ['#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::helloOrHi', []]],
],
],
],
'<hello',
[
'#lazy_builder' => [
// We specifically test an invalid callback here. We need to let
// PHPStan ignore it.
// @phpstan-ignore-next-line
'hello_or_hi',
[],
],
]
);
$hello->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:&lt;hello';
$hello->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => 'big_pipe_nojs_placeholder_attribute_safe:&lt;hello',
'#cache' => $cacheability_depends_on_session_only,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'big_pipe_nojs_placeholder_attribute_safe:&lt;hello' => $hello->placeholderRenderArray,
],
],
];
$hello->embeddedHtmlResponse = '<marquee>Llamas forever!</marquee>';
// 5. Edge case: non-#lazy_builder placeholder that calls Fiber::suspend().
$piggy = new BigPipePlaceholderTestCase(
[
'#markup' => BigPipeMarkup::create('<div>piggy</div>'),
'#attached' => [
'placeholders' => [
'<div>piggy</div>' => [
'#pre_render' => [
'\Drupal\big_pipe_test\BigPipeTestController::piggy',
],
],
],
],
],
'<div>piggy</div>',
[]
);
$piggy->bigPipePlaceholderId = 'divpiggydiv';
$piggy->bigPipePlaceholderRenderArray = [
'#prefix' => '<span data-big-pipe-placeholder-id="divpiggydiv">',
'interface_preview' => [],
'#suffix' => '</span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'divpiggydiv' => TRUE,
],
],
'big_pipe_placeholders' => [
'divpiggydiv' => $piggy->placeholderRenderArray,
],
],
];
$piggy->embeddedAjaxResponseCommands = [
[
'command' => 'insert',
'method' => 'replaceWith',
'selector' => '[data-big-pipe-placeholder-id="divpiggydiv"]',
'data' => '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>',
'settings' => NULL,
],
];
$piggy->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="divpiggydiv></span>';
$piggy->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => '<span data-big-pipe-nojs-placeholder-id="divpiggydiv"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'<span data-big-pipe-nojs-placeholder-id="divpiggydiv"></span>' => $piggy->placeholderRenderArray,
],
],
];
$piggy->embeddedHtmlResponse = '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>';
// 6. Edge case: non-#lazy_builder placeholder.
$current_time = new BigPipePlaceholderTestCase(
[
'#markup' => BigPipeMarkup::create('<time>CURRENT TIME</time>'),
'#attached' => [
'placeholders' => [
'<time>CURRENT TIME</time>' => [
'#pre_render' => [
'\Drupal\big_pipe_test\BigPipeTestController::currentTime',
],
],
],
],
],
'<time>CURRENT TIME</time>',
[
// We specifically test an invalid callback here. We need to let
// PHPStan ignore it.
// @phpstan-ignore-next-line
'#pre_render' => ['current_time'],
]
);
$current_time->bigPipePlaceholderId = 'timecurrent-timetime';
$current_time->bigPipePlaceholderRenderArray = [
'#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'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'timecurrent-timetime' => TRUE,
],
],
'big_pipe_placeholders' => [
'timecurrent-timetime' => $current_time->placeholderRenderArray,
],
],
];
$current_time->embeddedAjaxResponseCommands = [
[
'command' => 'insert',
'method' => 'replaceWith',
'selector' => '[data-big-pipe-placeholder-id="timecurrent-timetime"]',
'data' => '<time datetime="1991-03-14"></time>',
'settings' => NULL,
],
];
$current_time->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>';
$current_time->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => '<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
'<span data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></span>' => $current_time->placeholderRenderArray,
],
],
];
$current_time->embeddedHtmlResponse = '<time datetime="1991-03-14"></time>';
// 7. Edge case: #lazy_builder that throws an exception.
$exception = new BigPipePlaceholderTestCase(
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
'#create_placeholder' => TRUE,
],
// cspell:disable-next-line
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::exception" arguments="0=llamas&amp;1=suck" token="uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></drupal-render-placeholder>',
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
]
);
// cspell:disable-next-line
$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 = [
// cspell:disable-next-line
'#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'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
// cspell:disable-next-line
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args%5B0%5D=llamas&args%5B1%5D=suck&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => TRUE,
],
],
'big_pipe_placeholders' => [
// cspell:disable-next-line
'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->placeholderRenderArray,
],
],
];
$exception->embeddedAjaxResponseCommands = NULL;
// cspell:disable-next-line
$exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-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>';
$exception->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => $exception->bigPipeNoJsPlaceholder,
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
$exception->bigPipeNoJsPlaceholder => $exception->placeholderRenderArray,
],
],
];
$exception->embeddedHtmlResponse = NULL;
// cSpell:disable-next-line.
$token = 'PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU';
// 8. Edge case: response filter throwing an exception for this placeholder.
$embedded_response_exception = new BigPipePlaceholderTestCase(
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
'#create_placeholder' => TRUE,
],
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::responseException" arguments token="' . $token . ' "></drupal-render-placeholder>',
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
]
);
$embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=' . $token;
$embedded_response_exception->bigPipePlaceholderRenderArray = [
// cspell:disable-next-line
'#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'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=' . $token => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=' . $token => $embedded_response_exception->placeholderRenderArray,
],
],
];
$embedded_response_exception->embeddedAjaxResponseCommands = NULL;
$embedded_response_exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=' . $token . '"></span>';
$embedded_response_exception->bigPipeNoJsPlaceholderRenderArray = [
'#markup' => $embedded_response_exception->bigPipeNoJsPlaceholder,
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'big_pipe_nojs_placeholders' => [
$embedded_response_exception->bigPipeNoJsPlaceholder => $embedded_response_exception->placeholderRenderArray,
],
],
];
$exception->embeddedHtmlResponse = NULL;
return [
'html' => $status_messages,
'html_attribute_value' => $form_action,
'html_attribute_value_subset' => $csrf_token,
'edge_case__invalid_html' => $hello,
'edge_case__html_non_lazy_builder_suspend' => $piggy,
'edge_case__html_non_lazy_builder' => $current_time,
'exception__lazy_builder' => $exception,
'exception__embedded_response' => $embedded_response_exception,
];
}
}
/**
* Provides a placeholder for the BigPipe placeholder test cases.
*/
class BigPipePlaceholderTestCase {
/**
* The original render array.
*
* @var array
*/
public $renderArray;
/**
* The expected corresponding placeholder string.
*
* @var string
*/
public $placeholder;
/**
* The expected corresponding placeholder render array.
*
* @var array
*/
public $placeholderRenderArray;
/**
* The expected BigPipe placeholder ID.
*
* (Only possible for HTML placeholders.)
*
* @var null|string
*/
public $bigPipePlaceholderId = NULL;
/**
* The corresponding expected BigPipe placeholder render array.
*
* @var null|array
*/
public $bigPipePlaceholderRenderArray = NULL;
/**
* The corresponding expected embedded AJAX response.
*
* @var null|array
*/
public $embeddedAjaxResponseCommands = NULL;
/**
* The expected BigPipe no-JS placeholder.
*
* (Possible for all placeholders, HTML or non-HTML.)
*
* @var string
*/
public $bigPipeNoJsPlaceholder;
/**
* The corresponding expected BigPipe no-JS placeholder render array.
*
* @var array
*/
public $bigPipeNoJsPlaceholderRenderArray;
/**
* The corresponding expected embedded HTML response.
*
* @var string
*/
public $embeddedHtmlResponse;
public function __construct(array $render_array, $placeholder, array $placeholder_render_array) {
$this->renderArray = $render_array;
$this->placeholder = $placeholder;
$this->placeholderRenderArray = $placeholder_render_array;
}
}

View File

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_test;
use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Security\TrustedCallbackInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Returns responses for Big Pipe routes.
*/
class BigPipeTestController implements TrustedCallbackInterface {
/**
* Returns all BigPipe placeholder test case render arrays.
*
* @return array
* Render array containing various Big Pipe placeholder test cases.
*/
public function test() {
$has_session = \Drupal::service('session_configuration')->hasSession(\Drupal::requestStack()->getMainRequest());
$build = [];
$cases = BigPipePlaceholderTestCases::cases(\Drupal::getContainer());
// 1. HTML placeholder: status messages. Drupal renders those automatically,
// so all that we need to do in this controller is set a message.
if ($has_session) {
// Only set a message if a session already exists, otherwise we always
// trigger a session, which means we can't test no-session requests.
\Drupal::messenger()->addStatus('Hello from BigPipe!');
}
$build['html'] = $cases['html']->renderArray;
// 2. HTML attribute value placeholder: form action.
$build['html_attribute_value'] = $cases['html_attribute_value']->renderArray;
// 3. HTML attribute value subset placeholder: CSRF token in link.
$build['html_attribute_value_subset'] = $cases['html_attribute_value_subset']->renderArray;
// 4. Edge case: custom string to be considered as a placeholder that
// happens to not be valid HTML.
$build['edge_case__invalid_html'] = $cases['edge_case__invalid_html']->renderArray;
// 5. Edge case: non-#lazy_builder placeholder that suspends.
$build['edge_case__html_non_lazy_builder_suspend'] = $cases['edge_case__html_non_lazy_builder_suspend']->renderArray;
// 6. Edge case: non-#lazy_builder placeholder.
$build['edge_case__html_non_lazy_builder'] = $cases['edge_case__html_non_lazy_builder']->renderArray;
// 7. Exception: #lazy_builder that throws an exception.
$build['exception__lazy_builder'] = $cases['exception__lazy_builder']->renderArray;
// 8. Exception: placeholder that causes response filter to throw exception.
$build['exception__embedded_response'] = $cases['exception__embedded_response']->renderArray;
return $build;
}
/**
* @return array
* List of all BigPipe placeholder test cases.
*/
public static function nope() {
return ['#markup' => '<p>Nope.</p>'];
}
/**
* A page with multiple occurrences of the same placeholder.
*
* @see \Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipeMultiOccurrencePlaceholders()
*
* @return array
* Render array with multiple placeholders using a lazy builder.
*/
public function multiOccurrence() {
return [
'item1' => [
'#lazy_builder' => [static::class . '::counter', []],
'#create_placeholder' => TRUE,
],
'item2' => [
'#lazy_builder' => [static::class . '::counter', []],
'#create_placeholder' => TRUE,
],
'item3' => [
'#lazy_builder' => [static::class . '::counter', []],
'#create_placeholder' => TRUE,
],
];
}
/**
* A page with placeholder preview.
*
* @return array[]
* A render array with two containers:
* - 'user_container': Loads the users display name via a lazy builder.
* - 'user_links_container': Loads user links with a placeholder preview.
*/
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 . '::helloOrHi', []],
'#create_placeholder' => TRUE,
'#lazy_builder_preview' => [
'#attributes' => ['id' => 'render-array-preview'],
'#type' => 'container',
'#markup' => 'There is a lamb and there is a puppy',
],
],
],
];
}
/**
* Render API callback: Builds <time> markup with current time.
*
* This function is assigned as a #lazy_builder callback.
*
* Note: does not actually use current time, that would complicate testing.
*
* @return array
* A render array containing a <time> element with a predefined date
* and disabled caching for dynamic rendering.
*/
public static function currentTime() {
return [
'#markup' => '<time datetime="' . date('Y-m-d', 668948400) . '"></time>',
'#cache' => ['max-age' => 0],
];
}
/**
* Render API callback: Suspends its own execution then returns markup.
*
* This function is assigned as a #lazy_builder callback.
*
* @return array
* A render array with a pig-themed message wrapped in a <span>,
* and caching disabled to ensure dynamic rendering.
*/
public static function piggy(): array {
// Immediately call Fiber::suspend(), so that other placeholders are
// executed next. When this is resumed, it will immediately return the
// render array.
if (\Fiber::getCurrent() !== NULL) {
\Fiber::suspend();
}
return [
'#markup' => '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>',
'#cache' => ['max-age' => 0],
];
}
/**
* Render API callback: Says "hello" or "hi".
*
* This function is assigned as a #lazy_builder callback.
*
* @return array
* A render array with a marquee message using BigPipeMarkup,
* with caching disabled and a custom cache tag.
*/
public static function helloOrHi() {
return [
'#markup' => BigPipeMarkup::create('<marquee>llamas forever!</marquee>'),
'#cache' => [
'max-age' => 0,
'tags' => ['cache_tag_set_in_lazy_builder'],
],
];
}
/**
* The #lazy_builder callback; throws exception.
*
* @throws \Exception
*/
public static function exception() {
throw new \Exception('You are not allowed to say llamas are not cool!');
}
/**
* The #lazy_builder callback; returns content that will trigger an exception.
*
* @see \Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber::onRespondTriggerException()
*
* @return array
* A render array with plain text for testing BigPipe error handling.
*/
public static function responseException() {
return ['#plain_text' => BigPipeTestSubscriber::CONTENT_TRIGGER_EXCEPTION];
}
/**
* The #lazy_builder callback; returns the current count.
*
* @see \Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipeMultiOccurrencePlaceholders()
*
* @return array
* The render array.
*/
public static function counter() {
// Lazy builders are not allowed to build their own state like this function
// does, but in this case we're intentionally doing that for testing
// purposes: so we can ensure that each lazy builder is only ever called
// once with the same parameters.
static $count;
if (!isset($count)) {
$count = 0;
}
$count++;
return [
'#markup' => BigPipeMarkup::create("<p>The count is $count.</p>"),
'#cache' => ['max-age' => 0],
];
}
/**
* Route callback to test a trusted lazy builder redirect response.
*
* @return array
* The lazy builder callback.
*/
public function trustedRedirectLazyBuilder(): array {
return [
'redirect' => [
'#lazy_builder' => [static::class . '::redirectTrusted', []],
'#create_placeholder' => TRUE,
],
];
}
/**
* Supports Big Pipe testing of the enforced redirect response.
*
* @throws \Drupal\Core\Form\EnforcedResponseException
* Trigger catch of Big Pipe enforced redirect response exception.
*/
public static function redirectTrusted(): void {
$response = new RedirectResponse('/big_pipe_test');
throw new EnforcedResponseException($response);
}
/**
* Route callback to test an untrusted lazy builder redirect response.
*
* @return array
* The lazy builder callback.
*/
public function untrustedRedirectLazyBuilder(): array {
return [
'redirect' => [
'#lazy_builder' => [static::class . '::redirectUntrusted', []],
'#create_placeholder' => TRUE,
],
];
}
/**
* Supports Big Pipe testing of an untrusted external URL.
*
* @throws \Drupal\Core\Form\EnforcedResponseException
* Trigger catch of Big Pipe enforced redirect response exception.
*/
public static function redirectUntrusted(): void {
$response = new RedirectResponse('https://example.com');
throw new EnforcedResponseException($response);
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return [
'currentTime',
'piggy',
'helloOrHi',
'exception',
'responseException',
'counter',
'redirectTrusted',
'redirectUntrusted',
];
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_test\EventSubscriber;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\HtmlResponse;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines a test BigPipe subscriber that checks whether the session is empty.
*/
class BigPipeTestSubscriber implements EventSubscriberInterface {
/**
* @see \Drupal\big_pipe_test\BigPipeTestController::responseException()
*
* @var string
*/
const CONTENT_TRIGGER_EXCEPTION = 'NOPE!NOPE!NOPE!';
/**
* Triggers exception for embedded HTML/AJAX responses with certain content.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The event to process.
*
* @throws \Exception
*
* @see \Drupal\big_pipe_test\BigPipeTestController::responseException()
*/
public function onRespondTriggerException(ResponseEvent $event) {
$response = $event->getResponse();
if (!$response instanceof AttachmentsInterface) {
return;
}
$attachments = $response->getAttachments();
if (!isset($attachments['big_pipe_placeholders']) && !isset($attachments['big_pipe_nojs_placeholders'])) {
if (str_contains($response->getContent(), static::CONTENT_TRIGGER_EXCEPTION)) {
throw new \Exception('Oh noes!');
}
}
}
/**
* Exposes all BigPipe placeholders (JS and no-JS) via headers for testing.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The event to process.
*/
public function onRespondSetBigPipeDebugPlaceholderHeaders(ResponseEvent $event) {
$response = $event->getResponse();
if (!$response instanceof HtmlResponse) {
return;
}
$attachments = $response->getAttachments();
$response->headers->set('BigPipe-Test-Placeholders', '<none>');
$response->headers->set('BigPipe-Test-No-Js-Placeholders', '<none>');
if (!empty($attachments['big_pipe_placeholders'])) {
$response->headers->set('BigPipe-Test-Placeholders', implode(' ', array_keys($attachments['big_pipe_placeholders'])));
}
if (!empty($attachments['big_pipe_nojs_placeholders'])) {
$response->headers->set('BigPipe-Test-No-Js-Placeholders', implode(' ', array_map('rawurlencode', array_keys($attachments['big_pipe_nojs_placeholders']))));
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Run just before \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber::onRespond().
$events[KernelEvents::RESPONSE][] = ['onRespondSetBigPipeDebugPlaceholderHeaders', -9999];
// Run just after \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber::onRespond().
$events[KernelEvents::RESPONSE][] = ['onRespondTriggerException', -10001];
return $events;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form to test BigPipe.
*
* @internal
*/
class BigPipeTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'big_pipe_test_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#token'] = FALSE;
$form['big_pipe'] = [
'#type' => 'checkboxes',
'#title' => $this->t('BigPipe works…'),
'#options' => [
'js' => $this->t('… with JavaScript'),
'nojs' => $this->t('… without JavaScript'),
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\big_pipe_test\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for big_pipe_test.
*/
class BigPipeTestHooks {
/**
* Implements hook_page_top().
*/
#[Hook('page_top')]
public function pageTop(array &$page_top): void {
// Ensure this hook is invoked on every page load.
$page_top['#cache']['max-age'] = 0;
$request = \Drupal::request();
if ($request->query->get('trigger_session')) {
$request->getSession()->set('big_pipe_test', TRUE);
}
}
}

View File

@ -0,0 +1,580 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Functional;
use Behat\Mink\Element\NodeElement;
use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe_test\BigPipePlaceholderTestCases;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Database\Database;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests BigPipe's no-JS detection & response delivery (with and without JS).
*
* Covers:
* - big_pipe_page_attachments()
* - \Drupal\big_pipe\Controller\BigPipeController
* - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
* - \Drupal\big_pipe\Render\BigPipe
*
* @group big_pipe
*/
class BigPipeTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['big_pipe', 'big_pipe_messages_test', 'big_pipe_test', 'dblog'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ignore the <meta> refresh that big_pipe.module sets. It causes a redirect
// to a page that sets another cookie, which causes BrowserTestBase to lose
// the session cookie. To avoid this problem, tests should first call
// drupalGet() and then call checkForMetaRefresh() manually, and then reset
// $this->maximumMetaRefreshCount and $this->metaRefreshCount.
// @see doMetaRefresh()
$this->maximumMetaRefreshCount = 0;
}
/**
* Performs a single <meta> refresh explicitly.
*
* This test disables the automatic <meta> refresh checking, each time it is
* desired that this runs, a test case must explicitly call this.
*
* @see setUp()
*/
protected function performMetaRefresh(): void {
$this->maximumMetaRefreshCount = 1;
$this->checkForMetaRefresh();
$this->maximumMetaRefreshCount = 0;
$this->metaRefreshCount = 0;
}
/**
* Tests BigPipe's no-JS detection.
*
* Covers:
* - big_pipe_page_attachments()
* - \Drupal\big_pipe\Controller\BigPipeController
*/
public function testNoJsDetection(): void {
$no_js_to_js_markup = '<script>document.cookie = "' . BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"</script>';
// 1. No session (anonymous).
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSessionCookieExists('0');
$this->assertBigPipeNoJsCookieExists('0');
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertSession()->responseNotContains($no_js_to_js_markup);
// 2. Session (authenticated).
$this->drupalLogin($this->rootUser);
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
$this->assertSession()->responseContains('<noscript><meta http-equiv="Refresh" content="0; URL=' . base_path() . 'big_pipe/no-js?destination=' . UrlHelper::encodePath(base_path() . 'big_pipe_test') . '" />' . "\n" . '</noscript>');
$this->assertSession()->responseNotContains($no_js_to_js_markup);
$this->assertBigPipeNoJsMetaRefreshRedirect();
$this->assertBigPipeNoJsCookieExists('1');
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertSession()->responseContains($no_js_to_js_markup);
$this->drupalLogout();
// Close the prior connection and remove the collected state.
$this->getSession()->reset();
// 3. Session (anonymous).
$this->drupalGet(Url::fromRoute('user.login', [], ['query' => ['trigger_session' => 1]]));
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
$this->assertSession()->responseContains('<noscript><meta http-equiv="Refresh" content="0; URL=' . base_path() . 'big_pipe/no-js?destination=' . base_path() . 'big_pipe_test" />' . "\n" . '</noscript>');
$this->assertSession()->responseNotContains($no_js_to_js_markup);
$this->assertBigPipeNoJsMetaRefreshRedirect();
$this->assertBigPipeNoJsCookieExists('1');
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertSession()->responseContains($no_js_to_js_markup);
// Close the prior connection and remove the collected state.
$this->getSession()->reset();
// Edge case: route with '_no_big_pipe' option.
$this->drupalGet(Url::fromRoute('no_big_pipe'));
$this->assertSessionCookieExists('0');
$this->assertBigPipeNoJsCookieExists('0');
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertSession()->responseNotContains($no_js_to_js_markup);
$this->drupalLogin($this->rootUser);
$this->drupalGet(Url::fromRoute('no_big_pipe'));
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertSession()->responseNotContains($no_js_to_js_markup);
}
/**
* Tests BigPipe-delivered HTML responses when JavaScript is enabled.
*
* Covers:
* - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
* - \Drupal\big_pipe\Render\BigPipe
* - \Drupal\big_pipe\Render\BigPipe::sendPlaceholders()
*
* @see \Drupal\big_pipe_test\BigPipePlaceholderTestCases
*/
public function testBigPipe(): void {
// Simulate production.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
$this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
$connection = Database::getConnection();
$log_count = $connection->select('watchdog')->countQuery()->execute()->fetchField();
// By not calling performMetaRefresh() here, we simulate JavaScript being
// enabled, because as far as the BigPipe module is concerned, JavaScript is
// enabled in the browser as long as the BigPipe no-JS cookie is *not* set.
// @see setUp()
// @see performMetaRefresh()
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertBigPipeResponseHeadersPresent();
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'cache_tag_set_in_lazy_builder');
$this->setCsrfTokenSeedInTestEnvironment();
$cases = $this->getTestCases();
$this->assertBigPipeNoJsPlaceholders([
$cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
$cases['html_attribute_value']->bigPipeNoJsPlaceholder => $cases['html_attribute_value']->embeddedHtmlResponse,
$cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse,
]);
$this->assertBigPipePlaceholders([
$cases['html']->bigPipePlaceholderId => Json::encode($cases['html']->embeddedAjaxResponseCommands),
$cases['edge_case__html_non_lazy_builder_suspend']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder_suspend']->embeddedAjaxResponseCommands),
$cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder']->embeddedAjaxResponseCommands),
$cases['exception__lazy_builder']->bigPipePlaceholderId => NULL,
$cases['exception__embedded_response']->bigPipePlaceholderId => NULL,
], [
0 => $cases['html']->bigPipePlaceholderId,
1 => $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId,
// The suspended placeholder is replaced after the non-suspended
// placeholder even though it appears first in the page.
// @see Drupal\big_pipe\Render\BigPipe\Render::sendPlaceholders()
2 => $cases['edge_case__html_non_lazy_builder_suspend']->bigPipePlaceholderId,
]);
$this->assertSession()->responseContains('</body>');
// Verifying BigPipe assets are present.
$this->assertNotEmpty($this->getDrupalSettings());
$this->assertContains('big_pipe/big_pipe', explode(',', $this->getDrupalSettings()['ajaxPageState']['libraries']), 'BigPipe asset library is present.');
// Verify that the two expected exceptions are logged as errors.
$this->assertEquals($log_count + 2, (int) $connection->select('watchdog')->countQuery()->execute()->fetchField(), 'Two new watchdog entries.');
// Using dynamic select queries with the method range() allows contrib
// database drivers the ability to insert their own limit and offset
// functionality.
$records = $connection->select('watchdog', 'w')->fields('w')->orderBy('wid', 'DESC')->range(0, 2)->execute()->fetchAll();
$this->assertEquals(RfcLogLevel::WARNING, $records[0]->severity);
$this->assertStringContainsString('Oh noes!', (string) unserialize($records[0]->variables)['@message']);
$this->assertEquals(RfcLogLevel::WARNING, $records[1]->severity);
$this->assertStringContainsString('You are not allowed to say llamas are not cool!', (string) unserialize($records[1]->variables)['@message']);
// Verify that 4xx responses work fine. (4xx responses are handled by
// subrequests to a route pointing to a controller with the desired output.)
$this->drupalGet(Url::fromUri('base:non-existing-path'));
// Simulate development.
// Verifying BigPipe provides useful error output when an error occurs
// while rendering a placeholder if verbose error logging is enabled.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save();
$this->drupalGet(Url::fromRoute('big_pipe_test'));
// The 'edge_case__html_exception' case throws an exception.
$this->assertSession()->pageTextContains('The website encountered an unexpected error. Try again later');
$this->assertSession()->pageTextContains('You are not allowed to say llamas are not cool!');
// Check that stop signal and closing body tag are absent.
$this->assertSession()->responseNotContains(BigPipe::STOP_SIGNAL);
$this->assertSession()->responseNotContains('</body>');
// The exception is expected. Do not interpret it as a test failure.
unlink($this->root . '/' . $this->siteDirectory . '/error.log');
// Tests the enforced redirect response exception handles redirecting to
// a trusted redirect.
$this->drupalGet(Url::fromRoute('big_pipe_test_trusted_redirect'));
$this->assertSession()->responseContains('application/vnd.drupal-ajax');
$this->assertSession()->responseContains('[{"command":"redirect","url":"\/big_pipe_test"}]');
// Test that it rejects an untrusted redirect.
$this->drupalGet(Url::fromRoute('big_pipe_test_untrusted_redirect'));
$this->assertSession()->responseContains('Redirects to external URLs are not allowed by default');
}
/**
* Tests BigPipe-delivered HTML responses when JavaScript is disabled.
*
* Covers:
* - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
* - \Drupal\big_pipe\Render\BigPipe
* - \Drupal\big_pipe\Render\BigPipe::sendNoJsPlaceholders()
*
* @see \Drupal\big_pipe_test\BigPipePlaceholderTestCases
*/
public function testBigPipeNoJs(): void {
// Simulate production.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
$this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
// By calling performMetaRefresh() here, we simulate JavaScript being
// disabled, because as far as the BigPipe module is concerned, it is
// enabled in the browser when the BigPipe no-JS cookie is set.
// @see setUp()
// @see performMetaRefresh()
$this->performMetaRefresh();
$this->assertBigPipeNoJsCookieExists('1');
$this->drupalGet(Url::fromRoute('big_pipe_test'));
$this->assertBigPipeResponseHeadersPresent();
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'cache_tag_set_in_lazy_builder');
$this->setCsrfTokenSeedInTestEnvironment();
$cases = $this->getTestCases();
$this->assertBigPipeNoJsPlaceholders([
$cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
$cases['html_attribute_value']->bigPipeNoJsPlaceholder => $cases['html_attribute_value']->embeddedHtmlResponse,
$cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse,
$cases['html']->bigPipeNoJsPlaceholder => $cases['html']->embeddedHtmlResponse,
$cases['edge_case__html_non_lazy_builder']->bigPipeNoJsPlaceholder => $cases['edge_case__html_non_lazy_builder']->embeddedHtmlResponse,
$cases['exception__lazy_builder']->bigPipePlaceholderId => NULL,
$cases['exception__embedded_response']->bigPipePlaceholderId => NULL,
]);
// Verifying there are no BigPipe placeholders & replacements.
$this->assertSession()->responseHeaderEquals('BigPipe-Test-Placeholders', '<none>');
// Verifying BigPipe start/stop signals are absent.
$this->assertSession()->responseNotContains(BigPipe::START_SIGNAL);
$this->assertSession()->responseNotContains(BigPipe::STOP_SIGNAL);
// Verifying BigPipe assets are absent.
$this->assertArrayNotHasKey('bigPipePlaceholderIds', $this->getDrupalSettings());
$this->assertArrayNotHasKey('ajaxPageState', $this->getDrupalSettings());
$this->assertSession()->responseContains('</body>');
// Verify that 4xx responses work fine. (4xx responses are handled by
// subrequests to a route pointing to a controller with the desired output.)
$this->drupalGet(Url::fromUri('base:non-existing-path'));
// Simulate development.
// Verifying BigPipe provides useful error output when an error occurs
// while rendering a placeholder if verbose error logging is enabled.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save();
$this->drupalGet(Url::fromRoute('big_pipe_test'));
// The 'edge_case__html_exception' case throws an exception.
$this->assertSession()->pageTextContains('The website encountered an unexpected error. Try again later');
$this->assertSession()->pageTextContains('You are not allowed to say llamas are not cool!');
$this->assertSession()->responseNotContains('</body>');
// The exception is expected. Do not interpret it as a test failure.
unlink($this->root . '/' . $this->siteDirectory . '/error.log');
}
/**
* Tests BigPipe with a multi-occurrence placeholder.
*/
public function testBigPipeMultiOccurrencePlaceholders(): void {
$this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
// By not calling performMetaRefresh() here, we simulate JavaScript being
// enabled, because as far as the BigPipe module is concerned, JavaScript is
// enabled in the browser as long as the BigPipe no-JS cookie is *not* set.
// @see setUp()
// @see performMetaRefresh()
$this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
$this->assertSession()->pageTextContains('The count is 1.');
$this->assertSession()->responseNotContains('The count is 2.');
$this->assertSession()->responseNotContains('The count is 3.');
// By calling performMetaRefresh() here, we simulate JavaScript being
// disabled, because as far as the BigPipe module is concerned, it is
// enabled in the browser when the BigPipe no-JS cookie is set.
// @see setUp()
// @see performMetaRefresh()
$this->performMetaRefresh();
$this->assertBigPipeNoJsCookieExists('1');
$this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
$this->assertSession()->pageTextContains('The count is 1.');
$this->assertSession()->responseNotContains('The count is 2.');
$this->assertSession()->responseNotContains('The count is 3.');
}
/**
* @internal
*/
protected function assertBigPipeResponseHeadersPresent(): void {
// Check that Cache-Control header set to "private".
$this->assertSession()->responseHeaderContains('Cache-Control', 'private');
$this->assertSession()->responseHeaderEquals('Surrogate-Control', 'no-store, content="BigPipe/1.0"');
$this->assertSession()->responseHeaderEquals('X-Accel-Buffering', 'no');
}
/**
* Asserts expected BigPipe no-JS placeholders are present and replaced.
*
* @param array $expected_big_pipe_nojs_placeholders
* Keys: BigPipe no-JS placeholder markup. Values: expected replacement
* markup.
*
* @internal
*/
protected function assertBigPipeNoJsPlaceholders(array $expected_big_pipe_nojs_placeholders): void {
$this->assertSetsEqual(array_keys($expected_big_pipe_nojs_placeholders), array_map('rawurldecode', explode(' ', $this->getSession()->getResponseHeader('BigPipe-Test-No-Js-Placeholders'))));
foreach ($expected_big_pipe_nojs_placeholders as $big_pipe_nojs_placeholder => $expected_replacement) {
// Checking whether the replacement for the BigPipe no-JS placeholder
// $big_pipe_nojs_placeholder is present.
$this->assertSession()->responseNotContains($big_pipe_nojs_placeholder);
if ($expected_replacement !== NULL) {
$this->assertSession()->responseContains($expected_replacement);
}
}
}
/**
* Asserts expected BigPipe placeholders are present and replaced.
*
* @param array $expected_big_pipe_placeholders
* Keys: BigPipe placeholder IDs. Values: expected AJAX response.
* @param array $expected_big_pipe_placeholder_stream_order
* Keys: BigPipe placeholder IDs. Values: expected AJAX response. Keys are
* defined in the order that they are expected to be rendered & streamed.
*
* @internal
*/
protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholders, array $expected_big_pipe_placeholder_stream_order): void {
$this->assertSetsEqual(array_keys($expected_big_pipe_placeholders), explode(' ', $this->getSession()->getResponseHeader('BigPipe-Test-Placeholders')));
$placeholder_positions = [];
$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 . '">';
$this->assertSession()->responseContains($expected_placeholder_html);
$pos = strpos($this->getSession()->getPage()->getContent(), $expected_placeholder_html);
$placeholder_positions[$pos] = $big_pipe_placeholder_id;
// Verify expected placeholder replacement.
$expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">';
$xpath = '//script[@data-big-pipe-replacement-for-placeholder-with-id="' . Html::decodeEntities($big_pipe_placeholder_id) . '"]';
if ($expected_ajax_response === NULL) {
$this->assertSession()->elementNotExists('xpath', $xpath);
$this->assertSession()->responseNotContains($expected_placeholder_replacement);
continue;
}
$this->assertSession()->elementTextContains('xpath', $xpath, $expected_ajax_response);
$this->assertSession()->responseContains($expected_placeholder_replacement);
$pos = strpos($this->getSession()->getPage()->getContent(), $expected_placeholder_replacement);
$placeholder_replacement_positions[$pos] = $big_pipe_placeholder_id;
}
ksort($placeholder_positions, SORT_NUMERIC);
$this->assertEquals(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions));
$placeholders = array_map(function (NodeElement $element) {
return $element->getAttribute('data-big-pipe-placeholder-id');
}, $this->cssSelect('[data-big-pipe-placeholder-id]'));
$this->assertSameSize($expected_big_pipe_placeholders, array_unique($placeholders));
$expected_big_pipe_placeholders_with_replacements = [];
foreach ($expected_big_pipe_placeholder_stream_order as $big_pipe_placeholder_id) {
$expected_big_pipe_placeholders_with_replacements[$big_pipe_placeholder_id] = $expected_big_pipe_placeholders[$big_pipe_placeholder_id];
}
$this->assertEquals($expected_big_pipe_placeholders_with_replacements, array_filter($expected_big_pipe_placeholders));
$this->assertSetsEqual(array_keys($expected_big_pipe_placeholders_with_replacements), array_values($placeholder_replacement_positions));
$this->assertSame(count($expected_big_pipe_placeholders_with_replacements), preg_match_all('/' . preg_quote('<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="', '/') . '/', $this->getSession()->getPage()->getContent()));
// Verifying BigPipe start/stop signals.
$this->assertSession()->responseContains(BigPipe::START_SIGNAL);
$this->assertSession()->responseContains(BigPipe::STOP_SIGNAL);
$start_signal_position = strpos($this->getSession()->getPage()->getContent(), BigPipe::START_SIGNAL);
$stop_signal_position = strpos($this->getSession()->getPage()->getContent(), BigPipe::STOP_SIGNAL);
$this->assertTrue($start_signal_position < $stop_signal_position, 'BigPipe start signal appears before stop signal.');
// Verifying BigPipe placeholder replacements and start/stop signals were
// streamed in the correct order.
$expected_stream_order = array_keys($expected_big_pipe_placeholders_with_replacements);
array_unshift($expected_stream_order, BigPipe::START_SIGNAL);
array_push($expected_stream_order, BigPipe::STOP_SIGNAL);
$actual_stream_order = $placeholder_replacement_positions + [
$start_signal_position => BigPipe::START_SIGNAL,
$stop_signal_position => BigPipe::STOP_SIGNAL,
];
ksort($actual_stream_order, SORT_NUMERIC);
$this->assertEquals($expected_stream_order, array_values($actual_stream_order));
}
/**
* Ensures CSRF tokens can be generated for the current user's session.
*/
protected function setCsrfTokenSeedInTestEnvironment(): void {
// Retrieve the CSRF token from the child site from its serialized session
// record in the database.
$session_data = $this->container->get('session_handler.write_safe')->read($this->getSession()->getCookie($this->getSessionName()));
$csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s'];
// Ensure that the session is started before accessing a session bag.
// Otherwise the value stored in the bag is lost when subsequent session
// access triggers a session start automatically.
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request_stack = $this->container->get('request_stack');
$session = $request_stack->getSession();
if (!$session->isStarted()) {
$session->start();
}
// Store the CSRF token in the test runners session metadata bag.
$this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed);
}
/**
* @return \Drupal\big_pipe_test\BigPipePlaceholderTestCase[]
* An array of test cases.
*/
protected function getTestCases($has_session = TRUE) {
return BigPipePlaceholderTestCases::cases($this->container, $this->rootUser);
}
/**
* Asserts whether arrays A and B are equal, when treated as sets.
*
* @todo This method is broken. Fix it in
* https://www.drupal.org/project/drupal/issues/3144926
*
* @internal
*/
protected function assertSetsEqual(array $a, array $b): void {
count($a) == count($b) && !array_diff_assoc($a, $b);
}
/**
* Asserts whether a BigPipe no-JS cookie exists or not.
*
* @internal
*/
protected function assertBigPipeNoJsCookieExists(string $expected): void {
$this->assertCookieExists('big_pipe_nojs', $expected, 'BigPipe no-JS');
}
/**
* Asserts whether a session cookie exists or not.
*
* @internal
*/
protected function assertSessionCookieExists(string $expected): void {
$this->assertCookieExists($this->getSessionName(), $expected, 'Session');
}
/**
* Asserts whether a cookie exists on the client or not.
*
* @internal
*/
protected function assertCookieExists(string $cookie_name, string $expected, string $cookie_label): void {
$this->assertEquals($expected, !empty($this->getSession()->getCookie($cookie_name)), $expected ? "$cookie_label cookie exists." : "$cookie_label cookie does not exist.");
}
/**
* Calls ::performMetaRefresh() and asserts the responses.
*
* @internal
*/
protected function assertBigPipeNoJsMetaRefreshRedirect(): void {
$original_url = $this->getSession()->getCurrentUrl();
// Disable automatic following of redirects by the HTTP client, so that this
// test can analyze the response headers of each redirect response.
$this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
$this->performMetaRefresh();
$headers[0] = $this->getSession()->getResponseHeaders();
$statuses[0] = $this->getSession()->getStatusCode();
$this->performMetaRefresh();
$headers[1] = $this->getSession()->getResponseHeaders();
$statuses[1] = $this->getSession()->getStatusCode();
$this->getSession()->getDriver()->getClient()->followRedirects(TRUE);
$this->assertEquals($original_url, $this->getSession()->getCurrentUrl(), 'Redirected back to the original location.');
// First response: redirect.
$this->assertEquals(302, $statuses[0], 'The first response was a 302 (redirect).');
$this->assertStringStartsWith('big_pipe_nojs=1', $headers[0]['Set-Cookie'][0], 'The first response sets the big_pipe_nojs cookie.');
$this->assertEquals($original_url, $headers[0]['Location'][0], 'The first response redirected back to the original page.');
$this->assertEmpty(
array_diff([
'cookies:big_pipe_nojs',
'session.exists',
], explode(' ', $headers[0]['X-Drupal-Cache-Contexts'][0])),
'The first response varies by the "cookies:big_pipe_nojs" and "session.exists" cache contexts.'
);
$this->assertFalse(isset($headers[0]['Surrogate-Control']), 'The first response has no "Surrogate-Control" header.');
// Second response: redirect followed.
$this->assertEquals(200, $statuses[1], 'The second response was a 200.');
$this->assertEmpty(
array_diff([
'cookies:big_pipe_nojs',
'session.exists',
], explode(' ', $headers[0]['X-Drupal-Cache-Contexts'][0])),
'The second response varies by the "cookies:big_pipe_nojs" and "session.exists" cache contexts.'
);
$this->assertEquals('no-store, content="BigPipe/1.0"', $headers[1]['Surrogate-Control'][0], 'The second response has a "Surrogate-Control" header.');
// Check that the <meta> refresh is absent, only one redirect ever happens.
$this->assertSession()->responseNotContains('<noscript><meta http-equiv="Refresh" content="0; URL=');
}
/**
* Tests that response contains cacheability debug comments.
*/
public function testDebugCacheability(): void {
$this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists('1');
$this->assertBigPipeNoJsCookieExists('0');
// With debug_cacheability_headers enabled.
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertBigPipeResponseHeadersPresent();
$this->assertSession()->responseContains('<!-- big_pipe cache tags: -->');
$this->assertSession()
->responseContains('<!-- big_pipe cache contexts: languages:language_interface theme user.permissions -->');
// With debug_cacheability_headers disabled.
$this->setContainerParameter('http.response.debug_cacheability_headers', FALSE);
$this->rebuildContainer();
$this->resetAll();
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSession()->responseNotContains('<!-- big_pipe cache tags:');
$this->assertSession()
->responseNotContains('<!-- big_pipe cache contexts:');
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for big_pipe.
*
* @group big_pipe
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
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(): void {
$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', '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', 'Llamas forever!');
$this->assertSession()->pageTextNotContains('There is a lamb and there is a puppy');
}
}

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\FunctionalJavascript;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe_regression_test\BigPipeRegressionTestController;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* BigPipe regression tests.
*
* @group big_pipe
* @group #slow
*/
class BigPipeRegressionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'big_pipe',
'big_pipe_messages_test',
'big_pipe_regression_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Use the big_pipe_test_theme theme.
$this->container->get('theme_installer')->install(['big_pipe_test_theme']);
$this->container->get('config.factory')->getEditable('system.theme')->set('default', 'big_pipe_test_theme')->save();
}
/**
* Ensure BigPipe works despite inline JS containing the string "</body>".
*
* @see https://www.drupal.org/node/2678662
*/
public function testMultipleClosingBodies_2678662(): void {
$this->assertTrue($this->container->get('module_installer')->install(['render_placeholder_message_test'], TRUE), 'Installed modules.');
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet(Url::fromRoute('big_pipe_regression_test.2678662'));
// Confirm that AJAX behaviors were instantiated, if not, this points to a
// JavaScript syntax error and the JS variable has the appropriate content.
$javascript = <<<JS
(function(){
return Object.keys(Drupal.ajax.instances).length > 0 && hitsTheFloor === "</body>";
}())
JS;
$this->assertJsCondition($javascript);
// Besides verifying there is no JavaScript syntax error, also verify the
// HTML structure.
// The BigPipe stop signal is present just before the closing </body> and
// </html> tags.
$this->assertSession()
->responseContains(BigPipe::STOP_SIGNAL . "\n\n\n</body></html>");
$js_code_until_closing_body_tag = substr(BigPipeRegressionTestController::MARKER_2678662, 0, strpos(BigPipeRegressionTestController::MARKER_2678662, '</body>'));
// The BigPipe start signal does NOT start at the closing </body> tag string
// in an inline script.
$this->assertSession()
->responseNotContains($js_code_until_closing_body_tag . "\n" . BigPipe::START_SIGNAL);
// But the inline script itself should not be altered.
$this->assertSession()
->responseContains(BigPipeRegressionTestController::MARKER_2678662);
}
/**
* Ensure messages set in placeholders always appear.
*
* @see https://www.drupal.org/node/2712935
*/
public function testMessages_2712935(): void {
$this->assertTrue($this->container->get('module_installer')->install(['render_placeholder_message_test'], TRUE), 'Installed modules.');
$this->drupalLogin($this->drupalCreateUser());
$messages_markup = '<div class="messages messages--status" role="status"';
$test_routes = [
// Messages placeholder rendered first.
'render_placeholder_message_test.first',
// Messages placeholder rendered after one, before another.
'render_placeholder_message_test.middle',
// Messages placeholder rendered last.
'render_placeholder_message_test.last',
];
$assert = $this->assertSession();
foreach ($test_routes as $route) {
// Verify that we start off with zero messages queued.
$this->drupalGet(Url::fromRoute('render_placeholder_message_test.queued'));
$assert->responseNotContains($messages_markup);
// Verify the test case at this route behaves as expected.
$this->drupalGet(Url::fromRoute($route));
$assert->elementContains('css', 'p.logged-message:nth-of-type(1)', 'Message: P1');
$assert->elementContains('css', 'p.logged-message:nth-of-type(2)', 'Message: P2');
$assert->responseContains($messages_markup);
$assert->elementExists('css', 'div[aria-label="Status message"]');
$assert->responseContains('aria-label="Status message">P1');
$assert->responseContains('aria-label="Status message">P2');
// Verify that we end with all messages printed, hence again zero queued.
$this->drupalGet(Url::fromRoute('render_placeholder_message_test.queued'));
$assert->responseNotContains($messages_markup);
}
}
/**
* Tests edge cases with placeholder HTML.
*/
public function testPlaceholderHtmlEdgeCases(): void {
$this->drupalLogin($this->drupalCreateUser());
$this->doTestPlaceholderInParagraph_2802923();
$this->doTestBigPipeLargeContent();
$this->doTestMultipleReplacements();
}
/**
* Ensure default BigPipe placeholder HTML cannot split paragraphs.
*
* @see https://www.drupal.org/node/2802923
*/
protected function doTestPlaceholderInParagraph_2802923(): void {
$this->drupalGet(Url::fromRoute('big_pipe_regression_test.2802923'));
$this->assertJsCondition('document.querySelectorAll(\'p\').length === 1');
}
/**
* Tests BigPipe large content.
*
* Repeat loading of same page for two times, after second time the page is
* cached and the bug consistently reproducible.
*/
public function doTestBigPipeLargeContent(): void {
$assert_session = $this->assertSession();
$this->drupalGet(Url::fromRoute('big_pipe_test_large_content'));
$this->assertNotNull($assert_session->waitForElement('css', 'script[data-big-pipe-event="stop"]'));
$this->assertCount(0, $this->getDrupalSettings()['bigPipePlaceholderIds']);
$this->assertCount(2, $this->getSession()->getPage()->findAll('css', 'script[data-big-pipe-replacement-for-placeholder-with-id]'));
$assert_session->elementExists('css', '#big-pipe-large-content');
$this->drupalGet(Url::fromRoute('big_pipe_test_large_content'));
$this->assertNotNull($assert_session->waitForElement('css', 'script[data-big-pipe-event="stop"]'));
$this->assertCount(0, $this->getDrupalSettings()['bigPipePlaceholderIds']);
$this->assertCount(2, $this->getSession()->getPage()->findAll('css', 'script[data-big-pipe-replacement-for-placeholder-with-id]'));
$assert_session->elementExists('css', '#big-pipe-large-content');
}
/**
* Test BigPipe replacement of multiple complex replacements.
*
* In some situations with either a large number of replacements or multiple
* replacements involving complex operations, some replacements were not
* completed. This is a simulation of such a situation by rendering a lot of
* placeholders on a page.
*
* @see https://www.drupal.org/node/3390178
*/
protected function doTestMultipleReplacements(): void {
$user = $this->drupalCreateUser();
$this->drupalLogin($user);
$assert_session = $this->assertSession();
$this->drupalGet(Url::fromRoute('big_pipe_test_multiple_replacements'));
$this->assertNotNull($assert_session->waitForElement('css', 'script[data-big-pipe-event="stop"]'));
$this->assertCount(0, $this->getDrupalSettings()['bigPipePlaceholderIds']);
$this->assertCount(0, $this->getSession()->getPage()->findAll('css', 'span[data-big-pipe-placeholder-id]'));
$this->assertCount(BigPipeRegressionTestController::PLACEHOLDER_COUNT + 1, $this->getSession()->getPage()->findAll('css', 'script[data-big-pipe-replacement-for-placeholder-with-id]'));
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
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');
$this->container->get('theme_installer')->install(['stark']);
}
/**
* Tests template suggestions from big_pipe_theme_suggestions_big_pipe_interface_preview().
*/
public function testBigPipeThemeHookSuggestions(): void {
$entity = $this->controller->create([
'id' => 'test_block1',
'theme' => 'stark',
'plugin' => 'system_powered_by_block',
]);
$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);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Kernel;
use Drupal\big_pipe\Render\BigPipeResponse;
use Drupal\Core\Render\HtmlResponse;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that big_pipe responses can be serialized.
*
* @group big_pipe
*/
class SerializeResponseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['big_pipe'];
/**
* Tests that big_pipe responses can be serialized.
*
* @throws \Exception
*/
public function testSerialize(): void {
$response = new BigPipeResponse(new HtmlResponse());
$this->assertIsString(serialize($response));
// Checks that the response can be serialized after the big_pipe service is injected.
$response->setBigPipeService($this->container->get('big_pipe'));
$this->assertIsString(serialize($response));
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Unit\Render;
use Drupal\big_pipe\Render\BigPipeResponse;
use Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\RendererInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\TestTools\Random;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophet;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
* @group big_pipe
*/
class BigPipeResponseAttachmentsProcessorTest extends UnitTestCase {
/**
* @covers ::processAttachments
*
* @dataProvider nonHtmlResponseProvider
*/
public function testNonHtmlResponse($response_class): void {
$big_pipe_response_attachments_processor = $this->createBigPipeResponseAttachmentsProcessor($this->prophesize(AttachmentsResponseProcessorInterface::class));
$non_html_response = new $response_class();
$this->expectException(\AssertionError::class);
$big_pipe_response_attachments_processor->processAttachments($non_html_response);
}
/**
* Provides data to testNonHtmlResponse().
*/
public static function nonHtmlResponseProvider() {
return [
'AjaxResponse, which implements AttachmentsInterface' => [AjaxResponse::class],
'A dummy that implements AttachmentsInterface' => [get_class((new Prophet())->prophesize(AttachmentsInterface::class)->reveal())],
];
}
/**
* @covers ::processAttachments
*
* @dataProvider attachmentsProvider
*/
public function testHtmlResponse(array $attachments): void {
$big_pipe_response = new BigPipeResponse(new HtmlResponse('original'));
$big_pipe_response->setAttachments($attachments);
// This mock is the main expectation of this test: verify that the decorated
// service (that is this mock) never receives BigPipe placeholder
// attachments, because it doesn't know (nor should it) how to handle them.
$html_response_attachments_processor = $this->prophesize(AttachmentsResponseProcessorInterface::class);
$html_response_attachments_processor->processAttachments(Argument::that(function ($response) {
return $response instanceof HtmlResponse
&& empty(array_intersect(['big_pipe_placeholders', 'big_pipe_nojs_placeholders'], array_keys($response->getAttachments())));
}))
->will(function ($args) {
/** @var \Symfony\Component\HttpFoundation\Response|\Drupal\Core\Render\AttachmentsInterface $response */
$response = $args[0];
// Simulate its actual behavior.
$attachments = array_diff_key($response->getAttachments(), ['html_response_attachment_placeholders' => TRUE]);
$response->setContent('processed');
$response->setAttachments($attachments);
return $response;
})
->shouldBeCalled();
$big_pipe_response_attachments_processor = $this->createBigPipeResponseAttachmentsProcessor($html_response_attachments_processor);
$processed_big_pipe_response = $big_pipe_response_attachments_processor->processAttachments($big_pipe_response);
// The secondary expectation of this test: the original (passed in) response
// object remains unchanged, the processed (returned) response object has
// the expected values.
$this->assertSame($attachments, $big_pipe_response->getAttachments(), 'Attachments of original response object MUST NOT be changed.');
$this->assertEquals('original', $big_pipe_response->getContent(), 'Content of original response object MUST NOT be changed.');
$this->assertEquals(array_diff_key($attachments, ['html_response_attachment_placeholders' => TRUE]), $processed_big_pipe_response->getAttachments(), 'Attachments of returned (processed) response object MUST be changed.');
$this->assertEquals('processed', $processed_big_pipe_response->getContent(), 'Content of returned (processed) response object MUST be changed.');
}
/**
* Provides data to testHtmlResponse().
*/
public static function attachmentsProvider() {
$typical_cases = [
'no attachments' => [[]],
'libraries' => [['library' => ['core/drupal']]],
'libraries + drupalSettings' => [['library' => ['core/drupal'], 'drupalSettings' => ['foo' => 'bar']]],
];
$official_attachment_types = [
'html_head',
'feed',
'html_head_link',
'http_header',
'library',
'placeholders',
'drupalSettings',
'html_response_attachment_placeholders',
];
$official_attachments_with_random_values = [];
foreach ($official_attachment_types as $type) {
$official_attachments_with_random_values[$type] = Random::machineName();
}
$random_attachments = ['random' . Random::machineName() => Random::machineName()];
$edge_cases = [
'all official attachment types, with random assigned values, even if technically not valid, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator' => [$official_attachments_with_random_values],
'random attachment type (unofficial), with random assigned value, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator' => [$random_attachments],
];
$big_pipe_placeholder_attachments = ['big_pipe_placeholders' => [Random::machineName()]];
$big_pipe_nojs_placeholder_attachments = ['big_pipe_nojs_placeholders' => [Random::machineName()]];
$big_pipe_cases = [
'only big_pipe_placeholders' => [$big_pipe_placeholder_attachments],
'only big_pipe_nojs_placeholders' => [$big_pipe_nojs_placeholder_attachments],
'big_pipe_placeholders + big_pipe_nojs_placeholders' => [$big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
];
$combined_cases = [
'all official attachment types + big_pipe_placeholders + big_pipe_nojs_placeholders' => [$official_attachments_with_random_values + $big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
'random attachment types + big_pipe_placeholders + big_pipe_nojs_placeholders' => [$random_attachments + $big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
];
return $typical_cases + $edge_cases + $big_pipe_cases + $combined_cases;
}
/**
* Creates a BigPipeResponseAttachmentsProcessor with mostly dummies.
*
* @param \Prophecy\Prophecy\ObjectProphecy $decorated_html_response_attachments_processor
* An object prophecy implementing AttachmentsResponseProcessorInterface.
*
* @return \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
* The BigPipeResponseAttachmentsProcessor to test.
*/
protected function createBigPipeResponseAttachmentsProcessor(ObjectProphecy $decorated_html_response_attachments_processor) {
return new BigPipeResponseAttachmentsProcessor(
$decorated_html_response_attachments_processor->reveal(),
$this->prophesize(AssetResolverInterface::class)->reveal(),
$this->prophesize(ConfigFactoryInterface::class)->reveal(),
$this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
$this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
$this->prophesize(RequestStack::class)->reveal(),
$this->prophesize(RendererInterface::class)->reveal(),
$this->prophesize(ModuleHandlerInterface::class)->reveal(),
$this->prophesize(LanguageManagerInterface::class)->reveal()
);
}
}

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Unit\Render;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe\Render\BigPipeResponse;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\PlaceholderGeneratorInterface;
use Drupal\Core\Render\RenderCacheInterface;
use Drupal\Core\Render\Renderer;
use Drupal\Core\Routing\RequestContext;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Utility\CallableResolver;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @coversDefaultClass \Drupal\big_pipe\Render\BigPipe
* @group big_pipe
*/
class FiberPlaceholderTest extends UnitTestCase {
/**
* @covers \Drupal\big_pipe\Render\BigPipe::sendPlaceholders
*/
public function testLongPlaceholderFiberSuspendingLoop(): void {
$request_stack = $this->prophesize(RequestStack::class);
$request_stack->getMainRequest()
->willReturn(new Request());
$request_stack->getCurrentRequest()
->willReturn(new Request());
$callableResolver = $this->prophesize(CallableResolver::class);
$callableResolver->getCallableFromDefinition(Argument::any())
->willReturn([TurtleLazyBuilder::class, 'turtle']);
$renderer = new Renderer(
$callableResolver->reveal(),
$this->prophesize(ThemeManagerInterface::class)->reveal(),
$this->prophesize(ElementInfoManagerInterface::class)->reveal(),
$this->prophesize(PlaceholderGeneratorInterface::class)->reveal(),
$this->prophesize(RenderCacheInterface::class)->reveal(),
$request_stack->reveal(),
[
'required_cache_contexts' => [
'languages:language_interface',
'theme',
],
],
);
$session = $this->prophesize(SessionInterface::class);
$session->start()->willReturn(TRUE);
$bigpipe = new BigPipe(
$renderer,
$session->reveal(),
$request_stack->reveal(),
$this->prophesize(HttpKernelInterface::class)->reveal(),
$this->createMock(EventDispatcherInterface::class),
$this->prophesize(ConfigFactoryInterface::class)->reveal(),
$this->prophesize(MessengerInterface::class)->reveal(),
$this->prophesize(RequestContext::class)->reveal(),
$this->prophesize(LoggerInterface::class)->reveal(),
);
$response = new BigPipeResponse(new HtmlResponse());
$attachments = [
'library' => [],
'drupalSettings' => [
'ajaxPageState' => [],
],
'big_pipe_placeholders' => [
// cspell:disable-next-line
'callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&amp;&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => [
'#lazy_builder' => [
'\Drupal\Tests\big_pipe\Unit\Render\TurtleLazyBuilder::turtle',
[],
],
],
],
];
$response->setAttachments($attachments);
// Construct minimal HTML response.
// cspell:disable-next-line
$content = '<html><body><span data-big-pipe-placeholder-id="callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&amp;&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></body></html>';
$response->setContent($content);
// Capture the result to avoid PHPUnit complaining.
ob_start();
$fiber = new \Fiber(function () use ($bigpipe, $response) {
$bigpipe->sendContent($response);
});
$fiber->start();
$this->assertFalse($fiber->isTerminated(), 'Placeholder fibers with long execution time supposed to return control before terminating');
ob_get_clean();
}
}
/**
* Test class for testing fiber placeholders.
*/
class TurtleLazyBuilder implements TrustedCallbackInterface {
/**
* Render API callback: Suspends execution twice to simulate a long operation.
*
* This function is assigned as a #lazy_builder callback.
*
* @return array
* The lazy builder callback.
*/
public static function turtle(): array {
if (\Fiber::getCurrent() !== NULL) {
\Fiber::suspend();
}
if (\Fiber::getCurrent() !== NULL) {
\Fiber::suspend();
}
return [
'#markup' => '<span>Turtle is finally here. But how?</span>',
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['turtle'];
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Unit\Render;
use Drupal\big_pipe\Render\BigPipe;
use Drupal\big_pipe\Render\BigPipeResponse;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RequestContext;
use Drupal\Tests\UnitTestCase;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* @coversDefaultClass \Drupal\big_pipe\Render\BigPipe
* @group big_pipe
*/
class ManyPlaceholderTest extends UnitTestCase {
/**
* @covers \Drupal\big_pipe\Render\BigPipe::sendNoJsPlaceholders
*/
public function testManyNoJsPlaceHolders(): void {
$session = $this->prophesize(SessionInterface::class);
$session->start()->willReturn(TRUE);
$session->save()->shouldBeCalled();
$bigpipe = new BigPipe(
$this->prophesize(RendererInterface::class)->reveal(),
$session->reveal(),
$this->prophesize(RequestStack::class)->reveal(),
$this->prophesize(HttpKernelInterface::class)->reveal(),
$this->prophesize(EventDispatcherInterface::class)->reveal(),
$this->prophesize(ConfigFactoryInterface::class)->reveal(),
$this->prophesize(MessengerInterface::class)->reveal(),
$this->prophesize(RequestContext::class)->reveal(),
$this->prophesize(LoggerInterface::class)->reveal(),
);
$response = new BigPipeResponse(new HtmlResponse());
// Add many placeholders.
$many_placeholders = [];
for ($i = 0; $i < 400; $i++) {
$many_placeholders[$this->randomMachineName(80)] = $this->randomMachineName(80);
}
$attachments = [
'library' => [],
'big_pipe_nojs_placeholders' => $many_placeholders,
];
$response->setAttachments($attachments);
// Construct minimal HTML response.
$content = '<html><body>content<drupal-big-pipe-scripts-bottom-marker>script-bottom<drupal-big-pipe-scripts-bottom-marker></body></html>';
$response->setContent($content);
// Capture the result to avoid PHPUnit complaining.
ob_start();
$bigpipe->sendContent($response);
$result = ob_get_clean();
$this->assertNotEmpty($result);
}
}

View File

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Unit\Render\Placeholder;
use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
use Drupal\big_pipe_test\BigPipePlaceholderTestCases;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
* @group big_pipe
*/
class BigPipeStrategyTest extends UnitTestCase {
/**
* @covers ::processPlaceholders
*
* @dataProvider placeholdersProvider
*/
public function testProcessPlaceholders(array $placeholders, $method, $route_match_has_no_big_pipe_option, $request_has_session, $request_has_big_pipe_nojs_cookie, array $expected_big_pipe_placeholders): void {
$request = new Request();
$request->setMethod($method);
if ($request_has_big_pipe_nojs_cookie) {
$request->cookies->set(BigPipeStrategy::NOJS_COOKIE, 1);
}
$request_stack = $this->prophesize(RequestStack::class);
$request_stack->getCurrentRequest()
->willReturn($request);
$session_configuration = $this->prophesize(SessionConfigurationInterface::class);
$session_configuration->hasSession(Argument::type(Request::class))
->willReturn($request_has_session);
$route = $this->prophesize(Route::class);
$route->getOption('_no_big_pipe')
->willReturn($route_match_has_no_big_pipe_option);
$route_match = $this->prophesize(RouteMatchInterface::class);
$route_match->getRouteObject()
->willReturn($route);
$big_pipe_strategy = new BigPipeStrategy($session_configuration->reveal(), $request_stack->reveal(), $route_match->reveal());
$processed_placeholders = $big_pipe_strategy->processPlaceholders($placeholders);
if ($request->isMethodCacheable() && !$route_match_has_no_big_pipe_option && $request_has_session) {
$this->assertSameSize($expected_big_pipe_placeholders, $processed_placeholders, 'BigPipe is able to deliver all placeholders.');
foreach (array_keys($placeholders) as $placeholder) {
$this->assertSame($expected_big_pipe_placeholders[$placeholder], $processed_placeholders[$placeholder], "Verifying how BigPipeStrategy handles the placeholder '$placeholder'");
}
}
else {
$this->assertCount(0, $processed_placeholders);
}
}
/**
* Provides the test data for testProcessPlaceholders().
*
* @see \Drupal\big_pipe_test\BigPipePlaceholderTestCases
*/
public static function placeholdersProvider() {
$cases = BigPipePlaceholderTestCases::cases();
// Generate $placeholders variable as expected by
// \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface::processPlaceholders().
$placeholders = [
$cases['html']->placeholder => $cases['html']->placeholderRenderArray,
$cases['html_attribute_value']->placeholder => $cases['html_attribute_value']->placeholderRenderArray,
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->placeholderRenderArray,
$cases['edge_case__invalid_html']->placeholder => $cases['edge_case__invalid_html']->placeholderRenderArray,
$cases['edge_case__html_non_lazy_builder']->placeholder => $cases['edge_case__html_non_lazy_builder']->placeholderRenderArray,
$cases['exception__lazy_builder']->placeholder => $cases['exception__lazy_builder']->placeholderRenderArray,
$cases['exception__embedded_response']->placeholder => $cases['exception__embedded_response']->placeholderRenderArray,
];
return [
'_no_big_pipe absent, no session, no-JS cookie absent' => [
$placeholders,
'GET',
FALSE,
FALSE,
FALSE,
[],
],
'_no_big_pipe absent, no session, no-JS cookie present' => [
$placeholders,
'GET',
FALSE,
FALSE,
TRUE,
[],
],
'_no_big_pipe present, no session, no-JS cookie absent' => [
$placeholders,
'GET',
TRUE,
FALSE,
FALSE,
[],
],
'_no_big_pipe present, no session, no-JS cookie present' => [
$placeholders,
'GET',
TRUE,
FALSE,
TRUE,
[],
],
'_no_big_pipe present, session, no-JS cookie absent' => [
$placeholders,
'GET',
TRUE,
TRUE,
FALSE,
[],
],
'_no_big_pipe present, session, no-JS cookie present' => [
$placeholders,
'GET',
TRUE,
TRUE,
TRUE,
[],
],
'_no_big_pipe absent, session, no-JS cookie absent: (JS-powered) BigPipe placeholder used for HTML placeholders' => [
$placeholders,
'GET',
FALSE,
TRUE,
FALSE,
[
$cases['html']->placeholder => $cases['html']->bigPipePlaceholderRenderArray,
$cases['html_attribute_value']->placeholder => $cases['html_attribute_value']->bigPipeNoJsPlaceholderRenderArray,
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->bigPipeNoJsPlaceholderRenderArray,
$cases['edge_case__invalid_html']->placeholder => $cases['edge_case__invalid_html']->bigPipeNoJsPlaceholderRenderArray,
$cases['edge_case__html_non_lazy_builder']->placeholder => $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderRenderArray,
$cases['exception__lazy_builder']->placeholder => $cases['exception__lazy_builder']->bigPipePlaceholderRenderArray,
$cases['exception__embedded_response']->placeholder => $cases['exception__embedded_response']->bigPipePlaceholderRenderArray,
],
],
'_no_big_pipe absent, session, no-JS cookie absent: (JS-powered) BigPipe placeholder used for HTML placeholders — but unsafe method' => [
$placeholders,
'POST',
FALSE,
TRUE,
FALSE,
[],
],
'_no_big_pipe absent, session, no-JS cookie present: no-JS BigPipe placeholder used for HTML placeholders' => [
$placeholders,
'GET',
FALSE,
TRUE,
TRUE,
[
$cases['html']->placeholder => $cases['html']->bigPipeNoJsPlaceholderRenderArray,
$cases['html_attribute_value']->placeholder => $cases['html_attribute_value']->bigPipeNoJsPlaceholderRenderArray,
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->bigPipeNoJsPlaceholderRenderArray,
$cases['edge_case__invalid_html']->placeholder => $cases['edge_case__invalid_html']->bigPipeNoJsPlaceholderRenderArray,
$cases['edge_case__html_non_lazy_builder']->placeholder => $cases['edge_case__html_non_lazy_builder']->bigPipeNoJsPlaceholderRenderArray,
$cases['exception__lazy_builder']->placeholder => $cases['exception__lazy_builder']->bigPipeNoJsPlaceholderRenderArray,
$cases['exception__embedded_response']->placeholder => $cases['exception__embedded_response']->bigPipeNoJsPlaceholderRenderArray,
],
],
'_no_big_pipe absent, session, no-JS cookie present: no-JS BigPipe placeholder used for HTML placeholders — but unsafe method' => [
$placeholders,
'POST',
FALSE,
TRUE,
TRUE,
[],
],
];
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\big_pipe\Unit\StackMiddleware;
use Drupal\big_pipe\Render\BigPipeResponse;
use Drupal\big_pipe\StackMiddleware\ContentLength;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Defines a test for ContentLength middleware.
*
* @group big_pipe
* @coversDefaultClass \Drupal\big_pipe\StackMiddleware\ContentLength
*/
final class ContentLengthTest extends UnitTestCase {
/**
* @covers ::handle
* @dataProvider providerTestSetContentLengthHeader
*/
public function testHandle(false|int $expected_header, Response $response): void {
$kernel = $this->prophesize(HttpKernelInterface::class);
$request = Request::create('/');
$kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, TRUE)->willReturn($response);
$middleware = new ContentLength($kernel->reveal());
$response = $middleware->handle($request);
if ($expected_header === FALSE) {
$this->assertFalse($response->headers->has('Content-Length'));
return;
}
$this->assertSame((string) $expected_header, $response->headers->get('Content-Length'));
}
/**
* Provides data for testHandle().
*/
public static function providerTestSetContentLengthHeader() {
$response = new Response('Test content', 200);
$response->headers->set('Content-Length', (string) strlen('Test content'));
return [
'200 ok' => [
12,
$response,
],
'Big pipe' => [
FALSE,
new BigPipeResponse(new HtmlResponse('Test content')),
],
];
}
}

View File

@ -0,0 +1,5 @@
name: 'BigPipe test theme'
type: theme
base theme: stable9
description: 'Theme for testing BigPipe edge cases.'
version: VERSION

View File

@ -0,0 +1 @@
<span class="i-am-taking-up-space">LOOK AT ME I AM CONSUMING SPACE FOR LATER</span>

View File

@ -0,0 +1,13 @@
{#
/**
* @file
* Test that comments still work with the form above instead of below.
*
* @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testCommentForm_2698811()
*/
#}
<section{{ attributes }}>
{{ comment_form }}
{{ comments }}
</section>