From 9309d3d9f549ad09f16de6fe1130f34d55a8e2c4 Mon Sep 17 00:00:00 2001
From: catch <git config --global user.email catch@35733.no-reply.drupal.org>
Date: Thu, 12 Mar 2015 10:16:15 +0000
Subject: [PATCH] Issue #2381217 by Wim Leers, dawehner, Fabianx: Views should
 set cache tags on its render arrays, and bubble the output's cache tags to
 the cache items written to the Views output cache

---
 core/lib/Drupal/Core/Render/Renderer.php      |   9 +-
 .../node/src/Tests/Views/FrontPageTest.php    | 178 ++++++++++++++-
 .../AssertPageCacheContextsAndTagsTrait.php   |  93 ++++++++
 .../Cache/PageCacheTagsIntegrationTest.php    |  51 +----
 .../Plugin/views/cache/CachePluginBase.php    |  22 +-
 .../views/display/DisplayPluginBase.php       |   9 +
 .../Plugin/views/query/QueryPluginBase.php    |   7 +
 .../views/src/Plugin/views/query/Sql.php      |  18 ++
 .../src/Tests/AssertViewsCacheTagsTrait.php   |  87 ++++++++
 core/modules/views/src/Tests/GlossaryTest.php |  18 +-
 .../views/src/Tests/Plugin/CacheTest.php      |  45 +++-
 .../src/Tests/RenderCacheIntegrationTest.php  | 203 ++++++++++++++++++
 core/modules/views/src/ViewExecutable.php     |  18 ++
 .../views.view.entity_test_fields.yml         |  74 +++++++
 .../test_views/views.view.entity_test_row.yml |  41 ++++
 .../test_views/views.view.test_cache.yml      |  12 ++
 .../Core/Render/RendererBubblingTest.php      |  35 ++-
 .../Render/RendererPostRenderCacheTest.php    |  16 +-
 .../Drupal/Tests/Core/Render/RendererTest.php |   6 +-
 .../Tests/Core/Render/RendererTestBase.php    |   2 +
 20 files changed, 849 insertions(+), 95 deletions(-)
 create mode 100644 core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
 create mode 100644 core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php
 create mode 100644 core/modules/views/src/Tests/RenderCacheIntegrationTest.php
 create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml
 create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml

diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 189c2debf84d..6293e8fd7e54 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -544,11 +544,6 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
 
     $data = $this->getCacheableRenderArray($elements);
 
-    // Cache tags are cached, but we also want to associate the "rendered" cache
-    // tag. This allows us to invalidate the entire render cache, regardless of
-    // the cache bin.
-    $data['#cache']['tags'][] = 'rendered';
-
     $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
     $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : Cache::PERMANENT;
     $cache = $this->cacheFactory->get($bin);
@@ -690,7 +685,7 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
             'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
           ],
         ];
-        $cache->set($pre_bubbling_cid, $redirect_data, $expire, $redirect_data['#cache']['tags']);
+        $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
       }
 
       // Current cache contexts incomplete: this request only uses a subset of
@@ -711,7 +706,7 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
         $data['#cache']['contexts'] = $merged_cache_contexts;
       }
     }
-    $cache->set($cid, $data, $expire, $data['#cache']['tags']);
+    $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
   }
 
   /**
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index eb970d6bd7c6..c5dee988e6e4 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -7,6 +7,11 @@
 
 namespace Drupal\node\Tests\Views;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Url;
+use Drupal\node\Entity\Node;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+use Drupal\views\Tests\AssertViewsCacheTagsTrait;
 use Drupal\views\Tests\ViewTestBase;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Views;
@@ -18,6 +23,14 @@
  */
 class FrontPageTest extends ViewTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+  use AssertViewsCacheTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $dumpHeaders = TRUE;
+
   /**
    * The entity storage for nodes.
    *
@@ -35,7 +48,8 @@ class FrontPageTest extends ViewTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->nodeStorage = $this->container->get('entity.manager')->getStorage('node');
+    $this->nodeStorage = $this->container->get('entity.manager')
+      ->getStorage('node');
   }
 
   /**
@@ -173,4 +187,166 @@ public function testAdminFrontPage() {
     $this->assertPattern('/class=".+view-frontpage/', 'Frontpage view was rendered');
   }
 
+  /**
+   * Tests the cache tags when using the "none" cache plugin.
+   */
+  public function testCacheTagsWithCachePluginNone() {
+    $this->enablePageCaching();
+    $this->assertFrontPageViewCacheTags(FALSE);
+  }
+
+  /**
+   * Tests the cache tags when using the "tag" cache plugin.
+   */
+  public function testCacheTagsWithCachePluginTag() {
+    $this->enablePageCaching();
+
+    $view = Views::getView('frontpage');
+    $view->setDisplay('page_1');
+    $view->display_handler->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+    $view->save();
+
+    $this->assertFrontPageViewCacheTags(TRUE);
+  }
+
+  /**
+   * Tests the cache tags when using the "time" cache plugin.
+   */
+  public function testCacheTagsWithCachePluginTime() {
+    $this->enablePageCaching();
+
+    $view = Views::getView('frontpage');
+    $view->setDisplay('page_1');
+    $view->display_handler->overrideOption('cache', [
+      'type' => 'time',
+      'options' => [
+        'results_lifespan' => 3600,
+        'output_lifespan' => 3600,
+      ],
+    ]);
+    $view->save();
+
+    $this->assertFrontPageViewCacheTags(TRUE);
+  }
+
+  /**
+   * Tests the cache tags on the front page.
+   *
+   * @param bool $do_assert_views_caches
+   *   Whether to check Views' result & output caches.
+   */
+  protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
+    $view = Views::getView('frontpage');
+    $view->setDisplay('page_1');
+
+    $cache_contexts = [];
+
+    // Test before there are any nodes.
+    $empty_node_listing_cache_tags = [
+      'config:views.view.frontpage',
+      'node_list',
+    ];
+    $this->assertViewsCacheTags(
+      $view,
+      $empty_node_listing_cache_tags,
+      $do_assert_views_caches,
+      $empty_node_listing_cache_tags
+    );
+    $this->assertPageCacheContextsAndTags(
+      Url::fromRoute('view.frontpage.page_1'),
+      $cache_contexts,
+      Cache::mergeTags($empty_node_listing_cache_tags, ['rendered'])
+    );
+
+    // Create some nodes on the frontpage view. Add more than 10 nodes in order
+    // to enable paging.
+    $this->drupalCreateContentType(['type' => 'article']);
+    for ($i = 0; $i < 15; $i++) {
+      $node = Node::create([
+        'body' => [
+          [
+            'value' => $this->randomMachineName(32),
+            'format' => filter_default_format(),
+          ]
+        ],
+        'type' => 'article',
+        'created' => $i,
+        'title' => $this->randomMachineName(8),
+        'nid' => $i + 1,
+      ]);
+      $node->enforceIsNew(TRUE);
+      $node->save();
+    }
+    $cache_contexts = Cache::mergeContexts($cache_contexts, [
+      'theme',
+      'timezone',
+      'user.roles'
+    ]);
+
+    // First page.
+    $first_page_result_cache_tags = [
+      'config:views.view.frontpage',
+      'node_list',
+      'node:6',
+      'node:7',
+      'node:8',
+      'node:9',
+      'node:10',
+      'node:11',
+      'node:12',
+      'node:13',
+      'node:14',
+      'node:15',
+    ];
+    $first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, [
+      'config:filter.format.plain_text',
+      'node_view',
+      'user_view',
+      'user:0',
+    ]);
+    $view->setDisplay('page_1');
+    $view->setCurrentPage(0);
+    $this->assertViewsCacheTags(
+      $view,
+      $first_page_result_cache_tags,
+      $do_assert_views_caches,
+      $first_page_output_cache_tags
+    );
+    $this->assertPageCacheContextsAndTags(
+      Url::fromRoute('view.frontpage.page_1'),
+      $cache_contexts,
+      Cache::mergeTags($first_page_output_cache_tags, ['rendered'])
+    );
+
+    // Second page.
+    $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), $cache_contexts, [
+      // The cache tags for the listed nodes.
+      'node:1',
+      'node:2',
+      'node:3',
+      'node:4',
+      'node:5',
+      // The rest.
+      'config:filter.format.plain_text',
+      'config:views.view.frontpage',
+      'node_list',
+      'node_view',
+      'user_view',
+      'user:0',
+      'rendered',
+    ]);
+
+    // Let's update a node title on the first page and ensure that the page
+    // cache entry invalidates.
+    $node = Node::load(10);
+    $title = $node->getTitle() . 'a';
+    $node->setTitle($title);
+    $node->save();
+
+    $this->drupalGet(Url::fromRoute('view.frontpage.page_1'));
+    $this->assertText($title);
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
new file mode 100644
index 000000000000..15234216f836
--- /dev/null
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\Url;
+
+/**
+ * Provides test assertions for testing page-level cache contexts & tags.
+ *
+ * Can be used by test classes that extend \Drupal\simpletest\WebTestBase.
+ */
+trait AssertPageCacheContextsAndTagsTrait {
+
+  /**
+   * Enables page caching.
+   */
+  protected function enablePageCaching() {
+    $config = $this->config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+  }
+
+  /**
+   * Asserts page cache miss, then hit for the given URL; checks cache headers.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The URL to test.
+   * @param string[] $expected_contexts
+   *   The expected cache contexts for the given URL.
+   * @param string[] $expected_tags
+   *   The expected cache tags for the given URL.
+   */
+  protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
+    $absolute_url = $url->setAbsolute()->toString();
+    sort($expected_contexts);
+    sort($expected_tags);
+
+    $get_cache_header_values = function ($header_name) {
+      $header_value = $this->drupalGetHeader($header_name);
+      if (empty($header_value)) {
+        return [];
+      }
+      else {
+        return explode(' ', $header_value);
+      }
+    };
+
+    // Assert cache miss + expected cache contexts + tags.
+    $this->drupalGet($absolute_url);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+    $actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
+    $actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
+    $this->assertIdentical($actual_contexts, $expected_contexts);
+    if ($actual_contexts !== $expected_contexts) {
+      debug(array_diff($actual_contexts, $expected_contexts));
+    }
+    $this->assertIdentical($actual_tags, $expected_tags);
+    if ($actual_tags !== $expected_tags) {
+      debug(array_diff($actual_tags, $expected_tags));
+    }
+
+    // Assert cache hit + expected cache contexts + tags.
+    $this->drupalGet($absolute_url);
+    $actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
+    $actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $this->assertIdentical($actual_contexts, $expected_contexts);
+    if ($actual_contexts !== $expected_contexts) {
+      debug(array_diff($actual_contexts, $expected_contexts));
+    }
+    $this->assertIdentical($actual_tags, $expected_tags);
+    if ($actual_tags !== $expected_tags) {
+      debug(array_diff($actual_tags, $expected_tags));
+    }
+
+    // Assert page cache item + expected cache tags.
+    $cid_parts = array($url->setAbsolute()->toString(), 'html');
+    $cid = implode(':', $cid_parts);
+    $cache_entry = \Drupal::cache('render')->get($cid);
+    sort($cache_entry->tags);
+    $this->assertEqual($cache_entry->tags, $expected_tags);
+    if ($cache_entry->tags !== $expected_tags) {
+      debug(array_diff($cache_entry->tags, $expected_tags));
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
index b56a6ba9e92a..da74be826d68 100644
--- a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
+++ b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -7,9 +7,7 @@
 
 namespace Drupal\system\Tests\Cache;
 
-use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
-use Drupal\Core\Cache\Cache;
 
 /**
  * Enables the page cache and tests its cache tags in various scenarios.
@@ -21,6 +19,8 @@
  */
 class PageCacheTagsIntegrationTest extends WebTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   protected $profile = 'standard';
 
   protected $dumpHeaders = TRUE;
@@ -31,10 +31,7 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $config = $this->config('system.performance');
-    $config->set('cache.page.use_internal', 1);
-    $config->set('cache.page.max_age', 300);
-    $config->save();
+    $this->enablePageCaching();
   }
 
   /**
@@ -128,46 +125,10 @@ function testPageCacheTags() {
       'config:system.menu.footer',
       'config:system.menu.main',
       'config:system.site',
+      'comment_list',
+      'node_list',
+      'config:views.view.comments_recent',
     ));
   }
 
-  /**
-   * Asserts page cache miss, then hit for the given URL; checks cache headers.
-   *
-   * @param \Drupal\Core\Url $url
-   *   The URL to test.
-   * @param string[] $expected_contexts
-   *   The expected cache contexts for the given URL.
-   * @param string[] $expected_tags
-   *   The expected cache tags for the given URL.
-   */
-  protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
-    $absolute_url = $url->setAbsolute()->toString();
-    sort($expected_contexts);
-    sort($expected_tags);
-
-    // Assert cache miss + expected cache contexts + tags.
-    $this->drupalGet($absolute_url);
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
-    $actual_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
-    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
-    $this->assertIdentical($actual_contexts, $expected_contexts);
-    $this->assertIdentical($actual_tags, $expected_tags);
-
-    // Assert cache hit + expected cache contexts + tags.
-    $this->drupalGet($absolute_url);
-    $actual_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
-    $actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
-    $this->assertIdentical($actual_contexts, $expected_contexts);
-    $this->assertIdentical($actual_tags, $expected_tags);
-
-    // Assert page cache item + expected cache tags.
-    $cid_parts = array($url->setAbsolute()->toString(), 'html');
-    $cid = implode(':', $cid_parts);
-    $cache_entry = \Drupal::cache('render')->get($cid);
-    sort($cache_entry->tags);
-    $this->assertEqual($cache_entry->tags, $expected_tags);
-  }
-
 }
diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
index f66d3e6f2282..b14c49605803 100644
--- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -191,7 +191,7 @@ public function cacheSet($type) {
         // that is used to render the view for this request and rendering does
         // not happen twice.
         $this->storage = $this->view->display_handler->output = $this->renderer->getCacheableRenderArray($output);
-        \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->getCacheTags());
+        \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), Cache::mergeTags($this->storage['#cache']['tags'], ['rendered']));
         break;
     }
   }
@@ -239,9 +239,6 @@ public function cacheGet($type) {
 
   /**
    * Clear out cached data for a view.
-   *
-   * We're just going to nuke anything related to the view, regardless of display,
-   * to be sure that we catch everything. Maybe that's a bad idea.
    */
   public function cacheFlush() {
     Cache::invalidateTags($this->view->storage->getCacheTags());
@@ -301,12 +298,18 @@ public function generateResultsKey() {
         'langcode' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
         'base_url' => $GLOBALS['base_url'],
       );
-      foreach (array('exposed_info', 'page', 'sort', 'order', 'items_per_page', 'offset') as $key) {
+      foreach (array('exposed_info', 'sort', 'order') as $key) {
         if ($this->view->getRequest()->query->has($key)) {
           $key_data[$key] = $this->view->getRequest()->query->get($key);
         }
       }
 
+      $key_data['pager'] = [
+        'page' => $this->view->getCurrentPage(),
+        'items_per_page' => $this->view->getItemsPerPage(),
+        'offset' => $this->view->getOffset(),
+      ];
+
       $this->resultsKey = $this->view->storage->id() . ':' . $this->displayHandler->display['id'] . ':results:' . hash('sha256', serialize($key_data));
     }
 
@@ -343,18 +346,21 @@ public function generateOutputKey() {
    * @return string[]
    *   An array of cache tags based on the current view.
    */
-  protected function getCacheTags() {
+  public function getCacheTags() {
     $tags = $this->view->storage->getCacheTags();
 
+    // The list cache tags for the entity types listed in this view.
     $entity_information = $this->view->query->getEntityTableInfo();
 
     if (!empty($entity_information)) {
       // Add the list cache tags for each entity type used by this view.
-      foreach (array_keys($entity_information) as $entity_type) {
-        $tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($entity_type)->getListCacheTags());
+      foreach ($entity_information as $table => $metadata) {
+        $tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($metadata['entity_type'])->getListCacheTags());
       }
     }
 
+    $tags = Cache::mergeTags($tags, $this->view->getQuery()->getCacheTags());
+
     return $tags;
   }
 
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index 4f1d3b3c3212..7cd1041a14b4 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -2126,6 +2126,15 @@ public function render() {
       '#post_render_cache' => &$this->view->element['#post_render_cache'],
     );
 
+    if (!isset($element['#cache'])) {
+      $element['#cache'] = [];
+    }
+    $element['#cache'] += ['tags' => []];
+
+    // If the output is a render array, add cache tags, regardless of whether
+    // caching is enabled or not; cache tags must always be set.
+    $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $this->view->getCacheTags());
+
     return $element;
   }
 
diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
index 1c254dc47810..a206d03225f5 100644
--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
+++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
@@ -312,6 +312,13 @@ public function getEntityTableInfo() {
     return $entity_tables;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return [];
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php
index 47d19799a413..1be46f982e14 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\views\query;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -1537,6 +1538,23 @@ function loadEntities(&$results) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    $tags = [];
+    // Add cache tags for each row, if there is an entity associated with it.
+    if (!$this->hasAggregate) {
+      foreach ($this->view->result as $row)  {
+        if ($row->_entity) {
+          $tags = Cache::mergeTags($row->_entity->getCacheTags(), $tags);
+        }
+      }
+    }
+
+    return $tags;
+  }
+
   public function addSignature(ViewExecutable $view) {
     $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
   }
diff --git a/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php b/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php
new file mode 100644
index 000000000000..1617175f0339
--- /dev/null
+++ b/core/modules/views/src/Tests/AssertViewsCacheTagsTrait.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\AssertViewsCacheTagsTrait.
+ */
+
+namespace Drupal\views\Tests;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\views\ViewExecutable;
+use Symfony\Component\HttpFoundation\Request;
+
+trait AssertViewsCacheTagsTrait {
+
+
+  /**
+   * Asserts a view's result & output cache items' cache tags.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The view to test, must have caching enabled.
+   * @param null|string[] $expected_results_cache
+   *   NULL when expecting no results cache item, a set of cache tags expected
+   *   to be set on the results cache item otherwise.
+   * @param bool $views_caching_is_enabled
+   *   Whether to expect an output cache item. If TRUE, the cache tags must
+   *   match those in $expected_render_array_cache_tags.
+   * @param string[] $expected_render_array_cache_tags
+   *   A set of cache tags expected to be set on the built view's render array.
+   *
+   * @return array
+   *   The render array
+   */
+  protected function assertViewsCacheTags(ViewExecutable $view, $expected_results_cache, $views_caching_is_enabled, array $expected_render_array_cache_tags) {
+    $build = $view->preview();
+
+    // Ensure the current request is a GET request so that render caching is
+    // active for direct rendering of views, just like for actual requests.
+    /** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
+    $request_stack = \Drupal::service('request_stack');
+    $request_stack->push(new Request());
+    \Drupal::service('renderer')->renderRoot($build);
+    $request_stack->pop();
+
+    // Render array cache tags.
+    $this->pass('Checking render array cache tags.');
+    sort($expected_render_array_cache_tags);
+    $this->assertEqual($build['#cache']['tags'], $expected_render_array_cache_tags);
+
+    if ($views_caching_is_enabled) {
+      $this->pass('Checking Views results cache item cache tags.');
+      /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
+      $cache_plugin = $view->display_handler->getPlugin('cache');
+
+      // Results cache.
+      $results_cache_item = \Drupal::cache('data')->get($cache_plugin->generateResultsKey());
+      if (is_array($expected_results_cache)) {
+        $this->assertTrue($results_cache_item, 'Results cache item found.');
+        if ($results_cache_item) {
+          sort($expected_results_cache);
+          $this->assertEqual($results_cache_item->tags, $expected_results_cache);
+        }
+      }
+      else {
+        $this->assertFalse($results_cache_item, 'Results cache item not found.');
+      }
+
+      // Output cache.
+      $this->pass('Checking Views output cache item cache tags.');
+      $output_cache_item = \Drupal::cache('render')->get($cache_plugin->generateOutputKey());
+      if ($views_caching_is_enabled === TRUE) {
+        $this->assertTrue($output_cache_item, 'Output cache item found.');
+        if ($output_cache_item) {
+          $this->assertEqual($output_cache_item->tags, Cache::mergeTags($expected_render_array_cache_tags, ['rendered']));
+        }
+      }
+      else {
+        $this->assertFalse($output_cache_item, 'Output cache item not found.');
+      }
+    }
+
+    $view->destroy();
+
+    return $build;
+  }
+
+}
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index c9cb84a1a961..553400970170 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Url;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\views\Views;
 
 /**
@@ -18,6 +19,9 @@
  */
 class GlossaryTest extends ViewTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+  use AssertViewsCacheTagsTrait;
+
   /**
    * Modules to enable.
    *
@@ -39,6 +43,7 @@ public function testGlossaryView() {
       'a' => 3,
       'l' => 6,
     );
+    $nodes_by_char = [];
     foreach ($nodes_per_char as $char => $count) {
       $setting = array(
         'type' => $type->id()
@@ -46,7 +51,8 @@ public function testGlossaryView() {
       for ($i = 0; $i < $count; $i++) {
         $node = $setting;
         $node['title'] = $char . $this->randomString(3);
-        $this->drupalCreateNode($node);
+        $node = $this->drupalCreateNode($node);
+        $nodes_by_char[$char][] = $node;
       }
     }
 
@@ -77,6 +83,16 @@ public function testGlossaryView() {
       $result_count = trim(str_replace(array('|', '(', ')'), '', (string) $result[0]));
       $this->assertEqual($result_count, $count, 'The expected number got rendered.');
     }
+
+    // Verify cache tags.
+    $this->enablePageCaching();
+    $this->assertPageCacheContextsAndTags(Url::fromRoute('view.glossary.page_1'), [], [
+      '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_list',
+      'rendered',
+    ]);
   }
 
 }
diff --git a/core/modules/views/src/Tests/Plugin/CacheTest.php b/core/modules/views/src/Tests/Plugin/CacheTest.php
index 913aab21eac6..d730abf89e59 100644
--- a/core/modules/views/src/Tests/Plugin/CacheTest.php
+++ b/core/modules/views/src/Tests/Plugin/CacheTest.php
@@ -44,7 +44,6 @@ protected function setUp() {
    * @see views_plugin_cache_time
    */
   public function testTimeResultCaching() {
-    // Create a basic result which just 2 results.
     $view = Views::getView('test_cache');
     $view->setDisplay();
     $view->display_handler->overrideOption('cache', array(
@@ -55,6 +54,7 @@ public function testTimeResultCaching() {
       )
     ));
 
+    // Test the default (non-paged) display.
     $this->executeView($view);
     // Verify the result.
     $this->assertEqual(5, count($view->result), 'The number of returned rows match.');
@@ -67,7 +67,19 @@ public function testTimeResultCaching() {
     );
     db_insert('views_test_data')->fields($record)->execute();
 
-    // The Result should be the same as before, because of the caching.
+    // The result should be the same as before, because of the caching. (Note
+    // that views_test_data records don't have associated cache tags, and hence
+    // the results cache items aren't invalidated.)
+    $view->destroy();
+    $this->executeView($view);
+    // Verify the result.
+    $this->assertEqual(5, count($view->result), 'The number of returned rows match.');
+  }
+
+  /**
+   * Tests result caching with a pager.
+   */
+  public function testTimeResultCachingWithPager() {
     $view = Views::getView('test_cache');
     $view->setDisplay();
     $view->display_handler->overrideOption('cache', array(
@@ -78,9 +90,31 @@ public function testTimeResultCaching() {
       )
     ));
 
+    $mapping = ['views_test_data_name' => 'name'];
+
+    $view->setDisplay('page_1');
+    $view->setCurrentPage(0);
     $this->executeView($view);
-    // Verify the result.
-    $this->assertEqual(5, count($view->result), 'The number of returned rows match.');
+    $this->assertIdenticalResultset($view, [['name' => 'John'], ['name' => 'George']], $mapping);
+    $view->destroy();
+
+    $view->setDisplay('page_1');
+    $view->setCurrentPage(1);
+    $this->executeView($view);
+    $this->assertIdenticalResultset($view, [['name' => 'Ringo'], ['name' => 'Paul']], $mapping);
+    $view->destroy();
+
+    $view->setDisplay('page_1');
+    $view->setCurrentPage(0);
+    $this->executeView($view);
+    $this->assertIdenticalResultset($view, [['name' => 'John'], ['name' => 'George']], $mapping);
+    $view->destroy();
+
+    $view->setDisplay('page_1');
+    $view->setCurrentPage(2);
+    $this->executeView($view);
+    $this->assertIdenticalResultset($view, [['name' => 'Meredith']], $mapping);
+    $view->destroy();
   }
 
   /**
@@ -149,7 +183,8 @@ function testHeaderStorage() {
     drupal_render($output);
     $this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.');
     $this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.');
-    $this->assertEqual(['views_test_data:1'], $output['#cache']['tags']);
+    // Note: views_test_data_views_pre_render() adds some cache tags.
+    $this->assertEqual(['config:views.view.test_cache_header_storage', 'views_test_data:1'], $output['#cache']['tags']);
     $this->assertEqual(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']);
     $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.');
   }
diff --git a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
new file mode 100644
index 000000000000..747e170b51c5
--- /dev/null
+++ b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php
@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\RenderCacheIntegrationTest.
+ */
+
+namespace Drupal\views\Tests;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\views\Views;
+
+/**
+ * Tests the general integration between Views and the render cache.
+ *
+ * @group views
+ */
+class RenderCacheIntegrationTest extends ViewUnitTestBase {
+
+  use AssertViewsCacheTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $testViews = ['entity_test_fields', 'entity_test_row'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('user');
+  }
+
+  /**
+   * Tests a field-based view's cache tags when using the "none" cache plugin.
+   */
+  public function testFieldBasedViewCacheTagsWithCachePluginNone() {
+    $this->assertCacheTagsForFieldBasedView(FALSE);
+  }
+
+  /**
+   * Tests a field-based view's cache tags when using the "tag" cache plugin.
+   */
+  public function testFieldBasedViewCacheTagsWithCachePluginTag() {
+    $view = Views::getview('entity_test_fields');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+    $view->save();
+
+    $this->assertCacheTagsForFieldBasedView(TRUE);
+  }
+
+  /**
+   * Tests a field-based view's cache tags when using the "time" cache plugin.
+   */
+  public function testFieldBasedViewCacheTagsWithCachePluginTime() {
+    $view = Views::getview('entity_test_fields');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'time',
+      'options' => [
+        'results_lifespan' => 3600,
+        'output_lifespan' => 3600,
+      ],
+    ]);
+    $view->save();
+
+    $this->assertCacheTagsForFieldBasedView(TRUE);
+  }
+
+  /**
+   * Tests cache tags on output & result cache items for a field-based view.
+   *
+   * @param bool $do_assert_views_caches
+   *   Whether to check Views' result & output caches.
+   */
+  protected function assertCacheTagsForFieldBasedView($do_assert_views_caches) {
+    $this->pass('Checking cache tags for field-based view.');
+    $view = Views::getview('entity_test_fields');
+
+    // Empty result (no entities yet).
+    $base_tags =  ['config:views.view.entity_test_fields', 'entity_test_list'];
+    $this->assertViewsCacheTags($view, $base_tags, $do_assert_views_caches, $base_tags);
+
+
+    // Non-empty result (1 entity).
+    $entities[] = $entity = EntityTest::create();
+    $entity->save();
+
+    $tags_with_entity = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
+    $this->assertViewsCacheTags($view, $tags_with_entity, $do_assert_views_caches, $tags_with_entity);
+
+
+    // Paged result (more entities than the items-per-page limit).
+    for ($i = 0; $i < 5; $i++) {
+      $entities[] = $entity = EntityTest::create();
+      $entity->save();
+    }
+    // Page 1.
+    $tags_page_1 = Cache::mergeTags($base_tags, $entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
+    $this->assertViewsCacheTags($view, $tags_page_1, $do_assert_views_caches, $tags_page_1);
+    $view->destroy();
+    // Page 2.
+    $view->setCurrentPage(1);
+    $tags_page_2 = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
+    $this->assertViewsCacheTags($view, $tags_page_2, $do_assert_views_caches, $tags_page_2);
+    $view->destroy();
+
+    // Ensure that invalidation works on both pages.
+    $view->setCurrentPage(1);
+    $entities[0]->name->value = $random_name = $this->randomMachineName();
+    $entities[0]->save();
+    $build = $this->assertViewsCacheTags($view, $tags_page_2, $do_assert_views_caches, $tags_page_2);
+    $this->assertTrue(strpos($build['#markup'], $random_name) !== FALSE);
+    $view->destroy();
+
+    $view->setCurrentPage(0);
+    $entities[1]->name->value = $random_name = $this->randomMachineName();
+    $entities[1]->save();
+    $build = $this->assertViewsCacheTags($view, $tags_page_1, $do_assert_views_caches, $tags_page_1);
+    $this->assertTrue(strpos($build['#markup'], $random_name) !== FALSE);
+  }
+
+  /**
+   * Tests a entity-based view's cache tags when using the "none" cache plugin.
+   */
+  public function testEntityBasedViewCacheTagsWithCachePluginNone() {
+    $this->assertCacheTagsForEntityBasedView(FALSE);
+  }
+
+  /**
+   * Tests a entity-based view's cache tags when using the "tag" cache plugin.
+   */
+  public function testEntityBasedViewCacheTagsWithCachePluginTag() {
+    $view = Views::getview('entity_test_row');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'tag',
+    ]);
+    $view->save();
+
+    $this->assertCacheTagsForEntityBasedView(TRUE);
+  }
+
+  /**
+   * Tests a entity-based view's cache tags when using the "time" cache plugin.
+   */
+  public function testEntityBasedViewCacheTagsWithCachePluginTime() {
+    $view = Views::getview('entity_test_row');
+    $view->getDisplay()->overrideOption('cache', [
+      'type' => 'time',
+      'options' => [
+        'results_lifespan' => 3600,
+        'output_lifespan' => 3600,
+      ],
+    ]);
+    $view->save();
+
+    $this->assertCacheTagsForEntityBasedView(TRUE);
+  }
+
+  /**
+   * Tests cache tags on output & result cache items for an entity-based view.
+   */
+  protected function assertCacheTagsForEntityBasedView($do_assert_views_caches) {
+    $this->pass('Checking cache tags for entity-based view.');
+    $view = Views::getview('entity_test_row');
+
+    // Empty result (no entities yet).
+    $base_tags = $base_render_tags = ['config:views.view.entity_test_row', 'entity_test_list'];
+    $this->assertViewsCacheTags($view, $base_tags, $do_assert_views_caches, $base_tags);
+
+
+    // Non-empty result (1 entity).
+    $entities[] = $entity = EntityTest::create();
+    $entity->save();
+
+    $result_tags_with_entity = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
+    $render_tags_with_entity = Cache::mergeTags($base_render_tags, $entities[0]->getCacheTags(), ['entity_test_view']);
+    $this->assertViewsCacheTags($view, $result_tags_with_entity, $do_assert_views_caches, $render_tags_with_entity);
+
+
+    // Paged result (more entities than the items-per-page limit).
+    for ($i = 0; $i < 5; $i++) {
+      $entities[] = $entity = EntityTest::create();
+      $entity->save();
+    }
+
+    $new_entities_cache_tags = Cache::mergeTags($entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
+    $result_tags_page_1 = Cache::mergeTags($base_tags, $new_entities_cache_tags);
+    $render_tags_page_1 = Cache::mergeTags($base_render_tags, $new_entities_cache_tags, ['entity_test_view']);
+    $this->assertViewsCacheTags($view, $result_tags_page_1, $do_assert_views_caches, $render_tags_page_1);
+  }
+
+}
diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php
index 0eb445d4eb65..6350f0d4e73d 100644
--- a/core/modules/views/src/ViewExecutable.php
+++ b/core/modules/views/src/ViewExecutable.php
@@ -8,6 +8,7 @@
 namespace Drupal\views;
 
 use Drupal\Component\Utility\String;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Routing\RouteProviderInterface;
@@ -1398,6 +1399,7 @@ public function render($display_id = NULL) {
       }
 
       $this->display_handler->output = $this->display_handler->render();
+
       if ($cache) {
         $cache->cacheSet('output');
       }
@@ -1423,6 +1425,22 @@ public function render($display_id = NULL) {
     return $this->display_handler->output;
   }
 
+  /**
+   * Gets the cache tags associated with the executed view.
+   *
+   * Note: The cache plugin controls the used tags, so you can override it, if
+   *   needed.
+   *
+   * @return string[]
+   *   An array of cache tags.
+   */
+  public function getCacheTags() {
+    $this->initDisplay();
+    /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
+    $cache = $this->display_handler->getPlugin('cache');
+    return $cache->getCacheTags();
+  }
+
   /**
    * Builds the render array outline for the given display.
    *
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml
new file mode 100644
index 000000000000..8966bb80c458
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields.yml
@@ -0,0 +1,74 @@
+langcode: und
+status: true
+dependencies: {  }
+id: entity_test_fields
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: entity_test
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      access:
+        type: none
+      cache:
+        type: none
+      exposed_form:
+        type: basic
+      fields:
+        id:
+          alter:
+            alter_text: false
+          element_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          plugin_id: numeric
+          entity_type: entity_test
+          entity_field: id
+          id: id
+          table: entity_test
+          field: id
+        name:
+          alter:
+            alter_text: false
+            ellipsis: true
+            html: false
+            make_link: false
+            strip_tags: false
+            trim: false
+            word_boundary: true
+          empty_zero: false
+          field: name
+          hide_empty: false
+          id: name
+          table: entity_test
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: name
+      sorts:
+        id:
+          table: entity_test
+          id: id
+          field: id
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: id
+          order: desc
+      pager:
+        type: full
+        options:
+          items_per_page: 5
+      style:
+        type: default
+      row:
+        type: fields
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml
new file mode 100644
index 000000000000..2384fe054766
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_row.yml
@@ -0,0 +1,41 @@
+langcode: und
+status: true
+dependencies: {  }
+id: entity_test_row
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: entity_test
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      access:
+        type: none
+      cache:
+        type: none
+      exposed_form:
+        type: basic
+      sorts:
+        id:
+          table: entity_test
+          id: id
+          field: id
+          plugin_id: standard
+          entity_type: entity_test
+          entity_field: id
+          order: desc
+      pager:
+        type: full
+        options:
+          items_per_page: 5
+      style:
+        type: default
+      row:
+        type: 'entity:entity_test'
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_cache.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_cache.yml
index 82908d8ca483..6dd5fc522f93 100644
--- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_cache.yml
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_cache.yml
@@ -42,3 +42,15 @@ display:
           table: views_test_data
           field: id
           relationship: none
+
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_options:
+      defaults:
+        pager: false
+      pager:
+        type: full
+        options:
+          items_per_page: 2
+
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index b22ed7ead15a..929a307253f8 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -113,7 +113,7 @@ public function providerTestContextBubblingEdgeCases() {
         '#attached' => [],
         '#cache' => [
           'contexts' => ['foo'],
-          'tags' => ['rendered'],
+          'tags' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -141,7 +141,7 @@ public function providerTestContextBubblingEdgeCases() {
         '#attached' => [],
         '#cache' => [
           'contexts' => [],
-          'tags' => ['rendered'],
+          'tags' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -164,7 +164,7 @@ public function providerTestContextBubblingEdgeCases() {
         '#attached' => [],
         '#cache' => [
           'contexts' => [],
-          'tags' => ['rendered'],
+          'tags' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => '',
@@ -204,7 +204,7 @@ public function providerTestContextBubblingEdgeCases() {
         '#attached' => [],
         '#cache' => [
           'contexts' => ['bar', 'baz', 'foo'],
-          'tags' => ['rendered'],
+          'tags' => [],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -243,17 +243,14 @@ public function providerTestContextBubblingEdgeCases() {
           // The keys + contexts this redirects to.
           'keys' => ['parent'],
           'contexts' => ['bar', 'foo'],
-          // The 'rendered' cache tag is also present for the redirecting cache
-          // item, to ensure it is considered to be part of the render cache
-          // and thus invalidated along with everything else.
-          'tags' => ['dee', 'fiddle', 'har', 'rendered', 'yar'],
+          'tags' => ['dee', 'fiddle', 'har', 'yar'],
         ],
       ],
       'parent:bar:foo' => [
         '#attached' => [],
         '#cache' => [
           'contexts' => ['bar', 'foo'],
-          'tags' => ['dee', 'fiddle', 'har', 'yar', 'rendered'],
+          'tags' => ['dee', 'fiddle', 'har', 'yar'],
         ],
         '#post_render_cache' => [],
         '#markup' => 'parent',
@@ -334,14 +331,14 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#cache' => [
         'keys' => ['parent'],
         'contexts' => ['user.roles'],
-        'tags' => ['a', 'b', 'rendered'],
+        'tags' => ['a', 'b'],
       ],
     ]);
     $this->assertRenderCacheItem('parent:r.A', [
       '#attached' => [],
       '#cache' => [
         'contexts' => ['user.roles'],
-        'tags' => ['a', 'b', 'rendered'],
+        'tags' => ['a', 'b'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -357,14 +354,14 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#cache' => [
         'keys' => ['parent'],
         'contexts' => ['foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'rendered'],
+        'tags' => ['a', 'b', 'c'],
       ],
     ]);
     $this->assertRenderCacheItem('parent:foo:r.B', [
       '#attached' => [],
       '#cache' => [
         'contexts' => ['foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'rendered'],
+        'tags' => ['a', 'b', 'c'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -388,14 +385,14 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#cache' => [
         'keys' => ['parent'],
         'contexts' => ['foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'rendered'],
+        'tags' => ['a', 'b', 'c'],
       ],
     ]);
     $this->assertRenderCacheItem('parent:foo:r.A', [
       '#attached' => [],
       '#cache' => [
         'contexts' => ['foo', 'user.roles'],
-        'tags' => ['a', 'b', 'rendered'],
+        'tags' => ['a', 'b'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -411,7 +408,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#cache' => [
         'keys' => ['parent'],
         'contexts' => ['bar', 'foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'd', 'rendered'],
+        'tags' => ['a', 'b', 'c', 'd'],
       ],
     ];
     $this->assertRenderCacheItem('parent', $final_parent_cache_item);
@@ -419,7 +416,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#attached' => [],
       '#cache' => [
         'contexts' => ['bar', 'foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'd', 'rendered'],
+        'tags' => ['a', 'b', 'c', 'd'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -434,7 +431,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#attached' => [],
       '#cache' => [
         'contexts' => ['bar', 'foo', 'user.roles'],
-        'tags' => ['a', 'b', 'rendered'],
+        'tags' => ['a', 'b'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
@@ -449,7 +446,7 @@ public function testConditionalCacheContextBubblingSelfHealing() {
       '#attached' => [],
       '#cache' => [
         'contexts' => ['bar', 'foo', 'user.roles'],
-        'tags' => ['a', 'b', 'c', 'rendered'],
+        'tags' => ['a', 'b', 'c'],
       ],
       '#post_render_cache' => [],
       '#markup' => 'parent',
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
index 46a49be9f48a..da9f2327748d 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php
@@ -91,7 +91,7 @@ public function testPostRenderCacheWithColdCache() {
       '#post_render_cache' => $test_element['#post_render_cache'],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -227,7 +227,7 @@ public function testRenderChildrenPostRenderCacheDifferentContexts() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
 
@@ -322,7 +322,7 @@ public function testRenderChildrenPostRenderCacheComplex() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
 
@@ -348,7 +348,7 @@ public function testRenderChildrenPostRenderCacheComplex() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
 
@@ -462,7 +462,7 @@ public function testPlaceholder() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -560,7 +560,7 @@ public function testChildElementPlaceholder() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -588,7 +588,7 @@ public function testChildElementPlaceholder() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@@ -619,7 +619,7 @@ public function testChildElementPlaceholder() {
       ],
       '#cache' => [
         'contexts' => [],
-        'tags' => ['rendered'],
+        'tags' => [],
       ],
     ];
     $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 455834eb2f2c..ac2925c655c1 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Render;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
 
@@ -567,9 +568,12 @@ public function testRenderCache() {
       'render_cache_tag',
       'render_cache_tag_child:1',
       'render_cache_tag_child:2',
-      'rendered',
     ];
     $this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.');
+
+    // The cache item also has a 'rendered' cache tag.
+    $cache_item = $this->cacheFactory->get('render')->get('render_cache_test');
+    $this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index cf085545cb0c..b4320c955605 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Render;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Renderer;
@@ -152,6 +153,7 @@ protected function assertRenderCacheItem($cid, $data) {
     $this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', $cid));
     if ($cached !== FALSE) {
       $this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', $cid));
+      $this->assertSame(Cache::mergeTags($data['#cache']['tags'], ['rendered']), $cached->tags, "The cache item's cache tags also has the 'rendered' cache tag.");
     }
   }
 
-- 
GitLab