 .../Core/Entity/Entity/EntityFormDisplay.php       |  2 +-
 core/lib/Drupal/Core/Form/FormBuilder.php          |  3 --
 core/lib/Drupal/Core/Render/Renderer.php           | 25 ++++++------
 .../ckeditor/src/Tests/CKEditorLoadingTest.php     |  2 +
 .../Tests/CommentDefaultFormatterCacheTagsTest.php |  8 ++--
 core/modules/contact/src/MessageForm.php           |  3 ++
 .../src/Tests/ContactAuthenticatedUserTest.php     | 14 ++++++-
 core/modules/editor/src/Element.php                |  2 +
 .../modules/editor/src/Tests/EditorLoadingTest.php |  2 +
 .../editor/src/Tests/EditorSecurityTest.php        |  2 +
 core/modules/filter/src/Element/TextFormat.php     |  3 ++
 core/modules/language/language.module              |  6 ++-
 .../language/src/Element/LanguageConfiguration.php |  2 +
 .../src/Entity/ContentLanguageSettings.php         | 14 +++++++
 .../src/Tests/LanguageConfigurationElementTest.php |  1 +
 .../node/src/Controller/NodePreviewController.php  |  4 +-
 core/modules/node/src/NodeForm.php                 |  3 ++
 core/modules/node/src/Plugin/Search/NodeSearch.php |  5 +++
 .../node/src/Tests/NodeTypeInitialLanguageTest.php |  2 +
 .../src/Tests/SearchAdvancedSearchFormTest.php     |  1 +
 .../src/Tests/Entity/EntityTranslationFormTest.php |  2 +
 .../Drupal/Tests/Core/Form/FormBuilderTest.php     | 45 ++++++++++++++--------
 .../Drupal/Tests/Core/Render/RendererTest.php      | 20 ++++++++++
 23 files changed, 132 insertions(+), 39 deletions(-)

diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
index bee0144..cd55751 100644
--- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
+++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
@@ -171,7 +171,7 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt
         $items = $entity->get($name);
         $items->filterEmptyItems();
         $form[$name] = $widget->form($items, $form, $form_state);
-        $form[$name]['#access'] = $items->access('edit');
+        $form[$name]['#access'] = $items->access('edit', NULL, TRUE);
 
         // Assign the correct weight. This duplicates the reordering done in
         // processForm(), but is needed for other forms calling this method
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index bb7034f..6f31650 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -769,9 +769,6 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
               ]
             ]
           ],
-          '#cache' => [
-            'max-age' => 0,
-          ],
         );
       }
     }
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 9fc7053..a858746 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -214,6 +214,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       return '';
     }
 
+    // Do not print elements twice.
+    if (!empty($elements['#printed'])) {
+      return '';
+    }
+
+    $context = $this->getCurrentRenderContext();
+    if (!isset($context)) {
+      throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
+    }
+    $context->push(new BubbleableMetadata());
+
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
       if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
         $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
@@ -228,25 +239,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       if ($elements['#access'] instanceof AccessResultInterface) {
         $this->addCacheableDependency($elements, $elements['#access']);
         if (!$elements['#access']->isAllowed()) {
+          $context->update($elements);
+          $context->bubble();
           return '';
         }
       }
       elseif ($elements['#access'] === FALSE) {
+        $context->pop();
         return '';
       }
     }
 
-    // Do not print elements twice.
-    if (!empty($elements['#printed'])) {
-      return '';
-    }
-
-    $context = $this->getCurrentRenderContext();
-    if (!isset($context)) {
-      throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
-    }
-    $context->push(new BubbleableMetadata());
-
     // Set the bubbleable rendering metadata that has configurable defaults, if:
     // - this is the root call, to ensure that the final render array definitely
     //   has these configurable defaults, even when no subtree is render cached.
