Skip to content
Snippets Groups Projects
Commit e53b13af authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2678568 by Wim Leers: Ensure good UX & DX even when A) rendering of...

Issue #2678568 by Wim Leers: Ensure good UX & DX even when A) rendering of placeholder fails, B) some response event subscriber fails
parent 980b1199
Branches
Tags
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
...@@ -205,6 +205,12 @@ protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAss ...@@ -205,6 +205,12 @@ protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAss
* @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
* The cumulative assets sent so far; to be updated while rendering no-JS * The cumulative assets sent so far; to be updated while rendering no-JS
* BigPipe placeholders. * BigPipe placeholders.
*
* @throws \Exception
* If an exception is thrown during the rendering of a placeholder, it is
* caught to allow the other placeholders to still be replaced. But when
* error logging is configured to be verbose, the exception is rethrown to
* simplify debugging.
*/ */
protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) { protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
// Split the HTML on every no-JS placeholder string. // Split the HTML on every no-JS placeholder string.
...@@ -238,7 +244,19 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse ...@@ -238,7 +244,19 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse
], ],
], ],
]; ];
try {
$elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings); $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings);
}
catch (\Exception $e) {
if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
throw $e;
}
else {
trigger_error($e, E_USER_ERROR);
continue;
}
}
// Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
// before the HTML they're associated with. In other words: ensure the // before the HTML they're associated with. In other words: ensure the
...@@ -263,7 +281,19 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse ...@@ -263,7 +281,19 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse
// - the HTML to load the JS (at the top) can be rendered. // - the HTML to load the JS (at the top) can be rendered.
$fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request = $this->requestStack->getMasterRequest()->duplicate();
$fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())]); $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())]);
try {
$html_response = $this->filterEmbeddedResponse($fake_request, $html_response); $html_response = $this->filterEmbeddedResponse($fake_request, $html_response);
}
catch (\Exception $e) {
if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
throw $e;
}
else {
trigger_error($e, E_USER_ERROR);
continue;
}
}
// Send this embedded HTML response. // Send this embedded HTML response.
print $html_response->getContent(); print $html_response->getContent();
...@@ -290,6 +320,12 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse ...@@ -290,6 +320,12 @@ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAsse
* @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
* The cumulative assets sent so far; to be updated while rendering BigPipe * The cumulative assets sent so far; to be updated while rendering BigPipe
* placeholders. * placeholders.
*
* @throws \Exception
* If an exception is thrown during the rendering of a placeholder, it is
* caught to allow the other placeholders to still be replaced. But when
* error logging is configured to be verbose, the exception is rethrown to
* simplify debugging.
*/ */
protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) { protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) {
// Return early if there are no BigPipe placeholders to send. // Return early if there are no BigPipe placeholders to send.
...@@ -320,7 +356,18 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde ...@@ -320,7 +356,18 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
// Render the placeholder. // Render the placeholder.
$placeholder_render_array = $placeholders[$placeholder_id]; $placeholder_render_array = $placeholders[$placeholder_id];
try {
$elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array); $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array);
}
catch (\Exception $e) {
if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
throw $e;
}
else {
trigger_error($e, E_USER_ERROR);
continue;
}
}
// Create a new AjaxResponse. // Create a new AjaxResponse.
$ajax_response = new AjaxResponse(); $ajax_response = new AjaxResponse();
...@@ -342,7 +389,18 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde ...@@ -342,7 +389,18 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
// allows us to track the total set of asset libraries sent in the // allows us to track the total set of asset libraries sent in the
// initial HTML response plus all embedded AJAX responses sent so far. // initial HTML response plus all embedded AJAX responses sent so far.
$fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']); $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
try {
$ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response); $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);
}
catch (\Exception $e) {
if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
throw $e;
}
else {
trigger_error($e, E_USER_ERROR);
continue;
}
}
// Send this embedded AJAX response. // Send this embedded AJAX response.
$json = $ajax_response->getContent(); $json = $ajax_response->getContent();
......
...@@ -260,12 +260,94 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf ...@@ -260,12 +260,94 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
$current_time->embeddedHtmlResponse = '<time datetime=1991-03-14"></time>'; $current_time->embeddedHtmlResponse = '<time datetime=1991-03-14"></time>';
// 6. Edge case: #lazy_builder that throws an exception.
$exception = new BigPipePlaceholderTestCase(
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
'#create_placeholder' => TRUE,
],
'<drupal-render-placeholder callback="\Drupal\big_pipe_test\BigPipeTestController::exception" arguments="0=llamas&amp;1=suck" token="68a75f1a"></drupal-render-placeholder>',
[
'#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
]
);
$exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a';
$exception->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a"></div>',
'#cache' => $cacheability_depends_on_session_and_nojs_cookie,
'#attached' => [
'library' => ['big_pipe/big_pipe'],
'drupalSettings' => [
'bigPipePlaceholderIds' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args[0]=llamas&args[1]=suck&token=68a75f1a' => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a' => $exception->placeholderRenderArray,
],
],
];
$exception->embeddedAjaxResponseCommands = NULL;
$exception->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args[0]=llamas&amp;args[1]=suck&amp;token=68a75f1a"></div>';
$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;
// 7. 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="2a9bd022"></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=2a9bd022';
$embedded_response_exception->bigPipePlaceholderRenderArray = [
'#markup' => '<div data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022"></div>',
'#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=2a9bd022' => TRUE,
],
],
'big_pipe_placeholders' => [
'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022' => $embedded_response_exception->placeholderRenderArray,
],
],
];
$embedded_response_exception->embeddedAjaxResponseCommands = NULL;
$embedded_response_exception->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=2a9bd022"></div>';
$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 [ return [
'html' => $status_messages, 'html' => $status_messages,
'html_attribute_value' => $form_action, 'html_attribute_value' => $form_action,
'html_attribute_value_subset' => $csrf_token, 'html_attribute_value_subset' => $csrf_token,
'edge_case__invalid_html' => $hello, 'edge_case__invalid_html' => $hello,
'edge_case__html_non_lazy_builder' => $current_time, 'edge_case__html_non_lazy_builder' => $current_time,
'exception__lazy_builder' => $exception,
'exception__embedded_response' => $embedded_response_exception,
]; ];
} }
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
use Drupal\big_pipe\Render\BigPipe; use Drupal\big_pipe\Render\BigPipe;
use Drupal\Component\Serialization\Json; use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Html;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase; use Drupal\simpletest\WebTestBase;
...@@ -32,7 +33,7 @@ class BigPipeTest extends WebTestBase { ...@@ -32,7 +33,7 @@ class BigPipeTest extends WebTestBase {
* *
* @var array * @var array
*/ */
public static $modules = ['big_pipe', 'big_pipe_test']; public static $modules = ['big_pipe', 'big_pipe_test', 'dblog'];
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -121,13 +122,13 @@ public function testNoJsDetection() { ...@@ -121,13 +122,13 @@ public function testNoJsDetection() {
$this->cookies = []; $this->cookies = [];
// Edge case: route with '_no_big_pipe' option. // Edge case: route with '_no_big_pipe' option.
$this->drupalGet(Url::fromRoute('big_pipe_test.no_big_pipe')); $this->drupalGet(Url::fromRoute('no_big_pipe'));
$this->assertSessionCookieExists(FALSE); $this->assertSessionCookieExists(FALSE);
$this->assertBigPipeNoJsCookieExists(FALSE); $this->assertBigPipeNoJsCookieExists(FALSE);
$this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL='); $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
$this->assertNoRaw($no_js_to_js_markup); $this->assertNoRaw($no_js_to_js_markup);
$this->drupalLogin($this->rootUser); $this->drupalLogin($this->rootUser);
$this->drupalGet(Url::fromRoute('big_pipe_test.no_big_pipe')); $this->drupalGet(Url::fromRoute('no_big_pipe'));
$this->assertSessionCookieExists(TRUE); $this->assertSessionCookieExists(TRUE);
$this->assertBigPipeNoJsCookieExists(FALSE); $this->assertBigPipeNoJsCookieExists(FALSE);
$this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL='); $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
...@@ -145,10 +146,15 @@ public function testNoJsDetection() { ...@@ -145,10 +146,15 @@ public function testNoJsDetection() {
* @see \Drupal\big_pipe\Tests\BigPipePlaceholderTestCases * @see \Drupal\big_pipe\Tests\BigPipePlaceholderTestCases
*/ */
public function testBigPipe() { public function testBigPipe() {
// Simulate production.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
$this->drupalLogin($this->rootUser); $this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists(TRUE); $this->assertSessionCookieExists(TRUE);
$this->assertBigPipeNoJsCookieExists(FALSE); $this->assertBigPipeNoJsCookieExists(FALSE);
$log_count = db_query('SELECT COUNT(*) FROM {watchdog}')->fetchField();
// By not calling performMetaRefresh() here, we simulate JavaScript being // By not calling performMetaRefresh() here, we simulate JavaScript being
// enabled, because as far as the BigPipe module is concerned, JavaScript is // 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. // enabled in the browser as long as the BigPipe no-JS cookie is *not* set.
...@@ -167,15 +173,39 @@ public function testBigPipe() { ...@@ -167,15 +173,39 @@ public function testBigPipe() {
$this->assertBigPipePlaceholders([ $this->assertBigPipePlaceholders([
$cases['html']->bigPipePlaceholderId => Json::encode($cases['html']->embeddedAjaxResponseCommands), $cases['html']->bigPipePlaceholderId => Json::encode($cases['html']->embeddedAjaxResponseCommands),
$cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder']->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,
]); ]);
$this->assertRaw('</body>', 'Closing body tag present.');
$this->pass('Verifying BigPipe assets are present…', 'Debug'); $this->pass('Verifying BigPipe assets are present…', 'Debug');
$this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings present.'); $this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings present.');
$this->assertTrue(in_array('big_pipe/big_pipe', explode(',', $this->getDrupalSettings()['ajaxPageState']['libraries'])), 'BigPipe asset library is present.'); $this->assertTrue(in_array('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->assertEqual($log_count + 2, db_query('SELECT COUNT(*) FROM {watchdog}')->fetchField(), 'Two new watchdog entries.');
$records = db_query('SELECT * FROM {watchdog} ORDER BY wid DESC LIMIT 2')->fetchAll();
$this->assertEqual(RfcLogLevel::ERROR, $records[0]->severity);
$this->assertTrue(FALSE !== strpos((string) unserialize($records[0]->variables)['@message'], 'Oh noes!'));
$this->assertEqual(RfcLogLevel::ERROR, $records[0]->severity);
$this->assertTrue(FALSE !== strpos((string) unserialize($records[1]->variables)['@message'], 'You are not allowed to say llamas are not cool!'));
// Verify that 4xx responses work fine. (4xx responses are handled by // Verify that 4xx responses work fine. (4xx responses are handled by
// subrequests to a route pointing to a controller with the desired output.) // subrequests to a route pointing to a controller with the desired output.)
$this->drupalGet(Url::fromUri('base:non-existing-path')); $this->drupalGet(Url::fromUri('base:non-existing-path'));
// Simulate development.
$this->pass('Verifying BigPipe provides useful error output when an error occurs while rendering a placeholder if verbose error logging is enabled.', 'Debug');
$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->assertRaw('The website encountered an unexpected error. Please try again later');
$this->assertRaw('You are not allowed to say llamas are not cool!');
$this->assertNoRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal absent: error occurred before then.');
$this->assertNoRaw('</body>', 'Closing body tag absent: error occurred before then.');
// The exception is expected. Do not interpret it as a test failure.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
} }
/** /**
...@@ -189,6 +219,9 @@ public function testBigPipe() { ...@@ -189,6 +219,9 @@ public function testBigPipe() {
* @see \Drupal\big_pipe\Tests\BigPipePlaceholderTestCases * @see \Drupal\big_pipe\Tests\BigPipePlaceholderTestCases
*/ */
public function testBigPipeNoJs() { public function testBigPipeNoJs() {
// Simulate production.
$this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
$this->drupalLogin($this->rootUser); $this->drupalLogin($this->rootUser);
$this->assertSessionCookieExists(TRUE); $this->assertSessionCookieExists(TRUE);
$this->assertBigPipeNoJsCookieExists(FALSE); $this->assertBigPipeNoJsCookieExists(FALSE);
...@@ -211,6 +244,8 @@ public function testBigPipeNoJs() { ...@@ -211,6 +244,8 @@ public function testBigPipeNoJs() {
$cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse, $cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse,
$cases['html']->bigPipeNoJsPlaceholder => $cases['html']->embeddedHtmlResponse, $cases['html']->bigPipeNoJsPlaceholder => $cases['html']->embeddedHtmlResponse,
$cases['edge_case__html_non_lazy_builder']->bigPipeNoJsPlaceholder => $cases['edge_case__html_non_lazy_builder']->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,
]); ]);
$this->pass('Verifying there are no BigPipe placeholders & replacements…', 'Debug'); $this->pass('Verifying there are no BigPipe placeholders & replacements…', 'Debug');
...@@ -221,10 +256,22 @@ public function testBigPipeNoJs() { ...@@ -221,10 +256,22 @@ public function testBigPipeNoJs() {
$this->pass('Verifying BigPipe assets are absent…', 'Debug'); $this->pass('Verifying BigPipe assets are absent…', 'Debug');
$this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings and BigPipe asset library absent.'); $this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings and BigPipe asset library absent.');
$this->assertRaw('</body>', 'Closing body tag present.');
// Verify that 4xx responses work fine. (4xx responses are handled by // Verify that 4xx responses work fine. (4xx responses are handled by
// subrequests to a route pointing to a controller with the desired output.) // subrequests to a route pointing to a controller with the desired output.)
$this->drupalGet(Url::fromUri('base:non-existing-path')); $this->drupalGet(Url::fromUri('base:non-existing-path'));
// Simulate development.
$this->pass('Verifying BigPipe provides useful error output when an error occurs while rendering a placeholder if verbose error logging is enabled.', 'Debug');
$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->assertRaw('The website encountered an unexpected error. Please try again later');
$this->assertRaw('You are not allowed to say llamas are not cool!');
$this->assertNoRaw('</body>', 'Closing body tag absent: error occurred before then.');
// The exception is expected. Do not interpret it as a test failure.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
} }
protected function assertBigPipeResponseHeadersPresent() { protected function assertBigPipeResponseHeadersPresent() {
...@@ -246,9 +293,12 @@ protected function assertBigPipeNoJsPlaceholders(array $expected_big_pipe_nojs_p ...@@ -246,9 +293,12 @@ protected function assertBigPipeNoJsPlaceholders(array $expected_big_pipe_nojs_p
$this->assertSetsEqual(array_keys($expected_big_pipe_nojs_placeholders), array_map('rawurldecode', explode(' ', $this->drupalGetHeader('BigPipe-Test-No-Js-Placeholders')))); $this->assertSetsEqual(array_keys($expected_big_pipe_nojs_placeholders), array_map('rawurldecode', explode(' ', $this->drupalGetHeader('BigPipe-Test-No-Js-Placeholders'))));
foreach ($expected_big_pipe_nojs_placeholders as $big_pipe_nojs_placeholder => $expected_replacement) { foreach ($expected_big_pipe_nojs_placeholders as $big_pipe_nojs_placeholder => $expected_replacement) {
$this->pass('Checking whether the replacement for the BigPipe no-JS placeholder "' . $big_pipe_nojs_placeholder . '" is present:'); $this->pass('Checking whether the replacement for the BigPipe no-JS placeholder "' . $big_pipe_nojs_placeholder . '" is present:');
$this->assertNoRaw($big_pipe_nojs_placeholder);
if ($expected_replacement !== NULL) {
$this->assertRaw($expected_replacement); $this->assertRaw($expected_replacement);
} }
} }
}
/** /**
* Asserts expected BigPipe placeholders are present and replaced. * Asserts expected BigPipe placeholders are present and replaced.
...@@ -269,9 +319,14 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde ...@@ -269,9 +319,14 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde
$pos = strpos($this->getRawContent(), $expected_placeholder_html); $pos = strpos($this->getRawContent(), $expected_placeholder_html);
$placeholder_positions[$pos] = $big_pipe_placeholder_id; $placeholder_positions[$pos] = $big_pipe_placeholder_id;
// Verify expected placeholder replacement. // 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 . '">';
$result = $this->xpath('//script[@data-big-pipe-replacement-for-placeholder-with-id=:id]', [':id' => Html::decodeEntities($big_pipe_placeholder_id)]); $result = $this->xpath('//script[@data-big-pipe-replacement-for-placeholder-with-id=:id]', [':id' => Html::decodeEntities($big_pipe_placeholder_id)]);
if ($expected_ajax_response === NULL) {
$this->assertEqual(0, count($result));
$this->assertNoRaw($expected_placeholder_replacement);
continue;
}
$this->assertEqual($expected_ajax_response, trim((string) $result[0])); $this->assertEqual($expected_ajax_response, trim((string) $result[0]));
$expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">';
$this->assertRaw($expected_placeholder_replacement); $this->assertRaw($expected_placeholder_replacement);
$pos = strpos($this->getRawContent(), $expected_placeholder_replacement); $pos = strpos($this->getRawContent(), $expected_placeholder_replacement);
$placeholder_replacement_positions[$pos] = $big_pipe_placeholder_id; $placeholder_replacement_positions[$pos] = $big_pipe_placeholder_id;
...@@ -279,8 +334,9 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde ...@@ -279,8 +334,9 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde
ksort($placeholder_positions, SORT_NUMERIC); ksort($placeholder_positions, SORT_NUMERIC);
$this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions)); $this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions));
$this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<div data-big-pipe-placeholder-id="', '/') . '/', $this->getRawContent())); $this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<div data-big-pipe-placeholder-id="', '/') . '/', $this->getRawContent()));
$this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_replacement_positions)); $expected_big_pipe_placeholders_with_replacements = array_filter($expected_big_pipe_placeholders);
$this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="', '/') . '/', $this->getRawContent())); $this->assertEqual(array_keys($expected_big_pipe_placeholders_with_replacements), array_values($placeholder_replacement_positions));
$this->assertEqual(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->getRawContent()));
$this->pass('Verifying BigPipe start/stop signals…', 'Debug'); $this->pass('Verifying BigPipe start/stop signals…', 'Debug');
$this->assertRaw(BigPipe::START_SIGNAL, 'BigPipe start signal present.'); $this->assertRaw(BigPipe::START_SIGNAL, 'BigPipe start signal present.');
...@@ -290,7 +346,7 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde ...@@ -290,7 +346,7 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde
$this->assertTrue($start_signal_position < $stop_signal_position, 'BigPipe start signal appears before stop signal.'); $this->assertTrue($start_signal_position < $stop_signal_position, 'BigPipe start signal appears before stop signal.');
$this->pass('Verifying BigPipe placeholder replacements and start/stop signals were streamed in the correct order…', 'Debug'); $this->pass('Verifying BigPipe placeholder replacements and start/stop signals were streamed in the correct order…', 'Debug');
$expected_stream_order = array_keys($expected_big_pipe_placeholders); $expected_stream_order = array_keys($expected_big_pipe_placeholders_with_replacements);
array_unshift($expected_stream_order, BigPipe::START_SIGNAL); array_unshift($expected_stream_order, BigPipe::START_SIGNAL);
array_push($expected_stream_order, BigPipe::STOP_SIGNAL); array_push($expected_stream_order, BigPipe::STOP_SIGNAL);
$actual_stream_order = $placeholder_replacement_positions + [ $actual_stream_order = $placeholder_replacement_positions + [
......
...@@ -6,11 +6,11 @@ big_pipe_test: ...@@ -6,11 +6,11 @@ big_pipe_test:
requirements: requirements:
_access: 'TRUE' _access: 'TRUE'
big_pipe_test.no_big_pipe: no_big_pipe:
path: '/no_big_pipe' path: '/no_big_pipe'
defaults: defaults:
_controller: '\Drupal\big_pipe_test\BigPipeTestController::test' _controller: '\Drupal\big_pipe_test\BigPipeTestController::nope'
_title: 'BigPipe test' _title: '_no_big_pipe route option test'
options: options:
_no_big_pipe: TRUE _no_big_pipe: TRUE
requirements: requirements:
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\big_pipe_test; namespace Drupal\big_pipe_test;
use Drupal\big_pipe\Render\BigPipeMarkup; use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber;
class BigPipeTestController { class BigPipeTestController {
...@@ -34,9 +35,22 @@ public function test() { ...@@ -34,9 +35,22 @@ public function test() {
// 5. Edge case: non-#lazy_builder placeholder. // 5. Edge case: non-#lazy_builder placeholder.
$build['edge_case__html_non_lazy_builder'] = $cases['edge_case__html_non_lazy_builder']->renderArray; $build['edge_case__html_non_lazy_builder'] = $cases['edge_case__html_non_lazy_builder']->renderArray;
// 6. Exception: #lazy_builder that throws an exception.
$build['exception__lazy_builder'] = $cases['exception__lazy_builder']->renderArray;
// 7. Exception: placeholder that causes response filter to throw exception.
$build['exception__embedded_response'] = $cases['exception__embedded_response']->renderArray;
return $build; return $build;
} }
/**
* @return array
*/
public static function nope() {
return ['#markup' => '<p>Nope.</p>'];
}
/** /**
* #lazy_builder callback; builds <time> markup with current time. * #lazy_builder callback; builds <time> markup with current time.
* *
...@@ -63,4 +77,24 @@ public static function helloOrYarhar() { ...@@ -63,4 +77,24 @@ public static function helloOrYarhar() {
]; ];
} }
/**
* #lazy_builder callback; throws exception.
*
* @throws \Exception
*/
public static function exception() {
throw new \Exception('You are not allowed to say llamas are not cool!');
}
/**
* #lazy_builder callback; returns content that will trigger an exception.
*
* @see \Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber::onRespondTriggerException()
*
* @return array
*/
public static function responseException() {
return ['#plain_text' => BigPipeTestSubscriber::CONTENT_TRIGGER_EXCEPTION];
}
} }
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\big_pipe_test\EventSubscriber; namespace Drupal\big_pipe_test\EventSubscriber;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\HtmlResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
...@@ -14,13 +15,45 @@ ...@@ -14,13 +15,45 @@
class BigPipeTestSubscriber implements EventSubscriberInterface { 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.
*
* @see \Drupal\big_pipe_test\BigPipeTestController::responseException()
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*
* @throws \Exception
*/
public function onRespondTriggerException(FilterResponseEvent $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 (strpos($response->getContent(), static::CONTENT_TRIGGER_EXCEPTION) !== FALSE) {
throw new \Exception('Oh noes!');
}
}
}
/** /**
* Exposes all BigPipe placeholders (JS and no-JS) via headers for testing. * Exposes all BigPipe placeholders (JS and no-JS) via headers for testing.
* *
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process. * The event to process.
*/ */
public function onRespond(FilterResponseEvent $event) { public function onRespondSetBigPipeDebugPlaceholderHeaders(FilterResponseEvent $event) {
$response = $event->getResponse(); $response = $event->getResponse();
if (!$response instanceof HtmlResponse) { if (!$response instanceof HtmlResponse) {
return; return;
...@@ -44,8 +77,11 @@ public function onRespond(FilterResponseEvent $event) { ...@@ -44,8 +77,11 @@ public function onRespond(FilterResponseEvent $event) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function getSubscribedEvents() { public static function getSubscribedEvents() {
// Run *just* before \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber::onRespond(). // Run just before \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber::onRespond().
$events[KernelEvents::RESPONSE][] = ['onRespond', -99999]; $events[KernelEvents::RESPONSE][] = ['onRespondSetBigPipeDebugPlaceholderHeaders', -9999];
// Run just after \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber::onRespond().
$events[KernelEvents::RESPONSE][] = ['onRespondTriggerException', -10001];
return $events; return $events;
} }
......
...@@ -75,6 +75,8 @@ public function placeholdersProvider() { ...@@ -75,6 +75,8 @@ public function placeholdersProvider() {
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->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__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['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 [ return [
...@@ -90,6 +92,8 @@ public function placeholdersProvider() { ...@@ -90,6 +92,8 @@ public function placeholdersProvider() {
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->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__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['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 present: no-JS BigPipe placeholder used for HTML placeholders' => [$placeholders, FALSE, TRUE, TRUE, [ '_no_big_pipe absent, session, no-JS cookie present: no-JS BigPipe placeholder used for HTML placeholders' => [$placeholders, FALSE, TRUE, TRUE, [
$cases['html']->placeholder => $cases['html']->bigPipeNoJsPlaceholderRenderArray, $cases['html']->placeholder => $cases['html']->bigPipeNoJsPlaceholderRenderArray,
...@@ -97,6 +101,8 @@ public function placeholdersProvider() { ...@@ -97,6 +101,8 @@ public function placeholdersProvider() {
$cases['html_attribute_value_subset']->placeholder => $cases['html_attribute_value_subset']->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__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['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,
]], ]],
]; ];
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment