', [], [
'absolute' => TRUE,
'language' => $language,
'base_url' => $request->getBaseUrl() . ':90',
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index 9a68a062550..ffb53140d7b 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -191,7 +191,7 @@ class FrontPageTest extends ViewTestBase {
*/
public function testCacheTagsWithCachePluginNone() {
$this->enablePageCaching();
- $this->assertFrontPageViewCacheTags(FALSE);
+ $this->doTestFrontPageViewCacheTags(FALSE);
}
/**
@@ -207,7 +207,7 @@ class FrontPageTest extends ViewTestBase {
]);
$view->save();
- $this->assertFrontPageViewCacheTags(TRUE);
+ $this->doTestFrontPageViewCacheTags(TRUE);
}
/**
@@ -227,7 +227,7 @@ class FrontPageTest extends ViewTestBase {
]);
$view->save();
- $this->assertFrontPageViewCacheTags(TRUE);
+ $this->doTestFrontPageViewCacheTags(TRUE);
}
/**
@@ -236,7 +236,7 @@ class FrontPageTest extends ViewTestBase {
* @param bool $do_assert_views_caches
* Whether to check Views' result & output caches.
*/
- protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
+ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
@@ -248,7 +248,9 @@ class FrontPageTest extends ViewTestBase {
'user.permissions',
// Default cache contexts of the renderer.
'theme',
- 'url.query_args.pagers:0',
+ 'url.query_args',
+ // Attached feed.
+ 'url.site',
];
$cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index ebf086714d6..c471cea9684 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -71,12 +71,7 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
$cache_contexts = [
'languages:' . LanguageInterface::TYPE_INTERFACE,
- 'route.menu_active_trails:account',
- 'route.menu_active_trails:footer',
- 'route.menu_active_trails:main',
- 'route.menu_active_trails:tools',
- // The user login block access is not visible on certain routes.
- 'route.name',
+ 'route',
'theme',
'timezone',
'user.permissions',
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 86310863b87..8aa4dc32554 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -109,7 +109,10 @@ class EntityResource extends ResourceBase {
$this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
// 201 Created responses have an empty body.
- return new ResourceResponse(NULL, 201, array('Location' => $entity->url('canonical', ['absolute' => TRUE])));
+ $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
+ $response = new ResourceResponse(NULL, 201, ['Location' => $url->getGeneratedUrl()]);
+ $response->addCacheableDependency($url);
+ return $response;
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index b558b217d70..ee4b890aa8b 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -7,6 +7,7 @@
namespace Drupal\rest;
+use Drupal\Core\Render\RenderContext;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -103,10 +104,23 @@ class RequestHandler implements ContainerAwareInterface {
}
// Serialize the outgoing data for the response, if available.
- $data = $response->getResponseData();
- if ($data != NULL) {
- $output = $serializer->serialize($data, $format);
+ if ($response instanceof ResourceResponse && $data = $response->getResponseData()) {
+ // Serialization can invoke rendering (e.g., generating URLs), but the
+ // serialization API does not provide a mechanism to collect the
+ // bubbleable metadata associated with that (e.g., language and other
+ // contexts), so instead, allow those to "leak" and collect them here in
+ // a render context.
+ // @todo Add test coverage for language negotiation contexts in
+ // https://www.drupal.org/node/2135829.
+ $context = new RenderContext();
+ $output = $this->container->get('renderer')->executeInRenderContext($context, function() use ($serializer, $data, $format) {
+ return $serializer->serialize($data, $format);
+ });
$response->setContent($output);
+ if (!$context->isEmpty()) {
+ $response->addCacheableDependency($context->pop());
+ }
+
$response->headers->set('Content-Type', $request->getMimeType($format));
// Add rest settings config's cache tags.
$response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 975a309fca5..be8383980e9 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -313,7 +313,6 @@ function shortcut_preprocess_page(&$variables) {
'link' => $link,
'name' => $variables['title'],
);
- $query += \Drupal::destination()->getAsArray();
$shortcut_set = shortcut_current_displayed_set();
@@ -341,6 +340,7 @@ function shortcut_preprocess_page(&$variables) {
}
if (theme_get_setting('third_party_settings.shortcut.module_link')) {
+ $query += \Drupal::destination()->getAsArray();
$variables['title_suffix']['add_or_remove_shortcut'] = array(
'#attached' => array(
'library' => array(
diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
index e324d80501e..9a9113b9771 100644
--- a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
+++ b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
@@ -21,7 +21,7 @@ class ShortcutTranslationUITest extends ContentTranslationUITestBase {
/**
* {inheritdoc}
*/
- protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user'];
+ protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.site'];
/**
* Modules to enable.
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 0ea88ba4465..7c6a4283177 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -32,6 +32,7 @@ use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
+use Zend\Diactoros\Uri;
/**
* Test case for typical Drupal tests.
@@ -2393,16 +2394,35 @@ abstract class WebTestBase extends TestBase {
/**
* Takes a path and returns an absolute path.
*
- * @param $path
+ * This method is implemented in the way that browsers work, see
+ * https://url.spec.whatwg.org/#relative-state for more information about the
+ * possible cases.
+ *
+ * @param string $path
* A path from the internal browser content.
*
- * @return
+ * @return string
* The $path with $base_url prepended, if necessary.
*/
protected function getAbsoluteUrl($path) {
global $base_url, $base_path;
$parts = parse_url($path);
+
+ // In case the $path has a host, it is already an absolute URL and we are
+ // done.
+ if (!empty($parts['host'])) {
+ return $path;
+ }
+
+ // In case the $path contains just a query, we turn it into an absolute URL
+ // with the same scheme, host and path, see
+ // https://url.spec.whatwg.org/#relative-state.
+ if (array_keys($parts) === ['query']) {
+ $current_uri = new Uri($this->getUrl());
+ return (string) $current_uri->withQuery($parts['query']);
+ }
+
if (empty($parts['host'])) {
// Ensure that we have a string (and no xpath object).
$path = (string) $path;
@@ -2860,6 +2880,17 @@ abstract class WebTestBase extends TestBase {
$this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
}
+ /**
+ * Asserts that a cache context was not present in the last response.
+ *
+ * @param string $not_expected_cache_context
+ * The expected cache context.
+ */
+ protected function assertNoCacheContext($not_expected_cache_context) {
+ $cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
+ $this->assertFalse(in_array($not_expected_cache_context, $cache_contexts), "'" . $not_expected_cache_context . "' is not present in the X-Drupal-Cache-Contexts header.");
+ }
+
/**
* Asserts whether an expected cache tag was present in the last response.
*
diff --git a/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php b/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
index 27598d480f0..f86b85e3c08 100644
--- a/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
+++ b/core/modules/simpletest/tests/src/Unit/WebTestBaseTest.php
@@ -198,4 +198,41 @@ class WebTestBaseTest extends UnitTestCase {
$this->assertSame($expected, $clicklink_method->invoke($web_test, $label, $index));
}
+ /**
+ * @dataProvider providerTestGetAbsoluteUrl
+ */
+ public function testGetAbsoluteUrl($href, $expected_absolute_path) {
+ $web_test = $this->getMockBuilder('Drupal\simpletest\WebTestBase')
+ ->disableOriginalConstructor()
+ ->setMethods(['getUrl'])
+ ->getMock();
+
+ $web_test->expects($this->any())
+ ->method('getUrl')
+ ->willReturn('http://example.com/drupal/current-path?foo=baz');
+
+ $GLOBALS['base_url'] = 'http://example.com';
+ $GLOBALS['base_path'] = 'drupal';
+
+ $get_absolute_url_method = new \ReflectionMethod($web_test, 'getAbsoluteUrl');
+ $get_absolute_url_method->setAccessible(TRUE);
+
+ $this->assertSame($expected_absolute_path, $get_absolute_url_method->invoke($web_test, $href));
+ }
+
+ /**
+ * Provides test data for testGetAbsoluteUrl.
+ *
+ * @return array
+ */
+ public function providerTestGetAbsoluteUrl() {
+ $data = [];
+ $data['host'] = ['http://example.com/drupal/test-example', 'http://example.com/drupal/test-example'];
+ $data['path'] = ['/drupal/test-example', 'http://example.com/drupal/test-example'];
+ $data['path-with-query'] = ['/drupal/test-example?foo=bar', 'http://example.com/drupal/test-example?foo=bar'];
+ $data['just-query'] = ['?foo=bar', 'http://example.com/drupal/current-path?foo=bar'];
+
+ return $data;
+ }
+
}
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index d207a42ab49..a55ef02aaf1 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -220,7 +220,7 @@ class DbUpdateController extends ControllerBase {
$info[] = $this->t("Back up your code. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
$info[] = $this->t('Put your site into maintenance mode.', array(
- '@url' => $this->url('system.site_maintenance_mode'),
+ '@url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
));
$info[] = $this->t('Back up your database. This process will change your database values and in case of emergency you may need to revert to a backup.');
$info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
@@ -388,7 +388,7 @@ class DbUpdateController extends ControllerBase {
$dblog_exists = $this->moduleHandler->moduleExists('dblog');
if ($dblog_exists && $this->account->hasPermission('access site reports')) {
$log_message = $this->t('All errors have been logged.', array(
- '@url' => $this->url('dblog.overview'),
+ '@url' => Url::fromRoute('dblog.overview')->toString(TRUE)->getGeneratedUrl(),
));
}
else {
@@ -396,7 +396,7 @@ class DbUpdateController extends ControllerBase {
}
if (!empty($_SESSION['update_success'])) {
- $message = '' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your site. Otherwise, you may need to update your database manually.', array('@url' => $this->url(''))) . ' ' . $log_message . '
';
+ $message = '' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your site. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('')->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '
';
}
else {
$last = reset($_SESSION['updates_remaining']);
@@ -497,7 +497,7 @@ class DbUpdateController extends ControllerBase {
*/
public function requirements($severity, array $requirements) {
$options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array();
- $try_again_url = $this->url('system.db_update', $options);
+ $try_again_url = Url::fromRoute('system.db_update', $options)->toString(TRUE)->getGeneratedUrl();
$build['status_report'] = array(
'#theme' => 'status_report',
diff --git a/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
index 1759880620e..b3b363ad92a 100644
--- a/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
+++ b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php
@@ -42,6 +42,16 @@ class EarlyRenderingControllerTest extends WebTestBase {
$this->assertRaw('Hello world!');
$this->assertCacheTag('foo');
+ // AjaxResponse: non-early & early.
+ // @todo Add cache tags assertion when AjaxResponse is made cacheable in
+ // https://www.drupal.org/node/956186.
+ $this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response'));
+ $this->assertResponse(200);
+ $this->assertRaw('Hello world!');
+ $this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response.early'));
+ $this->assertResponse(200);
+ $this->assertRaw('Hello world!');
+
// Basic Response object: non-early & early.
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response'));
$this->assertResponse(200);
diff --git a/core/modules/system/src/Tests/Pager/PagerTest.php b/core/modules/system/src/Tests/Pager/PagerTest.php
index 87730fff58a..c556c292af6 100644
--- a/core/modules/system/src/Tests/Pager/PagerTest.php
+++ b/core/modules/system/src/Tests/Pager/PagerTest.php
@@ -65,7 +65,7 @@ class PagerTest extends WebTestBase {
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
preg_match('@page=(\d+)@', $elements[0]['href'], $matches);
$current_page = (int) $matches[1];
- $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+ $this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
$this->assertPagerItems($current_page);
}
@@ -77,18 +77,22 @@ class PagerTest extends WebTestBase {
$this->drupalGet('pager-test/query-parameters');
$this->assertText(t('Pager calls: 0'), 'Initial call to pager shows 0 calls.');
$this->assertText('pager.0.0');
+ $this->assertCacheContext('url.query_args');
// Go to last page, the count of pager calls need to go to 1.
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
- $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+ $this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
$this->assertText(t('Pager calls: 1'), 'First link call to pager shows 1 calls.');
$this->assertText('pager.0.60');
+ $this->assertCacheContext('url.query_args');
// Go back to first page, the count of pager calls need to go to 2.
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--first'));
- $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
+ $this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
+ $this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
$this->assertText(t('Pager calls: 2'), 'Second link call to pager shows 2 calls.');
$this->assertText('pager.0.0');
+ $this->assertCacheContext('url.query_args');
}
/**
diff --git a/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php b/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php
new file mode 100644
index 00000000000..2ae068ea821
--- /dev/null
+++ b/core/modules/system/src/Tests/Render/UrlBubbleableMetadataBubblingTest.php
@@ -0,0 +1,47 @@
+dumpHeaders = TRUE;
+ }
+
+ /**
+ * Tests that URL bubbleable metadata is correctly bubbled.
+ */
+ public function testUrlBubbleableMetadataBubbling() {
+ // Test that regular URLs bubble up bubbleable metadata when converted to
+ // string.
+ $url = Url::fromRoute('cache_test.url_bubbling');
+ $this->drupalGet($url);
+ $this->assertCacheContext('url.site');
+ $this->assertRaw($url->setAbsolute()->toString());
+ }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 7a5aed0b9bb..a826e69612a 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -657,7 +657,7 @@ function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
$pathPrefix = '';
$current_query = $request->query->all();
- Url::fromRoute('', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString();
+ Url::fromRoute('', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString(TRUE);
$current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
$current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
$path_settings = [
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index a386196c34c..9d806bb532c 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -385,7 +385,7 @@ system.theme_settings_theme:
'':
path: ''
options:
- _only_fragment: TRUE
+ _no_path: TRUE
requirements:
_access: 'TRUE'
diff --git a/core/modules/system/tests/modules/cache_test/cache_test.routing.yml b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml
new file mode 100644
index 00000000000..fb87d3ded0e
--- /dev/null
+++ b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml
@@ -0,0 +1,6 @@
+cache_test.url_bubbling:
+ path: '/cache-test/url-bubbling'
+ defaults:
+ _controller: '\Drupal\cache_test\Controller\CacheTestController::urlBubbling'
+ requirements:
+ _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php
new file mode 100644
index 00000000000..78c29b3ed02
--- /dev/null
+++ b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php
@@ -0,0 +1,29 @@
+')->setAbsolute();
+ return [
+ '#markup' => SafeMarkup::format('This URL is early-rendered: !url. Yet, its bubbleable metadata should be bubbled.', ['!url' => $url->toString()])
+ ];
+ }
+
+}
diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
index 4e050e4be1c..b71fd822b81 100644
--- a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
+++ b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml
@@ -12,6 +12,20 @@ early_rendering_controller_test.render_array.early:
requirements:
_access: 'TRUE'
+# Controller returning an AjaxResponse.
+early_rendering_controller_test.ajax_response:
+ path: '/early-rendering-controller-test/ajax-response'
+ defaults:
+ _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponse'
+ requirements:
+ _access: 'TRUE'
+early_rendering_controller_test.ajax_response.early:
+ path: '/early-rendering-controller-test/ajax-response/early'
+ defaults:
+ _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponseEarly'
+ requirements:
+ _access: 'TRUE'
+
# Controller returning a basic Response object.
early_rendering_controller_test.response:
path: '/early-rendering-controller-test/response'
diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php b/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
index f551a25698c..d560f94a3f9 100644
--- a/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
+++ b/core/modules/system/tests/modules/early_rendering_controller_test/src/EarlyRenderingTestController.php
@@ -7,6 +7,8 @@
namespace Drupal\early_rendering_controller_test;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -76,6 +78,18 @@ class EarlyRenderingTestController extends ControllerBase {
];
}
+ public function ajaxResponse() {
+ $response = new AjaxResponse();
+ $response->addCommand(new InsertCommand(NULL, $this->renderArray()));
+ return $response;
+ }
+
+ public function ajaxResponseEarly() {
+ $response = new AjaxResponse();
+ $response->addCommand(new InsertCommand(NULL, $this->renderArrayEarly()));
+ return $response;
+ }
+
public function response() {
return new Response('Hello world!');
}
diff --git a/core/modules/views/src/Controller/ViewAjaxController.php b/core/modules/views/src/Controller/ViewAjaxController.php
index 83a203be024..01e69ef05f8 100644
--- a/core/modules/views/src/Controller/ViewAjaxController.php
+++ b/core/modules/views/src/Controller/ViewAjaxController.php
@@ -14,6 +14,8 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\views\Ajax\ScrollTopCommand;
@@ -174,9 +176,18 @@ class ViewAjaxController implements ContainerInjectionInterface {
// Reuse the same DOM id so it matches that in drupalSettings.
$view->dom_id = $dom_id;
- if ($preview = $view->preview($display_id, $args)) {
- $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
+ $context = new RenderContext();
+ $preview = $this->renderer->executeInRenderContext($context, function() use ($view, $display_id, $args) {
+ return $view->preview($display_id, $args);
+ });
+ if (!$context->isEmpty()) {
+ $bubbleable_metadata = $context->pop();
+ BubbleableMetadata::createFromRenderArray($preview)
+ ->merge($bubbleable_metadata)
+ ->applyTo($preview);
}
+ $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
+
return $response;
}
else {
diff --git a/core/modules/views/src/Plugin/views/pager/Full.php b/core/modules/views/src/Plugin/views/pager/Full.php
index da1a0ec7b99..a0af225493e 100644
--- a/core/modules/views/src/Plugin/views/pager/Full.php
+++ b/core/modules/views/src/Plugin/views/pager/Full.php
@@ -96,6 +96,7 @@ class Full extends SqlBase {
'#element' => $this->options['id'],
'#parameters' => $input,
'#quantity' => $this->options['quantity'],
+ '#route_name' => !empty($this->view->live_preview) ? '' : '',
);
}
diff --git a/core/modules/views/src/Plugin/views/pager/Mini.php b/core/modules/views/src/Plugin/views/pager/Mini.php
index 65472429457..72f5d1c3877 100644
--- a/core/modules/views/src/Plugin/views/pager/Mini.php
+++ b/core/modules/views/src/Plugin/views/pager/Mini.php
@@ -103,6 +103,7 @@ class Mini extends SqlBase {
'#tags' => $tags,
'#element' => $this->options['id'],
'#parameters' => $input,
+ '#route_name' => !empty($this->view->live_preview) ? '' : '',
);
}
diff --git a/core/modules/views/src/Plugin/views/pager/SqlBase.php b/core/modules/views/src/Plugin/views/pager/SqlBase.php
index b2d73892483..4262701d9f1 100644
--- a/core/modules/views/src/Plugin/views/pager/SqlBase.php
+++ b/core/modules/views/src/Plugin/views/pager/SqlBase.php
@@ -382,14 +382,9 @@ abstract class SqlBase extends PagerPluginBase implements CacheablePluginInterfa
* {@inheritdoc}
*/
public function getCacheContexts() {
- $contexts = ['url.query_args.pagers:' . $this->options['id']];
- if ($this->options['expose']['items_per_page']) {
- $contexts[] = 'url.query_args:items_per_page';
- }
- if ($this->options['expose']['offset']) {
- $contexts[] = 'url.query_args:offset';
- }
- return $contexts;
+ // The rendered link needs to play well with any other query parameter used
+ // on the page, like other pagers and exposed filter.
+ return ['url.query_args'];
}
}
diff --git a/core/modules/views/src/Plugin/views/style/Table.php b/core/modules/views/src/Plugin/views/style/Table.php
index 4efeda2e6a4..8a594cf0afd 100644
--- a/core/modules/views/src/Plugin/views/style/Table.php
+++ b/core/modules/views/src/Plugin/views/style/Table.php
@@ -444,8 +444,9 @@ class Table extends StylePluginBase implements CacheablePluginInterface {
foreach ($this->options['info'] as $field_id => $info) {
if (!empty($info['sortable'])) {
- $contexts[] = 'url.query_args:order';
- $contexts[] = 'url.query_args:sort';
+ // The rendered link needs to play well with any other query parameter
+ // used on the page, like pager and exposed filter.
+ $contexts[] = 'url.query_args';
break;
}
}
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index 7d55f85e3ec..72854e90e7e 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -71,17 +71,29 @@ class GlossaryTest extends ViewTestBase {
$url = Url::fromRoute('view.glossary.page_1');
// Verify cache tags.
- $this->assertPageCacheContextsAndTags($url, ['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url', 'user.node_grants:view', 'user.permissions'], [
- 'config:views.view.glossary',
- 'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
- 'node_list',
- 'user:0',
- 'user_list',
- 'rendered',
- // FinishResponseSubscriber adds this cache tag to responses that have the
- // 'user.permissions' cache context for anonymous users.
- 'config:user.role.anonymous',
- ]);
+ $this->assertPageCacheContextsAndTags(
+ $url,
+ [
+ 'languages:' . LanguageInterface::TYPE_CONTENT,
+ 'languages:' . LanguageInterface::TYPE_INTERFACE,
+ 'theme',
+ 'url',
+ 'user.node_grants:view',
+ 'user.permissions',
+ 'route',
+ ],
+ [
+ 'config:views.view.glossary',
+ 'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
+ 'node_list',
+ 'user:0',
+ 'user_list',
+ 'rendered',
+ // FinishResponseSubscriber adds this cache tag to responses that have the
+ // 'user.permissions' cache context for anonymous users.
+ 'config:user.role.anonymous',
+ ]
+ );
// Check the actual page response.
$this->drupalGet($url);
diff --git a/core/modules/views/src/Tests/Handler/FieldWebTest.php b/core/modules/views/src/Tests/Handler/FieldWebTest.php
index 39e1eb2b04f..3ffec6b8d5e 100644
--- a/core/modules/views/src/Tests/Handler/FieldWebTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldWebTest.php
@@ -68,23 +68,21 @@ class FieldWebTest extends HandlerTestBase {
$this->assertResponse(200);
// Only the id and name should be click sortable, but not the name.
- $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
- $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
- $this->assertNoLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'created']]));
+ $this->assertLinkByHref(\Drupal::url('', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
+ $this->assertLinkByHref(\Drupal::url('', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
+ $this->assertNoLinkByHref(\Drupal::url('', [], ['query' => ['order' => 'created']]));
// Check that the view returns the click sorting cache contexts.
$expected_contexts = [
'languages:language_interface',
'theme',
- 'url.query_args.pagers:0',
- 'url.query_args:order',
- 'url.query_args:sort',
+ 'url.query_args',
];
$this->assertCacheContexts($expected_contexts);
// Clicking a click sort should change the order.
$this->clickLink(t('ID'));
- $this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
+ $this->assertLinkByHref(\Drupal::url('', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
// Check that the output has the expected order (asc).
$ids = $this->clickSortLoadIdsFromOutput();
$this->assertEqual($ids, range(1, 5));
diff --git a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
index ffc9bbe513d..1427c5de508 100644
--- a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
+++ b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php
@@ -206,11 +206,7 @@ class ExposedFormTest extends ViewTestBase {
'languages:language_interface',
'entity_test_view_grants',
'theme',
- 'url.query_args.pagers:0',
- 'url.query_args:items_per_page',
- 'url.query_args:offset',
- 'url.query_args:sort_order',
- 'url.query_args:sort_by',
+ 'url.query_args',
'languages:language_content'
];
diff --git a/core/modules/views/src/Tests/Plugin/PagerTest.php b/core/modules/views/src/Tests/Plugin/PagerTest.php
index 4a75fdc5f35..c1c6c97bb13 100644
--- a/core/modules/views/src/Tests/Plugin/PagerTest.php
+++ b/core/modules/views/src/Tests/Plugin/PagerTest.php
@@ -261,7 +261,7 @@ class PagerTest extends PluginTestBase {
// Test pager cache contexts.
$this->drupalGet('test_pager_full');
- $this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args.pagers:0', 'user.node_grants:view']);
+ $this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args', 'user.node_grants:view']);
}
/**
diff --git a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
index b1235b1ecba..de7903981b0 100644
--- a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
+++ b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
@@ -292,7 +292,7 @@ class RenderCacheIntegrationTest extends ViewUnitTestBase {
$view = View::load('test_display');
$view->save();
- $this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
+ $this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
}
}
diff --git a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
index dc5a898f69a..f054b912d03 100644
--- a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
+++ b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
@@ -7,11 +7,13 @@
namespace Drupal\Tests\views\Unit\Controller {
+use Drupal\Core\Render\RenderContext;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Ajax\ViewAjaxResponse;
use Drupal\views\Controller\ViewAjaxController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\views\Controller\ViewAjaxController
@@ -76,6 +78,11 @@ class ViewAjaxControllerTest extends UnitTestCase {
$elements['#attached'] = [];
return isset($elements['#markup']) ? $elements['#markup'] : '';
}));
+ $this->renderer->expects($this->any())
+ ->method('executeInRenderContext')
+ ->willReturnCallback(function (RenderContext $context, callable $callable) {
+ return $callable();
+ });
$this->currentPath = $this->getMockBuilder('Drupal\Core\Path\CurrentPathStack')
->disableOriginalConstructor()
->getMock();
@@ -83,8 +90,23 @@ class ViewAjaxControllerTest extends UnitTestCase {
$this->viewAjaxController = new ViewAjaxController($this->viewStorage, $this->executableFactory, $this->renderer, $this->currentPath, $this->redirectDestination);
+ $request_stack = new RequestStack();
+ $request_stack->push(new Request());
+ $args = [
+ $this->getMock('\Drupal\Core\Controller\ControllerResolverInterface'),
+ $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface'),
+ $this->getMock('\Drupal\Core\Render\ElementInfoManagerInterface'),
+ $this->getMock('\Drupal\Core\Render\RenderCacheInterface'),
+ $request_stack,
+ [
+ 'required_cache_contexts' => [
+ 'languages:language_interface',
+ 'theme',
+ ],
+ ],
+ ];
$this->renderer = $this->getMockBuilder('Drupal\Core\Render\Renderer')
- ->disableOriginalConstructor()
+ ->setConstructorArgs($args)
->setMethods(NULL)
->getMock();
$container = new ContainerBuilder();
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index fbe962e3da3..7728d89f335 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -494,7 +494,9 @@ function template_preprocess_views_view_table(&$variables) {
'attributes' => array('title' => $title),
'query' => $query,
);
- $variables['header'][$field]['content'] = \Drupal::l($label, new Url('', [], $link_options));
+ // It is ok to specify no URL path here as we will always reload the
+ // current page.
+ $variables['header'][$field]['content'] = \Drupal::l($label, new Url('', [], $link_options));
}
$variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
@@ -1050,6 +1052,10 @@ function template_preprocess_views_mini_pager(&$variables) {
}
$variables['items']['next']['attributes'] = new Attribute();
}
+
+ // This is is based on the entire current query string. We need to ensure
+ // cacheability is affected accordingly.
+ $variables['#cache']['contexts'][] = 'url.query_args';
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
index a554b8a5b20..35a0dab5c58 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/RenderElementTest.php
@@ -9,6 +9,7 @@ namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\GeneratedUrl;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
@@ -56,8 +57,8 @@ class RenderElementTest extends UnitTestCase {
$prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
$url = '/test?foo=bar&ajax_form=1';
- $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
- ->willReturn($url);
+ $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
+ ->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
$url_generator = $prophecy->reveal();
$this->container->set('url_generator', $url_generator);
@@ -87,8 +88,8 @@ class RenderElementTest extends UnitTestCase {
$prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
$url = '/test?foo=bar&other=query&ajax_form=1';
- $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
- ->willReturn($url);
+ $prophecy->generateFromRoute('', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
+ ->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
$url_generator = $prophecy->reveal();
$this->container->set('url_generator', $url_generator);
diff --git a/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php
new file mode 100644
index 00000000000..29e3fbf70eb
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/MetadataBubblingUrlGeneratorTest.php
@@ -0,0 +1,84 @@
+renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
+ $this->renderer->expects($this->any())
+ ->method('hasRenderContext')
+ ->willReturn(TRUE);
+
+ $this->generator = new MetadataBubblingUrlGenerator($this->generator, $this->renderer);
+ }
+
+ /**
+ * Tests bubbling of cacheable metadata for URLs.
+ *
+ * @param bool $collect_bubbleable_metadata
+ * Whether bubbleable metadata should be collected.
+ * @param int $invocations
+ * The expected amount of invocations for the ::bubble() method.
+ * @param array $options
+ * The URL options.
+ *
+ * @covers ::bubble
+ *
+ * @dataProvider providerUrlBubbleableMetadataBubbling
+ */
+ public function testUrlBubbleableMetadataBubbling($collect_bubbleable_metadata, $invocations, array $options) {
+ $self = $this;
+
+ $this->renderer->expects($this->exactly($invocations))
+ ->method('render')
+ ->willReturnCallback(function ($build) use ($self) {
+ $self->assertTrue(!empty($build['#cache']));
+ });
+
+ $url = new Url('test_1', [], $options);
+ $url->setUrlGenerator($this->generator);
+ $url->toString($collect_bubbleable_metadata);
+ }
+
+ /**
+ * Data provider for ::testUrlBubbleableMetadataBubbling().
+ */
+ public function providerUrlBubbleableMetadataBubbling() {
+ return [
+ // No bubbling when bubbleable metadata is collected.
+ [TRUE, 0, []],
+ // Bubbling when bubbleable metadata is not collected.
+ [FALSE, 1, []],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index 9f25596812c..9ab89785e92 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -11,10 +11,9 @@ use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\ContextCacheKeys;
use Drupal\Core\Cache\MemoryBackend;
-use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
-use Drupal\Core\Render\Renderer;
use Drupal\Core\Render\RenderCache;
+use Drupal\Core\Render\Renderer;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\Request;
@@ -102,6 +101,9 @@ class RendererTestBase extends UnitTestCase {
$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
$this->elementInfo = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
$this->requestStack = new RequestStack();
+ $request = new Request();
+ $request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
+ $this->requestStack->push($request);
$this->cacheFactory = $this->getMock('Drupal\Core\Cache\CacheFactoryInterface');
$this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
->disableOriginalConstructor()
@@ -129,7 +131,7 @@ class RendererTestBase extends UnitTestCase {
return new ContextCacheKeys($keys, new CacheableMetadata());
});
$this->renderCache = new RenderCache($this->requestStack, $this->cacheFactory, $this->cacheContextsManager);
- $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->rendererConfig);
+ $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->requestStack, $this->rendererConfig);
$container = new ContainerBuilder();
$container->set('cache_contexts_manager', $this->cacheContextsManager);
diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
index e78a0b07ce5..a0f621370bd 100644
--- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
@@ -23,6 +23,7 @@ use Symfony\Component\Routing\RouteCollection;
/**
* Confirm that the UrlGenerator is functioning properly.
*
+ * @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
* @group Routing
*/
class UrlGeneratorTest extends UnitTestCase {
@@ -70,11 +71,14 @@ class UrlGeneratorTest extends UnitTestCase {
$first_route = new Route('/test/one');
$second_route = new Route('/test/two/{narf}');
$third_route = new Route('/test/two/');
- $fourth_route = new Route('/test/four', array(), array(), array(), '', ['https']);
+ $fourth_route = new Route('/test/four', [], [], [], '', ['https']);
+ $none_route = new Route('', [], [], ['_no_path' => TRUE]);
+
$routes->add('test_1', $first_route);
$routes->add('test_2', $second_route);
$routes->add('test_3', $third_route);
$routes->add('test_4', $fourth_route);
+ $routes->add('', $none_route);
// Create a route provider stub.
$provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
@@ -85,22 +89,26 @@ class UrlGeneratorTest extends UnitTestCase {
// are not passed in and default to an empty array.
$route_name_return_map = $routes_names_return_map = array();
$return_map_values = array(
- array(
+ [
'route_name' => 'test_1',
'return' => $first_route,
- ),
- array(
+ ],
+ [
'route_name' => 'test_2',
'return' => $second_route,
- ),
- array(
+ ],
+ [
'route_name' => 'test_3',
'return' => $third_route,
- ),
- array(
+ ],
+ [
'route_name' => 'test_4',
'return' => $fourth_route,
- ),
+ ],
+ [
+ 'route_name' => '',
+ 'return' => $none_route,
+ ],
);
foreach ($return_map_values as $values) {
$route_name_return_map[] = array($values['route_name'], $values['return']);
@@ -414,6 +422,43 @@ class UrlGeneratorTest extends UnitTestCase {
}
}
+ /**
+ * Tests generating a relative URL with no path.
+ *
+ * @param array $options
+ * An array of URL options.
+ * @param string $expected_url
+ * The expected relative URL.
+ *
+ * @covers ::generateFromRoute
+ *
+ * @dataProvider providerTestNoPath
+ */
+ public function testNoPath($options, $expected_url) {
+ $url = $this->generator->generateFromRoute('', [], $options);
+ $this->assertEquals($expected_url, $url);
+ }
+
+ /**
+ * Data provider for ::testNoPath().
+ */
+ public function providerTestNoPath() {
+ return [
+ // Empty options.
+ [[], ''],
+ // Query parameters only.
+ [['query' => ['foo' => 'bar']], '?foo=bar'],
+ // Multiple query parameters.
+ [['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
+ // Fragment only.
+ [['fragment' => 'foo'], '#foo'],
+ // Query parameters and fragment.
+ [['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
+ // Multiple query parameters and fragment.
+ [['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
+ ];
+ }
+
/**
* Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
*