diff --git a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
index 1081eb9..92e694e 100644
--- a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
+++ b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\ckeditor\Tests;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -126,6 +127,7 @@ function testLoading() {
     // configuration also results in modified CKEditor configuration, so we
     // don't test that here.
     \Drupal::service('module_installer')->install(array('ckeditor_test'));
+    Cache::invalidateTags(['rendered']);
     $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
     $editor_settings = $editor->getSettings();
     $editor_settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
index 1164ba0..074df52 100644
--- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
+++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
@@ -89,6 +89,7 @@ public function testCacheTags() {
       'config:field.field.comment.comment.comment_body',
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
+      'config:filter_format_list',
       'config:user.settings',
     ];
     sort($expected_cache_tags);
@@ -135,6 +136,7 @@ public function testCacheTags() {
       'config:field.field.comment.comment.comment_body',
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
+      'config:filter_format_list',
       'config:user.settings',
     ];
     sort($expected_cache_tags);
@@ -149,10 +151,10 @@ public function testCacheTags() {
       ->view($commented_entity);
     $renderer->renderRoot($build);
 
-    // The entity itself was cached but the top-level element is max-age 0 due
-    // to the bubbled up max age due to the lazy-built comment form.
+    // The entity itself was cached, and the the top-level element also has a
+    // non-zero max-age despite the lazy-built form.
     $this->assertIdentical(Cache::PERMANENT, $build['entity']['#cache']['max-age']);
-    $this->assertIdentical(0, $build['#cache']['max-age'], 'Top level render array has max-age 0');
+    $this->assertIdentical(Cache::PERMANENT, $build['#cache']['max-age'], 'Comment form is cacheable.');
 
     // The children (fields) of the entity render array are only built in case
     // of a cache miss.
diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php
index 46bb5d9..ac435ae 100644
--- a/core/modules/contact/src/MessageForm.php
+++ b/core/modules/contact/src/MessageForm.php
@@ -118,6 +118,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Your email address'),
       '#required' => TRUE,
     );
+    $form['#cache']['contexts'][] = 'user.roles:authenticated';
     if ($user->isAnonymous()) {
       $form['#attached']['library'][] = 'core/drupal.form';
       $form['#attributes']['data-user-info-from-browser'] = TRUE;
@@ -129,11 +130,13 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['name']['#value'] = $user->getUsername();
       $form['name']['#required'] = FALSE;
       $form['name']['#plain_text'] = $user->getUsername();
+      $form['name']['#cache']['contexts'][] = 'user';
 
       $form['mail']['#type'] = 'item';
       $form['mail']['#value'] = $user->getEmail();
       $form['mail']['#required'] = FALSE;
       $form['mail']['#plain_text'] = $user->getEmail();
+      $form['mail']['#cache']['contexts'][] = 'user';
     }
 
     // The user contact form has a preset recipient.
diff --git a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
index 2adc4c2..923878e 100644
--- a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
+++ b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
@@ -21,19 +21,29 @@ class ContactAuthenticatedUserTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('contact');
+  public static $modules = array('contact', 'contact_test');
 
   /**
    * Tests that name and email fields are not present for authenticated users.
    */
   function testContactSiteWideTextfieldsLoggedInTestCase() {
-    $this->drupalLogin($this->drupalCreateUser(array('access site-wide contact form')));
+    $user1 = $this->drupalCreateUser(array('access site-wide contact form'));
+    $this->drupalLogin($user1);
     $this->drupalGet('contact');
+    $this->assertResponse(200);
+    $this->assertCacheContext('user');
 
     // Ensure that there is no textfield for name.
     $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'name')));
+    $this->assertRaw($user1->getAccountName());
 
     // Ensure that there is no textfield for email.
     $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'mail')));
+
+    // Log in as a different user and confirm that
+    $user2 = $this->drupalCreateUser(array('access site-wide contact form'));
+    $this->drupalLogin($user2);
+    $this->drupalGet('contact');
+    $this->assertRaw($user2->getAccountName());
   }
 }
diff --git a/core/modules/editor/src/Element.php b/core/modules/editor/src/Element.php
index 43b99ba..4af65a1 100644
--- a/core/modules/editor/src/Element.php
+++ b/core/modules/editor/src/Element.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\editor;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\editor\Entity\Editor;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\Component\Plugin\PluginManagerInterface;
@@ -53,6 +54,7 @@ function preRenderTextFormat(array $element) {
     $format_ids = array_keys($element['format']['format']['#options']);
 
     // Early-return if no text editor is associated with any of the text formats.
+    $element['#cache']['tags'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('editor')->getListCacheTags());
     $editors = Editor::loadMultiple($format_ids);
     foreach ($editors as $key => $editor) {
       $definition = $this->pluginManager->getDefinition($editor->getEditor());
diff --git a/core/modules/editor/src/Tests/EditorLoadingTest.php b/core/modules/editor/src/Tests/EditorLoadingTest.php
index 002ccd3..95dda17 100644
--- a/core/modules/editor/src/Tests/EditorLoadingTest.php
+++ b/core/modules/editor/src/Tests/EditorLoadingTest.php
@@ -245,6 +245,7 @@ public function testSupportedElementTypes() {
     // Assert the unicorn editor works with textfields.
     $this->drupalLogin($this->privilegedUser);
     $this->drupalGet('node/1/edit');
+    $this->assertCacheTag('config:editor_list');
     list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
     $this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
     $this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
@@ -261,6 +262,7 @@ public function testSupportedElementTypes() {
     ))->save();
 
     $this->drupalGet('node/1/edit');
+    $this->assertCacheTag('config:editor_list');
     list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
     $this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
     $this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
diff --git a/core/modules/editor/src/Tests/EditorSecurityTest.php b/core/modules/editor/src/Tests/EditorSecurityTest.php
index bb39450..db15b05 100644
--- a/core/modules/editor/src/Tests/EditorSecurityTest.php
+++ b/core/modules/editor/src/Tests/EditorSecurityTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\editor\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -428,6 +429,7 @@ function testEditorXssFilterOverride() {
     // Enable editor_test.module's hook_editor_xss_filter_alter() implementation
     // to alter the text editor XSS filter class being used.
     \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
+    Cache::invalidateTags(['rendered']);
 
     // First: the Insecure text editor XSS filter.
     $this->drupalGet('node/2/edit');
diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php
index 9247dba..08ec542 100644
--- a/core/modules/filter/src/Element/TextFormat.php
+++ b/core/modules/filter/src/Element/TextFormat.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Element;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Core\Render\Element;
@@ -210,6 +211,8 @@ public static function processFormat(&$element, FormStateInterface $form_state,
     $user_has_access = isset($formats[$element['#format']]);
     $user_is_admin = $user->hasPermission('administer filters');
 
+    $element['#cache']['tags'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('filter_format')->getListCacheTags());
+
     // If the stored format does not exist or if it is not among the allowed
     // formats for this textarea, administrators have to assign a new format.
     if ((!$format_exists || !$format_allowed) && $user_is_admin) {
diff --git a/core/modules/language/language.module b/core/modules/language/language.module
index e92baa2..750c36a 100644
--- a/core/modules/language/language.module
+++ b/core/modules/language/language.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\Entity\EntityFormInterface;
 use Drupal\Core\Entity\EntityInterface;
@@ -157,6 +158,7 @@ function language_process_language_select($element) {
     foreach (\Drupal::languageManager()->getLanguages($element['#languages']) as $langcode => $language) {
       $element['#options'][$langcode] = $language->isLocked() ? t('- @name -', array('@name' => $language->getName())) : $language->getName();
     }
+    $element['#cache']['tags'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('language_content_settings')->getListCacheTags());
   }
   return $element;
 }
@@ -407,7 +409,7 @@ function language_form_alter(&$form, FormStateInterface $form_state) {
     $langcode_key = $entity_type->getKey('langcode');
     if (isset($form[$langcode_key])) {
       $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
-      $form[$langcode_key]['#access'] = $language_configuration->isLanguageAlterable();
+      $form[$langcode_key]['#access'] = AccessResult::allowedIf($language_configuration->isLanguageAlterable())->addCacheableDependency($language_configuration);
     }
   }
 }
@@ -434,7 +436,7 @@ function language_entity_field_access($operation, FieldDefinitionInterface $fiel
       // Grant access depending on whether the entity language can be altered.
       $entity = $items->getEntity();
       $config = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
-      return AccessResult::forbiddenIf(!$config->isLanguageAlterable());
+      return AccessResult::forbiddenIf(!$config->isLanguageAlterable())->addCacheableDependency($config);
     }
   }
   return AccessResult::neutral();
diff --git a/core/modules/language/src/Element/LanguageConfiguration.php b/core/modules/language/src/Element/LanguageConfiguration.php
index 2fad091..a7e0f1e 100644
--- a/core/modules/language/src/Element/LanguageConfiguration.php
+++ b/core/modules/language/src/Element/LanguageConfiguration.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\language\Element;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\Element\FormElement;
@@ -56,6 +57,7 @@ public static function processLanguageConfiguration(&$element, FormStateInterfac
       '#title' => t('Show language selector on create and edit pages'),
       '#default_value' => ($default_config != NULL) ? $default_config->isLanguageAlterable() : FALSE,
     );
+    $element['#cache']['tags'] = Cache::mergeTags(isset($element['#cache']['tags']) ? $element['#cache']['tags'] : [], \Drupal::entityManager()->getDefinition('language_content_settings')->getListCacheTags());
 
     // Add the entity type and bundle information to the form if they are set.
     // They will be used, in the submit handler, to generate the names of the
diff --git a/core/modules/language/src/Entity/ContentLanguageSettings.php b/core/modules/language/src/Entity/ContentLanguageSettings.php
index 7501012..171c8d7 100644
--- a/core/modules/language/src/Entity/ContentLanguageSettings.php
+++ b/core/modules/language/src/Entity/ContentLanguageSettings.php
@@ -203,4 +203,18 @@ public function calculateDependencies() {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTagsToInvalidate() {
+    // Because ::loadByEntityTypeBundle() auto-creates config entities on demand
+    // without saving them, we cannot rely on individual cache tags anywhere,
+    // because they're cache tags for ephemeral config entities. Therefore, the
+    // only cache tag we can rely on for this config entity, is its list cache
+    // tag.
+    // @todo Remove this once ::loadByEntityTypeBundle() no longer creates
+    // ephemeral (unsaved) ContentLanguageSettings config entities.
+    return $this->getEntityType()->getListCacheTags();
+  }
+
 }
diff --git a/core/modules/language/src/Tests/LanguageConfigurationElementTest.php b/core/modules/language/src/Tests/LanguageConfigurationElementTest.php
index 9591618..95d523a 100644
--- a/core/modules/language/src/Tests/LanguageConfigurationElementTest.php
+++ b/core/modules/language/src/Tests/LanguageConfigurationElementTest.php
@@ -37,6 +37,7 @@ protected function setUp() {
    */
   public function testLanguageConfigurationElement() {
     $this->drupalGet('language-tests/language_configuration_element');
+    $this->assertCacheTag('config:language_content_settings_list');
     $edit['lang_configuration[langcode]'] = 'current_interface';
     $edit['lang_configuration[language_alterable]'] = FALSE;
     $this->drupalPostForm(NULL, $edit, 'Save');
diff --git a/core/modules/node/src/Controller/NodePreviewController.php b/core/modules/node/src/Controller/NodePreviewController.php
index b1009be..540adc6 100644
--- a/core/modules/node/src/Controller/NodePreviewController.php
+++ b/core/modules/node/src/Controller/NodePreviewController.php
@@ -25,7 +25,9 @@ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $lan
     $build['#attached']['library'][] = 'node/drupal.node.preview';
 
     // Don't render cache previews.
-    unset($build['#cache']);
+    $build['#cache'] = [
+      'max-age' => 0
+    ];
 
     foreach ($node_preview->uriRelationships() as $rel) {
       // Set the node path as the canonical URL to prevent duplicate content.
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index bb6c49d..6d10f15 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -75,6 +75,9 @@ public function form(array $form, FormStateInterface $form_state) {
     $uuid = $this->entity->uuid();
     $store = $this->tempStoreFactory->get('node_preview');
 
+    // Because of the temp store integration, this is not cacheable.
+    $form['#cache']['max-age'] = 0;
+
     // If the user is creating a new node, the UUID is passed in the request.
     if ($request_uuid = \Drupal::request()->query->get('uuid')) {
       $uuid = $request_uuid;
diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php
index 0fa464e..0e2e226 100644
--- a/core/modules/node/src/Plugin/Search/NodeSearch.php
+++ b/core/modules/node/src/Plugin/Search/NodeSearch.php
@@ -530,6 +530,11 @@ public function searchFormAlter(array &$form, FormStateInterface $form_state) {
       '#attributes' => array('class' => array('search-advanced')),
       '#access' => $this->account && $this->account->hasPermission('use advanced search'),
       '#open' => $used_advanced,
+      '#cache' => [
+        'contexts' => [
+          'url.query_args',
+        ],
+      ],
     );
     $form['advanced']['keywords-fieldset'] = array(
       '#type' => 'fieldset',
diff --git a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
index f7df02f..0614937 100644
--- a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
+++ b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
@@ -47,6 +47,7 @@ function testNodeTypeInitialLanguageDefaults() {
     $this->assert(empty($language_field), 'Language field is not visible on manage fields tab.');
 
     $this->drupalGet('node/add/article');
+    $this->assertCacheTag('config:language_content_settings_list');
     $this->assertNoField('langcode', 'Language is not selectable on node add/edit page by default.');
 
     // Adds a new language and set it as default.
@@ -66,6 +67,7 @@ function testNodeTypeInitialLanguageDefaults() {
     );
     $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type'));
     $this->drupalGet('node/add/article');
+    $this->assertCacheTag('config:language_content_settings_list');
     $this->assertField('langcode[0][value]', 'Language is selectable on node add/edit page when language not hidden.');
     $this->assertOptionSelected('edit-langcode-0-value', 'hu', 'The initial language is the site default on the node add page after the site default language is changed.');
 
diff --git a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php
index 3650a93..278718c 100644
--- a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php
+++ b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php
@@ -59,6 +59,7 @@ function testNodeType() {
     // Search for the title of the node with a POST query.
     $edit = array('or' => $this->node->label());
     $this->drupalPostForm('search/node', $edit, t('Advanced search'));
+    $this->assertCacheContext('url.query_args');
     $this->assertText($this->node->label(), 'Basic page node is found with POST query.');
 
     // Search by node type.
diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php
index 805f682..b216730 100644
--- a/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityTranslationFormTest.php
@@ -59,6 +59,7 @@ function testEntityFormLanguage() {
     $edit['title[0][value]'] = $this->randomMachineName(8);
     $edit['body[0][value]'] = $this->randomMachineName(16);
     $this->drupalGet('node/add/page');
+    $this->assertCacheTag('config:language_content_settings_list');
     $form_langcode = \Drupal::state()->get('entity_test.form_langcode');
     $this->drupalPostForm(NULL, $edit, t('Save'));
 
@@ -68,6 +69,7 @@ function testEntityFormLanguage() {
 
     // Edit the node and test the form language.
     $this->drupalGet($this->langcodes[0] . '/node/' . $node->id() . '/edit');
+    $this->assertCacheTag('config:language_content_settings_list');
     $form_langcode = \Drupal::state()->get('entity_test.form_langcode');
     $this->assertTrue($node->language()->getId() == $form_langcode, 'Form language is the same as the entity language.');
 
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 7bd51bc..26f6ea6 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -817,7 +817,7 @@ public function providerTestInvalidToken() {
    *
    * @dataProvider providerTestFormTokenCacheability
    */
-  public function testFormTokenCacheability($token, $is_authenticated, $expected_form_cacheability, $expected_token_cacheability, $method) {
+  public function testFormTokenCacheability($token, $is_authenticated, $method) {
     $user = $this->prophesize(AccountProxyInterface::class);
     $user->isAuthenticated()
       ->willReturn($is_authenticated);
@@ -842,19 +842,32 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f
 
     $form_state = new FormState();
     $built_form = $this->formBuilder->buildForm($form_arg, $form_state);
-    if (!isset($expected_form_cacheability) || ($method == 'get' && !is_string($token))) {
+
+    // FormBuilder does not even consider to set a form token when:
+    // - #token = FALSE (opting out explicitly)
+    // - #method = GET and #token is not set to a string (GET forms don't get a
+    //   form token by default, and this form did not explicitly opt in)
+    if ($token === FALSE || ($method == 'get' && !is_string($token))) {
       $this->assertFalse(isset($built_form['#cache']));
-    }
-    else {
-      $this->assertTrue(isset($built_form['#cache']));
-      $this->assertEquals($expected_form_cacheability, $built_form['#cache']);
-    }
-    if (!isset($expected_token_cacheability)) {
       $this->assertFalse(isset($built_form['form_token']));
     }
+    // Otherwise, a form token is set, but only if the user is logged in. It is
+    // impossible (and unnecessary) to set a form token if the user is not
+    // logged in, because there is no session, and hence no CSRF token.
     else {
-      $this->assertTrue(isset($built_form['form_token']));
-      $this->assertEquals($expected_token_cacheability, $built_form['form_token']['#cache']);
+      // For forms that are eligible for form tokens, a cache context must be
+      // set that indicates the form token only exists for logged in users.
+      $this->assertTrue(isset($built_form['#cache']));
+      $this->assertEquals(['contexts' => ['user.roles:authenticated']], $built_form['#cache']);
+      // Finally, verify that a form token is generated when appropriate, with
+      // the expected cacheability metadata (or lack thereof).
+      if (!$is_authenticated) {
+        $this->assertFalse(isset($built_form['form_token']));
+      }
+      else {
+        $this->assertTrue(isset($built_form['form_token']));
+        $this->assertFalse(isset($built_form['form_token']['#cache']));
+      }
     }
   }
 
@@ -865,12 +878,12 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f
    */
   function providerTestFormTokenCacheability() {
     return [
-      'token:none,authenticated:true' => [NULL, TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'post'],
-      'token:none,authenticated:false' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'post'],
-      'token:false,authenticated:false' => [FALSE, FALSE, NULL, NULL, 'post'],
-      'token:false,authenticated:true' => [FALSE, TRUE, NULL, NULL, 'post'],
-      'token:none,authenticated:false,method:get' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'get'],
-      'token:test_form_id,authenticated:false,method:get' => ['test_form_id', TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'get'],
+      'token:none,authenticated:true' => [NULL, TRUE, 'post'],
+      'token:none,authenticated:false' => [NULL, FALSE, 'post'],
+      'token:false,authenticated:false' => [FALSE, FALSE, 'post'],
+      'token:false,authenticated:true' => [FALSE, TRUE, 'post'],
+      'token:none,authenticated:false,method:get' => [NULL, FALSE, 'get'],
+      'token:test_form_id,authenticated:true,method:get' => ['test_form_id', TRUE, 'get'],
     ];
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 48b394e..0a35c17 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -489,7 +489,27 @@ public function testRenderAccessCacheablityDependencyInheritance() {
     $build = [
       '#access' => AccessResult::allowed()->addCacheContexts(['user']),
     ];
+    $this->renderer->renderPlain($build);
 
+    $build = [
+      '#access' => AccessResult::forbidden()->addCacheContexts(['user']),
+    ];
+    $this->renderer->renderPlain($build);
+
+    // Simulate the theme system/Twig: a recursive call to Renderer::render(),
+    // just like the theme system or a Twig template would have done.
+    $this->themeManager->expects($this->any())
+      ->method('render')
+      ->willReturnCallback(function ($hook, $vars) {
+        return $this->renderer->render($vars['child']);
+      });
+
+    $build = [
+      '#theme' => 'something',
+      'child' => [
+        '#access' => AccessResult::forbidden()->addCacheContexts(['user']),
+      ],
+    ];
     $this->renderer->renderPlain($build);
 
     $this->assertEquals(['languages:language_interface', 'theme', 'user'], $build['#cache']['contexts']);
