diff --git a/core/modules/node/src/Tests/NodeRevisionsTest.php b/core/modules/node/src/Tests/NodeRevisionsTest.php
new file mode 100644
index 000000000..fdc929a84
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeRevisionsTest.php
@@ -0,0 +1,471 @@
+save();
+ ConfigurableLanguage::createFromLangcode('it')->save();
+
+ $field_storage_definition = [
+ 'field_name' => 'untranslatable_string_field',
+ 'entity_type' => 'node',
+ 'type' => 'string',
+ 'cardinality' => 1,
+ 'translatable' => FALSE,
+ ];
+ $field_storage = FieldStorageConfig::create($field_storage_definition);
+ $field_storage->save();
+
+ $field_definition = [
+ 'field_storage' => $field_storage,
+ 'bundle' => 'page',
+ ];
+ $field = FieldConfig::create($field_definition);
+ $field->save();
+
+ // Enable translation for page nodes.
+ \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
+
+ // Create and log in user.
+ $web_user = $this->drupalCreateUser(
+ [
+ 'view page revisions',
+ 'revert page revisions',
+ 'delete page revisions',
+ 'edit any page content',
+ 'delete any page content',
+ 'access contextual links',
+ 'translate any entity',
+ 'administer content types',
+ ]
+ );
+
+ $this->drupalLogin($web_user);
+
+ // Create initial node.
+ $node = $this->drupalCreateNode();
+ $settings = get_object_vars($node);
+ $settings['revision'] = 1;
+ $settings['isDefaultRevision'] = TRUE;
+
+ $nodes = [];
+ $logs = [];
+
+ // Get original node.
+ $nodes[] = clone $node;
+
+ // Create three revisions.
+ $revision_count = 3;
+ for ($i = 0; $i < $revision_count; $i++) {
+ $logs[] = $node->revision_log = $this->randomMachineName(32);
+
+ // Create revision with a random title and body and update variables.
+ $node->title = $this->randomMachineName();
+ $node->body = [
+ 'value' => $this->randomMachineName(32),
+ 'format' => filter_default_format(),
+ ];
+ $node->untranslatable_string_field->value = $this->randomString();
+ $node->setNewRevision();
+
+ // Edit the 1st and 2nd revision with a different user.
+ if ($i < 2) {
+ $editor = $this->drupalCreateUser();
+ $node->setRevisionUserId($editor->id());
+ }
+ else {
+ $node->setRevisionUserId($web_user->id());
+ }
+
+ $node->save();
+
+ // Make sure we get revision information.
+ $node = Node::load($node->id());
+ $nodes[] = clone $node;
+ }
+
+ $this->nodes = $nodes;
+ $this->revisionLogs = $logs;
+ }
+
+ /**
+ * Checks node revision related operations.
+ */
+ public function testRevisions() {
+ $node_storage = $this->container->get('entity.manager')->getStorage('node');
+ $nodes = $this->nodes;
+ $logs = $this->revisionLogs;
+
+ // Get last node for simple checks.
+ $node = $nodes[3];
+
+ // Confirm the correct revision text appears on "view revisions" page.
+ $this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
+ $this->assertText($node->body->value, 'Correct text displays for version.');
+
+ // Confirm the correct log message appears on "revisions overview" page.
+ $this->drupalGet("node/" . $node->id() . "/revisions");
+ foreach ($logs as $revision_log) {
+ $this->assertText($revision_log, 'Revision log message found.');
+ }
+ // Original author, and editor names should appear on revisions overview.
+ $web_user = $nodes[0]->revision_uid->entity;
+ $this->assertText(t('by @name', ['@name' => $web_user->getAccountName()]));
+ $editor = $nodes[2]->revision_uid->entity;
+ $this->assertText(t('by @name', ['@name' => $editor->getAccountName()]));
+
+ // Confirm that this is the default revision.
+ $this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
+
+ // Confirm that the "Edit" and "Delete" contextual links appear for the
+ // default revision.
+ $ids = ['node:node=' . $node->id() . ':changed=' . $node->getChangedTime()];
+ $json = $this->renderContextualLinks($ids, 'node/' . $node->id());
+ $this->verbose($json[$ids[0]]);
+
+ $expected = '
Edit';
+ $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.');
+ $expected = 'Delete';
+ $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Delete" contextual link is shown for the default revision.');
+
+ // Confirm that revisions revert properly.
+ $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", [], t('Revert'));
+ $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
+ '@type' => 'Basic page',
+ '%title' => $nodes[1]->label(),
+ '%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),
+ ]), 'Revision reverted.');
+ $node_storage->resetCache([$node->id()]);
+ $reverted_node = $node_storage->load($node->id());
+ $this->assertTrue(($nodes[1]->body->value == $reverted_node->body->value), 'Node reverted correctly.');
+ // Confirm the revision author is the user performing the revert.
+ $this->assertTrue($reverted_node->getRevisionUserId() == $this->loggedInUser->id(), 'Node revision author is user performing revert.');
+ // And that its not the revision author.
+ $this->assertTrue($reverted_node->getRevisionUserId() != $nodes[1]->getRevisionUserId(), 'Node revision author is not original revision author.');
+
+ // Confirm that this is not the default version.
+ $node = node_revision_load($node->getRevisionId());
+ $this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
+
+ // Confirm that "Edit" and "Delete" contextual links don't appear for
+ // non-default revision.
+ $ids = ['node_revision::node=' . $node->id() . '&node_revision=' . $node->getRevisionId() . ':'];
+ $json = $this->renderContextualLinks($ids, 'node/' . $node->id() . '/revisions/' . $node->getRevisionId() . '/view');
+ $this->verbose($json[$ids[0]]);
+
+ $this->assertFalse(strstr($json[$ids[0]], ''), 'The "Edit" contextual link is not shown for a non-default revision.');
+ $this->assertFalse(strstr($json[$ids[0]], ''), 'The "Delete" contextual link is not shown for a non-default revision.');
+
+ // Confirm revisions delete properly.
+ $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", [], t('Delete'));
+ $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', [
+ '%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),
+ '@type' => 'Basic page',
+ '%title' => $nodes[1]->label(),
+ ]), 'Revision deleted.');
+ $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Revision not found.');
+ $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_field_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Field revision not found.');
+
+ // Set the revision timestamp to an older date to make sure that the
+ // confirmation message correctly displays the stored revision date.
+ $old_revision_date = REQUEST_TIME - 86400;
+ db_update('node_revision')
+ ->condition('vid', $nodes[2]->getRevisionId())
+ ->fields([
+ 'revision_timestamp' => $old_revision_date,
+ ])
+ ->execute();
+ $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert", [], t('Revert'));
+ $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
+ '@type' => 'Basic page',
+ '%title' => $nodes[2]->label(),
+ '%revision-date' => format_date($old_revision_date),
+ ]));
+
+ // Make a new revision and set it to not be default.
+ // This will create a new revision that is not "front facing".
+ $new_node_revision = clone $node;
+ $new_body = $this->randomMachineName();
+ $new_node_revision->body->value = $new_body;
+ // Save this as a non-default revision.
+ $new_node_revision->setNewRevision();
+ $new_node_revision->isDefaultRevision = FALSE;
+ $new_node_revision->save();
+
+ $this->drupalGet('node/' . $node->id());
+ $this->assertNoText($new_body, 'Revision body text is not present on default version of node.');
+
+ // Verify that the new body text is present on the revision.
+ $this->drupalGet("node/" . $node->id() . "/revisions/" . $new_node_revision->getRevisionId() . "/view");
+ $this->assertText($new_body, 'Revision body text is present when loading specific revision.');
+
+ // Verify that the non-default revision vid is greater than the default
+ // revision vid.
+ $default_revision = db_select('node', 'n')
+ ->fields('n', ['vid'])
+ ->condition('nid', $node->id())
+ ->execute()
+ ->fetchCol();
+ $default_revision_vid = $default_revision[0];
+ $this->assertTrue($new_node_revision->getRevisionId() > $default_revision_vid, 'Revision vid is greater than default revision vid.');
+
+ // Create an 'EN' node with a revision log message.
+ $node = $this->drupalCreateNode();
+ $node->title = 'Node title in EN';
+ $node->revision_log = 'Simple revision message (EN)';
+ $node->save();
+
+ $this->drupalGet("node/" . $node->id() . "/revisions");
+ $this->assertResponse(403);
+
+ // Create a new revision and new log message.
+ $node = Node::load($node->id());
+ $node->body->value = 'New text (EN)';
+ $node->revision_log = 'New revision message (EN)';
+ $node->setNewRevision();
+ $node->save();
+
+ // Check both revisions are shown on the node revisions overview page.
+ $this->drupalGet("node/" . $node->id() . "/revisions");
+ $this->assertText('Simple revision message (EN)');
+ $this->assertText('New revision message (EN)');
+
+ // Create an 'EN' node with a revision log message.
+ $node = $this->drupalCreateNode();
+ $node->langcode = 'en';
+ $node->title = 'Node title in EN';
+ $node->revision_log = 'Simple revision message (EN)';
+ $node->save();
+
+ $this->drupalGet("node/" . $node->id() . "/revisions");
+ $this->assertResponse(403);
+
+ // Add a translation in 'DE' and create a new revision and new log message.
+ $translation = $node->addTranslation('de');
+ $translation->title->value = 'Node title in DE';
+ $translation->body->value = 'New text (DE)';
+ $translation->revision_log = 'New revision message (DE)';
+ $translation->setNewRevision();
+ $translation->save();
+
+ // View the revision UI in 'IT', only the original node revision is shown.
+ $this->drupalGet("it/node/" . $node->id() . "/revisions");
+ $this->assertText('Simple revision message (EN)');
+ $this->assertNoText('New revision message (DE)');
+
+ // View the revision UI in 'DE', only the translated node revision is shown.
+ $this->drupalGet("de/node/" . $node->id() . "/revisions");
+ $this->assertNoText('Simple revision message (EN)');
+ $this->assertText('New revision message (DE)');
+
+ // View the revision UI in 'EN', only the original node revision is shown.
+ $this->drupalGet("node/" . $node->id() . "/revisions");
+ $this->assertText('Simple revision message (EN)');
+ $this->assertNoText('New revision message (DE)');
+ }
+
+ /**
+ * Checks that revisions are correctly saved without log messages.
+ */
+ public function testNodeRevisionWithoutLogMessage() {
+ $node_storage = $this->container->get('entity.manager')->getStorage('node');
+ // Create a node with an initial log message.
+ $revision_log = $this->randomMachineName(10);
+ $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
+
+ // Save over the same revision and explicitly provide an empty log message
+ // (for example, to mimic the case of a node form submitted with no text in
+ // the "log message" field), and check that the original log message is
+ // preserved.
+ $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage1';
+
+ $node = clone $node;
+ $node->title = $new_title;
+ $node->revision_log = '';
+ $node->setNewRevision(FALSE);
+
+ $node->save();
+ $this->drupalGet('node/' . $node->id());
+ $this->assertText($new_title, 'New node title appears on the page.');
+ $node_storage->resetCache([$node->id()]);
+ $node_revision = $node_storage->load($node->id());
+ $this->assertEqual($node_revision->revision_log->value, $revision_log, 'After an existing node revision is re-saved without a log message, the original log message is preserved.');
+
+ // Create another node with an initial revision log message.
+ $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
+
+ // Save a new node revision without providing a log message, and check that
+ // this revision has an empty log message.
+ $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage2';
+
+ $node = clone $node;
+ $node->title = $new_title;
+ $node->setNewRevision();
+ $node->revision_log = NULL;
+
+ $node->save();
+ $this->drupalGet('node/' . $node->id());
+ $this->assertText($new_title, 'New node title appears on the page.');
+ $node_storage->resetCache([$node->id()]);
+ $node_revision = $node_storage->load($node->id());
+ $this->assertTrue(empty($node_revision->revision_log->value), 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
+ }
+
+ /**
+ * Gets server-rendered contextual links for the given contextual links IDs.
+ *
+ * @param string[] $ids
+ * An array of contextual link IDs.
+ * @param string $current_path
+ * The Drupal path for the page for which the contextual links are rendered.
+ *
+ * @return string
+ * The decoded JSON response body.
+ */
+ protected function renderContextualLinks(array $ids, $current_path) {
+ $post = [];
+ for ($i = 0; $i < count($ids); $i++) {
+ $post['ids[' . $i . ']'] = $ids[$i];
+ }
+ $response = $this->drupalPost('contextual/render', 'application/json', $post, ['query' => ['destination' => $current_path]]);
+
+ return Json::decode($response);
+ }
+
+ /**
+ * Tests the revision translations are correctly reverted.
+ */
+ public function testRevisionTranslationRevert() {
+ // Create a node and a few revisions.
+ $node = $this->drupalCreateNode(['langcode' => 'en']);
+
+ $initial_revision_id = $node->getRevisionId();
+ $initial_title = $node->label();
+ $this->createRevisions($node, 2);
+
+ // Translate the node and create a few translation revisions.
+ $translation = $node->addTranslation('it');
+ $this->createRevisions($translation, 3);
+ $revert_id = $node->getRevisionId();
+ $translated_title = $translation->label();
+ $untranslatable_string = $node->untranslatable_string_field->value;
+
+ // Create a new revision for the default translation in-between a series of
+ // translation revisions.
+ $this->createRevisions($node, 1);
+ $default_translation_title = $node->label();
+
+ // And create a few more translation revisions.
+ $this->createRevisions($translation, 2);
+ $translation_revision_id = $translation->getRevisionId();
+
+ // Now revert the a translation revision preceding the last default
+ // translation revision, and check that the desired value was reverted but
+ // the default translation value was preserved.
+ $revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
+ 'node' => $node->id(),
+ 'node_revision' => $revert_id,
+ 'langcode' => 'it',
+ ]);
+ $this->drupalPostForm($revert_translation_url, [], t('Revert'));
+ /** @var \Drupal\node\NodeStorage $node_storage */
+ $node_storage = $this->container->get('entity.manager')->getStorage('node');
+ $node_storage->resetCache();
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $node_storage->load($node->id());
+ $this->assertTrue($node->getRevisionId() > $translation_revision_id);
+ $this->assertEqual($node->label(), $default_translation_title);
+ $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
+ $this->assertNotEqual($node->untranslatable_string_field->value, $untranslatable_string);
+
+ $latest_revision_id = $translation->getRevisionId();
+
+ // Now revert the a translation revision preceding the last default
+ // translation revision again, and check that the desired value was reverted
+ // but the default translation value was preserved. But in addition the
+ // untranslated field will be reverted as well.
+ $this->drupalPostForm($revert_translation_url, ['revert_untranslated_fields' => TRUE], t('Revert'));
+ $node_storage->resetCache();
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $node_storage->load($node->id());
+ $this->assertTrue($node->getRevisionId() > $latest_revision_id);
+ $this->assertEqual($node->label(), $default_translation_title);
+ $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
+ $this->assertEqual($node->untranslatable_string_field->value, $untranslatable_string);
+
+ $latest_revision_id = $translation->getRevisionId();
+
+ // Now revert the entity revision to the initial one where the translation
+ // didn't exist.
+ $revert_url = Url::fromRoute('node.revision_revert_confirm', [
+ 'node' => $node->id(),
+ 'node_revision' => $initial_revision_id,
+ ]);
+ $this->drupalPostForm($revert_url, [], t('Revert'));
+ $node_storage->resetCache();
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $node_storage->load($node->id());
+ $this->assertTrue($node->getRevisionId() > $latest_revision_id);
+ $this->assertEqual($node->label(), $initial_title);
+ $this->assertFalse($node->hasTranslation('it'));
+ }
+
+ /**
+ * Creates a series of revisions for the specified node.
+ *
+ * @param \Drupal\node\NodeInterface $node
+ * The node object.
+ * @param $count
+ * The number of revisions to be created.
+ */
+ protected function createRevisions(NodeInterface $node, $count) {
+ for ($i = 0; $i < $count; $i++) {
+ $node->title = $this->randomString();
+ $node->untranslatable_string_field->value = $this->randomString();
+ $node->setNewRevision(TRUE);
+ $node->save();
+ }
+ }
+
+}
diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php
new file mode 100644
index 000000000..9938bb0ab
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeTypeTest.php
@@ -0,0 +1,259 @@
+assertTrue(isset($node_types['article']), 'Node type article is available.');
+ $this->assertTrue(isset($node_types['page']), 'Node type basic page is available.');
+
+ $this->assertEqual($node_types['article']->label(), $node_names['article'], 'Correct node type base has been returned.');
+
+ $article = NodeType::load('article');
+ $this->assertEqual($node_types['article'], $article, 'Correct node type has been returned.');
+ $this->assertEqual($node_types['article']->label(), $article->label(), 'Correct node type name has been returned.');
+ }
+
+ /**
+ * Tests creating a content type programmatically and via a form.
+ */
+ public function testNodeTypeCreation() {
+ // Create a content type programmatically.
+ $type = $this->drupalCreateContentType();
+
+ $type_exists = (bool) NodeType::load($type->id());
+ $this->assertTrue($type_exists, 'The new content type has been created in the database.');
+
+ // Log in a test user.
+ $web_user = $this->drupalCreateUser(['create ' . $type->label() . ' content']);
+ $this->drupalLogin($web_user);
+
+ $this->drupalGet('node/add/' . $type->id());
+ $this->assertResponse(200, 'The new content type can be accessed at node/add.');
+
+ // Create a content type via the user interface.
+ $web_user = $this->drupalCreateUser(['bypass node access', 'administer content types']);
+ $this->drupalLogin($web_user);
+
+ $this->drupalGet('node/add');
+ $this->assertCacheTag('config:node_type_list');
+ $this->assertCacheContext('user.permissions');
+ $elements = $this->cssSelect('dl.node-type-list dt');
+ $this->assertEqual(3, count($elements));
+
+ $edit = [
+ 'name' => 'foo',
+ 'title_label' => 'title for foo',
+ 'type' => 'foo',
+ ];
+ $this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields'));
+ $type_exists = (bool) NodeType::load('foo');
+ $this->assertTrue($type_exists, 'The new content type has been created in the database.');
+
+ $this->drupalGet('node/add');
+ $elements = $this->cssSelect('dl.node-type-list dt');
+ $this->assertEqual(4, count($elements));
+ }
+
+ /**
+ * Tests editing a node type using the UI.
+ */
+ public function testNodeTypeEditing() {
+ $this->drupalPlaceBlock('system_breadcrumb_block');
+ $web_user = $this->drupalCreateUser(['bypass node access', 'administer content types', 'administer node fields']);
+ $this->drupalLogin($web_user);
+
+ $field = FieldConfig::loadByName('node', 'page', 'body');
+ $this->assertEqual($field->getLabel(), 'Body', 'Body field was found.');
+
+ // Verify that title and body fields are displayed.
+ $this->drupalGet('node/add/page');
+ $this->assertRaw('Title', 'Title field was found.');
+ $this->assertRaw('Body', 'Body field was found.');
+
+ // Rename the title field.
+ $edit = [
+ 'title_label' => 'Foo',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
+
+ $this->drupalGet('node/add/page');
+ $this->assertRaw('Foo', 'New title label was displayed.');
+ $this->assertNoRaw('Title', 'Old title label was not displayed.');
+
+ // Change the name and the description.
+ $edit = [
+ 'name' => 'Bar',
+ 'description' => 'Lorem ipsum.',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
+
+ $this->drupalGet('node/add');
+ $this->assertRaw('Bar', 'New name was displayed.');
+ $this->assertRaw('Lorem ipsum', 'New description was displayed.');
+ $this->clickLink('Bar');
+ $this->assertRaw('Foo', 'Title field was found.');
+ $this->assertRaw('Body', 'Body field was found.');
+
+ // Change the name through the API
+ /** @var \Drupal\node\NodeTypeInterface $node_type */
+ $node_type = NodeType::load('page');
+ $node_type->set('name', 'NewBar');
+ $node_type->save();
+
+ /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
+ $bundle_info = \Drupal::service('entity_type.bundle.info');
+ $node_bundles = $bundle_info->getBundleInfo('node');
+ $this->assertEqual($node_bundles['page']['label'], 'NewBar', 'Node type bundle cache is updated');
+
+ // Remove the body field.
+ $this->drupalPostForm('admin/structure/types/manage/page/fields/node.page.body/delete', [], t('Delete'));
+ // Resave the settings for this type.
+ $this->drupalPostForm('admin/structure/types/manage/page', [], t('Save content type'));
+ $front_page_path = Url::fromRoute('')->toString();
+ $this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
+ $front_page_path => 'Home',
+ 'admin/structure/types' => 'Content types',
+ 'admin/structure/types/manage/page' => 'NewBar',
+ ]);
+ // Check that the body field doesn't exist.
+ $this->drupalGet('node/add/page');
+ $this->assertNoRaw('Body', 'Body field was not found.');
+ }
+
+ /**
+ * Tests deleting a content type that still has content.
+ */
+ public function testNodeTypeDeletion() {
+ $this->drupalPlaceBlock('page_title_block');
+ // Create a content type programmatically.
+ $type = $this->drupalCreateContentType();
+
+ // Log in a test user.
+ $web_user = $this->drupalCreateUser([
+ 'bypass node access',
+ 'administer content types',
+ ]);
+ $this->drupalLogin($web_user);
+
+ // Add a new node of this type.
+ $node = $this->drupalCreateNode(['type' => $type->id()]);
+ // Attempt to delete the content type, which should not be allowed.
+ $this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
+ $this->assertRaw(
+ t('%type is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the %type content.', ['%type' => $type->label()]),
+ 'The content type will not be deleted until all nodes of that type are removed.'
+ );
+ $this->assertNoText(t('This action cannot be undone.'), 'The node type deletion confirmation form is not available.');
+
+ // Delete the node.
+ $node->delete();
+ // Attempt to delete the content type, which should now be allowed.
+ $this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
+ $this->assertRaw(
+ t('Are you sure you want to delete the content type %type?', ['%type' => $type->label()]),
+ 'The content type is available for deletion.'
+ );
+ $this->assertText(t('This action cannot be undone.'), 'The node type deletion confirmation form is available.');
+
+ // Test that a locked node type could not be deleted.
+ $this->container->get('module_installer')->install(['node_test_config']);
+ // Lock the default node type.
+ $locked = \Drupal::state()->get('node.type.locked');
+ $locked['default'] = 'default';
+ \Drupal::state()->set('node.type.locked', $locked);
+ // Call to flush all caches after installing the forum module in the same
+ // way installing a module through the UI does.
+ $this->resetAll();
+ $this->drupalGet('admin/structure/types/manage/default');
+ $this->assertNoLink(t('Delete'));
+ $this->drupalGet('admin/structure/types/manage/default/delete');
+ $this->assertResponse(403);
+ $this->container->get('module_installer')->uninstall(['node_test_config']);
+ $this->container = \Drupal::getContainer();
+ unset($locked['default']);
+ \Drupal::state()->set('node.type.locked', $locked);
+ $this->drupalGet('admin/structure/types/manage/default');
+ $this->clickLink(t('Delete'));
+ $this->assertResponse(200);
+ $this->drupalPostForm(NULL, [], t('Delete'));
+ $this->assertFalse((bool) NodeType::load('default'), 'Node type with machine default deleted.');
+ }
+
+ /**
+ * Tests Field UI integration for content types.
+ */
+ public function testNodeTypeFieldUiPermissions() {
+ // Create an admin user who can only manage node fields.
+ $admin_user_1 = $this->drupalCreateUser(['administer content types', 'administer node fields']);
+ $this->drupalLogin($admin_user_1);
+
+ // Test that the user only sees the actions available to him.
+ $this->drupalGet('admin/structure/types');
+ $this->assertLinkByHref('admin/structure/types/manage/article/fields');
+ $this->assertNoLinkByHref('admin/structure/types/manage/article/display');
+
+ // Create another admin user who can manage node fields display.
+ $admin_user_2 = $this->drupalCreateUser(['administer content types', 'administer node display']);
+ $this->drupalLogin($admin_user_2);
+
+ // Test that the user only sees the actions available to him.
+ $this->drupalGet('admin/structure/types');
+ $this->assertNoLinkByHref('admin/structure/types/manage/article/fields');
+ $this->assertLinkByHref('admin/structure/types/manage/article/display');
+ }
+
+ /**
+ * Tests for when there are no content types defined.
+ */
+ public function testNodeTypeNoContentType() {
+ /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
+ $bundle_info = \Drupal::service('entity_type.bundle.info');
+ $this->assertEqual(2, count($bundle_info->getBundleInfo('node')), 'The bundle information service has 2 bundles for the Node entity type.');
+ $web_user = $this->drupalCreateUser(['administer content types']);
+ $this->drupalLogin($web_user);
+
+ // Delete 'article' bundle.
+ $this->drupalPostForm('admin/structure/types/manage/article/delete', [], t('Delete'));
+ // Delete 'page' bundle.
+ $this->drupalPostForm('admin/structure/types/manage/page/delete', [], t('Delete'));
+
+ // Navigate to content type administration screen
+ $this->drupalGet('admin/structure/types');
+ $this->assertRaw(t('No content types available. Add content type.', [
+ ':link' => Url::fromRoute('node.type_add')->toString(),
+ ]), 'Empty text when there are no content types in the system is correct.');
+
+ $bundle_info->clearCachedBundles();
+ $this->assertEqual(0, count($bundle_info->getBundleInfo('node')), 'The bundle information service has 0 bundles for the Node entity type.');
+ }
+
+}
diff --git a/core/modules/node/src/Tests/PagePreviewTest.php b/core/modules/node/src/Tests/PagePreviewTest.php
new file mode 100644
index 000000000..2bc9cd3ce
--- /dev/null
+++ b/core/modules/node/src/Tests/PagePreviewTest.php
@@ -0,0 +1,475 @@
+addDefaultCommentField('node', 'page');
+
+ $web_user = $this->drupalCreateUser(['edit own page content', 'create page content', 'administer menu']);
+ $this->drupalLogin($web_user);
+
+ // Add a vocabulary so we can test different view modes.
+ $vocabulary = Vocabulary::create([
+ 'name' => $this->randomMachineName(),
+ 'description' => $this->randomMachineName(),
+ 'vid' => $this->randomMachineName(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ 'help' => '',
+ ]);
+ $vocabulary->save();
+
+ $this->vocabulary = $vocabulary;
+
+ // Add a term to the vocabulary.
+ $term = Term::create([
+ 'name' => $this->randomMachineName(),
+ 'description' => $this->randomMachineName(),
+ 'vid' => $this->vocabulary->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ]);
+ $term->save();
+
+ $this->term = $term;
+
+ // Create an image field.
+ FieldStorageConfig::create([
+ 'field_name' => 'field_image',
+ 'entity_type' => 'node',
+ 'type' => 'image',
+ 'settings' => [],
+ 'cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED,
+ ])->save();
+
+ $field_config = FieldConfig::create([
+ 'field_name' => 'field_image',
+ 'label' => 'Images',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'required' => FALSE,
+ 'settings' => [],
+ ]);
+ $field_config->save();
+
+ // Create a field.
+ $this->fieldName = mb_strtolower($this->randomMachineName());
+ $handler_settings = [
+ 'target_bundles' => [
+ $this->vocabulary->id() => $this->vocabulary->id(),
+ ],
+ 'auto_create' => TRUE,
+ ];
+ $this->createEntityReferenceField('node', 'page', $this->fieldName, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+
+ entity_get_form_display('node', 'page', 'default')
+ ->setComponent($this->fieldName, [
+ 'type' => 'entity_reference_autocomplete_tags',
+ ])
+ ->save();
+
+ // Show on default display and teaser.
+ entity_get_display('node', 'page', 'default')
+ ->setComponent($this->fieldName, [
+ 'type' => 'entity_reference_label',
+ ])
+ ->save();
+ entity_get_display('node', 'page', 'teaser')
+ ->setComponent($this->fieldName, [
+ 'type' => 'entity_reference_label',
+ ])
+ ->save();
+
+ entity_get_form_display('node', 'page', 'default')
+ ->setComponent('field_image', [
+ 'type' => 'image_image',
+ 'settings' => [],
+ ])
+ ->save();
+
+ entity_get_display('node', 'page', 'default')
+ ->setComponent('field_image')
+ ->save();
+
+ // Create a multi-value text field.
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => 'field_test_multi',
+ 'entity_type' => 'node',
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ 'type' => 'text',
+ 'settings' => [
+ 'max_length' => 50,
+ ],
+ ]);
+ $field_storage->save();
+ FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'page',
+ ])->save();
+
+ entity_get_form_display('node', 'page', 'default')
+ ->setComponent('field_test_multi', [
+ 'type' => 'text_textfield',
+ ])
+ ->save();
+
+ entity_get_display('node', 'page', 'default')
+ ->setComponent('field_test_multi', [
+ 'type' => 'string',
+ ])
+ ->save();
+ }
+
+ /**
+ * Checks the node preview functionality.
+ */
+ public function testPagePreview() {
+ $title_key = 'title[0][value]';
+ $body_key = 'body[0][value]';
+ $term_key = $this->fieldName . '[target_id]';
+
+ // Fill in node creation form and preview node.
+ $edit = [];
+ $edit[$title_key] = '' . $this->randomMachineName(8) . '';
+ $edit[$body_key] = $this->randomMachineName(16);
+ $edit[$term_key] = $this->term->getName();
+
+ // Upload an image.
+ $test_image = current($this->drupalGetTestFiles('image', 39325));
+ $edit['files[field_image_0][]'] = \Drupal::service('file_system')->realpath($test_image->uri);
+ $this->drupalPostForm('node/add/page', $edit, t('Upload'));
+
+ // Add an alt tag and preview the node.
+ $this->drupalPostForm(NULL, ['field_image[0][alt]' => 'Picture of llamas'], t('Preview'));
+
+ // Check that the preview is displaying the title, body and term.
+ $this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.');
+ $this->assertEscaped($edit[$title_key], 'Title displayed and escaped.');
+ $this->assertText($edit[$body_key], 'Body displayed.');
+ $this->assertText($edit[$term_key], 'Term displayed.');
+ $this->assertLink(t('Back to content editing'));
+
+ // Check that we see the class of the node type on the body element.
+ $body_class_element = $this->xpath("//body[contains(@class, 'page-node-type-page')]");
+ $this->assertTrue(!empty($body_class_element), 'Node type body class found.');
+
+ // Get the UUID.
+ $url = parse_url($this->getUrl());
+ $paths = explode('/', $url['path']);
+ $view_mode = array_pop($paths);
+ $uuid = array_pop($paths);
+
+ // Switch view mode. We'll remove the body from the teaser view mode.
+ entity_get_display('node', 'page', 'teaser')
+ ->removeComponent('body')
+ ->save();
+
+ $view_mode_edit = ['view_mode' => 'teaser'];
+ $this->drupalPostForm('node/preview/' . $uuid . '/full', $view_mode_edit, t('Switch'));
+ $this->assertRaw('view-mode-teaser', 'View mode teaser class found.');
+ $this->assertNoText($edit[$body_key], 'Body not displayed.');
+
+ // Check that the title, body and term fields are displayed with the
+ // values after going back to the content edit page.
+ $this->clickLink(t('Back to content editing'));
+ $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
+ $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
+ $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
+ $this->assertFieldByName('field_image[0][alt]', 'Picture of llamas');
+ $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')], NULL, [], [], 'node-page-form');
+ $this->assertFieldByName('field_test_multi[0][value]');
+ $this->assertFieldByName('field_test_multi[1][value]');
+
+ // Return to page preview to check everything is as expected.
+ $this->drupalPostForm(NULL, [], t('Preview'));
+ $this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.');
+ $this->assertEscaped($edit[$title_key], 'Title displayed and escaped.');
+ $this->assertText($edit[$body_key], 'Body displayed.');
+ $this->assertText($edit[$term_key], 'Term displayed.');
+ $this->assertLink(t('Back to content editing'));
+
+ // Assert the content is kept when reloading the page.
+ $this->drupalGet('node/add/page', ['query' => ['uuid' => $uuid]]);
+ $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
+ $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
+ $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
+
+ // Save the node - this is a new POST, so we need to upload the image.
+ $this->drupalPostForm('node/add/page', $edit, t('Upload'));
+ $this->drupalPostForm(NULL, ['field_image[0][alt]' => 'Picture of llamas'], t('Save'));
+ $node = $this->drupalGetNodeByTitle($edit[$title_key]);
+
+ // Check the term was displayed on the saved node.
+ $this->drupalGet('node/' . $node->id());
+ $this->assertText($edit[$term_key], 'Term displayed.');
+
+ // Check the term appears again on the edit form.
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertFieldByName($term_key, $edit[$term_key] . ' (' . $this->term->id() . ')', 'Term field displayed.');
+
+ // Check with two new terms on the edit form, additionally to the existing
+ // one.
+ $edit = [];
+ $newterm1 = $this->randomMachineName(8);
+ $newterm2 = $this->randomMachineName(8);
+ $edit[$term_key] = $this->term->getName() . ', ' . $newterm1 . ', ' . $newterm2;
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Preview'));
+ $this->assertRaw('>' . $newterm1 . '<', 'First new term displayed.');
+ $this->assertRaw('>' . $newterm2 . '<', 'Second new term displayed.');
+ // The first term should be displayed as link, the others not.
+ $this->assertLink($this->term->getName());
+ $this->assertNoLink($newterm1);
+ $this->assertNoLink($newterm2);
+
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
+
+ // Check with one more new term, keeping old terms, removing the existing
+ // one.
+ $edit = [];
+ $newterm3 = $this->randomMachineName(8);
+ $edit[$term_key] = $newterm1 . ', ' . $newterm3 . ', ' . $newterm2;
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Preview'));
+ $this->assertRaw('>' . $newterm1 . '<', 'First existing term displayed.');
+ $this->assertRaw('>' . $newterm2 . '<', 'Second existing term displayed.');
+ $this->assertRaw('>' . $newterm3 . '<', 'Third new term displayed.');
+ $this->assertNoText($this->term->getName());
+ $this->assertLink($newterm1);
+ $this->assertLink($newterm2);
+ $this->assertNoLink($newterm3);
+
+ // Check that editing an existing node after it has been previewed and not
+ // saved doesn't remember the previous changes.
+ $edit = [
+ $title_key => $this->randomMachineName(8),
+ ];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Preview'));
+ $this->assertText($edit[$title_key], 'New title displayed.');
+ $this->clickLink(t('Back to content editing'));
+ $this->assertFieldByName($title_key, $edit[$title_key], 'New title value displayed.');
+ // Navigate away from the node without saving.
+ $this->drupalGet('');
+ // Go back to the edit form, the title should have its initial value.
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertFieldByName($title_key, $node->label(), 'Correct title value displayed.');
+
+ // Check with required preview.
+ $node_type = NodeType::load('page');
+ $node_type->setPreviewMode(DRUPAL_REQUIRED);
+ $node_type->save();
+ $this->drupalGet('node/add/page');
+ $this->assertNoRaw('edit-submit');
+ $this->drupalPostForm('node/add/page', [$title_key => 'Preview'], t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->assertRaw('edit-submit');
+
+ // Check that destination is remembered when clicking on preview. When going
+ // back to the edit form and clicking save, we should go back to the
+ // original destination, if set.
+ $destination = 'node';
+ $this->drupalPostForm($node->toUrl('edit-form'), [], t('Preview'), ['query' => ['destination' => $destination]]);
+ $parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
+ $options = ['absolute' => TRUE, 'query' => ['destination' => $destination]];
+ $this->assertUrl(Url::fromRoute('entity.node.preview', $parameters, $options));
+ $this->drupalPostForm(NULL, ['view_mode' => 'teaser'], t('Switch'));
+ $this->clickLink(t('Back to content editing'));
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->assertUrl($destination);
+
+ // Check that preview page works as expected without a destination set.
+ $this->drupalPostForm($node->toUrl('edit-form'), [], t('Preview'));
+ $parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
+ $this->assertUrl(Url::fromRoute('entity.node.preview', $parameters, ['absolute' => TRUE]));
+ $this->drupalPostForm(NULL, ['view_mode' => 'teaser'], t('Switch'));
+ $this->clickLink(t('Back to content editing'));
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->assertUrl($node->toUrl());
+ $this->assertResponse(200);
+
+ /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+ $file_system = \Drupal::service('file_system');
+ // Assert multiple items can be added and are not lost when previewing.
+ $test_image_1 = current($this->drupalGetTestFiles('image', 39325));
+ $edit_image_1['files[field_image_0][]'] = $file_system->realpath($test_image_1->uri);
+ $test_image_2 = current($this->drupalGetTestFiles('image', 39325));
+ $edit_image_2['files[field_image_1][]'] = $file_system->realpath($test_image_2->uri);
+ $edit['field_image[0][alt]'] = 'Alt 1';
+
+ $this->drupalPostForm('node/add/page', $edit_image_1, t('Upload'));
+ $this->drupalPostForm(NULL, $edit, t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->assertFieldByName('files[field_image_1][]');
+ $this->drupalPostForm(NULL, $edit_image_2, t('Upload'));
+ $this->assertNoFieldByName('files[field_image_1][]');
+
+ $title = 'node_test_title';
+ $example_text_1 = 'example_text_preview_1';
+ $example_text_2 = 'example_text_preview_2';
+ $example_text_3 = 'example_text_preview_3';
+ $this->drupalGet('node/add/page');
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_test_multi[0][value]' => $example_text_1,
+ ];
+ $this->assertRaw('Storage is not set');
+ $this->drupalPostForm(NULL, $edit, t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->assertRaw('Storage is set');
+ $this->assertFieldByName('field_test_multi[0][value]');
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->assertText('Basic page ' . $title . ' has been created.');
+ $node = $this->drupalGetNodeByTitle($title);
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]);
+ $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]);
+ $edit = [
+ 'field_test_multi[1][value]' => $example_text_2,
+ 'field_test_multi[2][value]' => $example_text_3,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->drupalPostForm(NULL, $edit, t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->assertFieldByName('field_test_multi[0][value]', $example_text_1);
+ $this->assertFieldByName('field_test_multi[1][value]', $example_text_2);
+ $this->assertFieldByName('field_test_multi[2][value]', $example_text_3);
+
+ // Now save the node and make sure all values got saved.
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->assertText($example_text_1);
+ $this->assertText($example_text_2);
+ $this->assertText($example_text_3);
+
+ // Edit again, change the menu_ui settings and click on preview.
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $edit = [
+ 'menu[enabled]' => TRUE,
+ 'menu[title]' => 'Changed title',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Preview'));
+ $this->clickLink(t('Back to content editing'));
+ $this->assertFieldChecked('edit-menu-enabled', 'Menu option is still checked');
+ $this->assertFieldByName('menu[title]', 'Changed title', 'Menu link title is correct after preview');
+
+ // Save, change the title while saving and make sure that it is correctly
+ // saved.
+ $edit = [
+ 'menu[enabled]' => TRUE,
+ 'menu[title]' => 'Second title change',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertFieldByName('menu[title]', 'Second title change', 'Menu link title is correct after saving');
+
+ }
+
+ /**
+ * Checks the node preview functionality, when using revisions.
+ */
+ public function testPagePreviewWithRevisions() {
+ $title_key = 'title[0][value]';
+ $body_key = 'body[0][value]';
+ $term_key = $this->fieldName . '[target_id]';
+ // Force revision on "Basic page" content.
+ $node_type = NodeType::load('page');
+ $node_type->setNewRevision(TRUE);
+ $node_type->save();
+
+ // Fill in node creation form and preview node.
+ $edit = [];
+ $edit[$title_key] = $this->randomMachineName(8);
+ $edit[$body_key] = $this->randomMachineName(16);
+ $edit[$term_key] = $this->term->id();
+ $edit['revision_log[0][value]'] = $this->randomString(32);
+ $this->drupalPostForm('node/add/page', $edit, t('Preview'));
+
+ // Check that the preview is displaying the title, body and term.
+ $this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.');
+ $this->assertText($edit[$title_key], 'Title displayed.');
+ $this->assertText($edit[$body_key], 'Body displayed.');
+ $this->assertText($edit[$term_key], 'Term displayed.');
+
+ // Check that the title and body fields are displayed with the correct
+ // values after going back to the content edit page.
+ $this->clickLink(t('Back to content editing')); $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
+ $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');
+ $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.');
+
+ // Check that the revision log field has the correct value.
+ $this->assertFieldByName('revision_log[0][value]', $edit['revision_log[0][value]'], 'Revision log field displayed.');
+
+ // Save the node after coming back from the preview page so we can create a
+ // pending revision for it.
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $node = $this->drupalGetNodeByTitle($edit[$title_key]);
+
+ // Check that previewing a pending revision of a node works. This can not be
+ // accomplished through the UI so we have to use API calls.
+ // @todo Change this test to use the UI when we will be able to create
+ // pending revisions in core.
+ // @see https://www.drupal.org/node/2725533
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(FALSE);
+
+ /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
+ $controller_resolver = \Drupal::service('controller_resolver');
+ $node_preview_controller = $controller_resolver->getControllerFromDefinition('\Drupal\node\Controller\NodePreviewController::view');
+ $node_preview_controller($node, 'full');
+ }
+
+ /**
+ * Checks the node preview accessible for simultaneous node editing.
+ */
+ public function testSimultaneousPreview() {
+ $title_key = 'title[0][value]';
+ $node = $this->drupalCreateNode([]);
+
+ $edit = [$title_key => 'New page title'];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Preview'));
+ $this->assertText($edit[$title_key]);
+
+ $user2 = $this->drupalCreateUser(['edit any page content']);
+ $this->drupalLogin($user2);
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertFieldByName($title_key, $node->label(), 'No title leaked from previous user.');
+
+ $edit2 = [$title_key => 'Another page title'];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit2, t('Preview'));
+ $this->assertUrl(\Drupal::url('entity.node.preview', ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'], ['absolute' => TRUE]));
+ $this->assertText($edit2[$title_key]);
+ }
+
+}
diff --git a/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php b/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php
new file mode 100644
index 000000000..dc23d0ce5
--- /dev/null
+++ b/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php
@@ -0,0 +1,118 @@
+drupalCreateContentType(['type' => 'page']);
+ $this->drupalCreateNode(['promote' => 1]);
+ $this->drupalGet('node');
+
+ $user = $this->drupalCreateUser(['administer nodes', 'access contextual links']);
+ $this->drupalLogin($user);
+
+ $response = $this->renderContextualLinks(['node:node=1:'], 'node');
+ $this->assertResponse(200);
+ $json = Json::decode($response);
+ $this->setRawContent($json['node:node=1:']);
+
+ // @todo Add these back when the functionality for making Views displays
+ // appear in contextual links is working again.
+ // $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.');
+ // $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.');
+ }
+
+ /**
+ * Get server-rendered contextual links for the given contextual link ids.
+ *
+ * Copied from \Drupal\contextual\Tests\ContextualDynamicContextTest::renderContextualLinks().
+ *
+ * @param array $ids
+ * An array of contextual link ids.
+ * @param string $current_path
+ * The Drupal path for the page for which the contextual links are rendered.
+ *
+ * @return string
+ * The response body.
+ */
+ protected function renderContextualLinks($ids, $current_path) {
+ // Build POST values.
+ $post = [];
+ for ($i = 0; $i < count($ids); $i++) {
+ $post['ids[' . $i . ']'] = $ids[$i];
+ }
+
+ // Serialize POST values.
+ foreach ($post as $key => $value) {
+ // Encode according to application/x-www-form-urlencoded
+ // Both names and values needs to be urlencoded, according to
+ // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+ $post[$key] = urlencode($key) . '=' . urlencode($value);
+ }
+ $post = implode('&', $post);
+
+ // Perform HTTP request.
+ return $this->curlExec([
+ CURLOPT_URL => \Drupal::url('contextual.render', [], ['absolute' => TRUE, 'query' => ['destination' => $current_path]]),
+ CURLOPT_POST => TRUE,
+ CURLOPT_POSTFIELDS => $post,
+ CURLOPT_HTTPHEADER => [
+ 'Accept: application/json',
+ 'Content-Type: application/x-www-form-urlencoded',
+ ],
+ ]);
+ }
+
+ /**
+ * Tests if the node page works if Contextual Links is disabled.
+ *
+ * All views have Contextual links enabled by default, even with the
+ * Contextual links module disabled. This tests if no calls are done to the
+ * Contextual links module by views when it is disabled.
+ *
+ * @see https://www.drupal.org/node/2379811
+ */
+ public function testPageWithDisabledContextualModule() {
+ \Drupal::service('module_installer')->uninstall(['contextual']);
+ \Drupal::service('module_installer')->install(['views_ui']);
+
+ // Ensure that contextual links don't get called for admin users.
+ $admin_user = User::load(1);
+ $admin_user->setPassword('new_password');
+ $admin_user->pass_raw = 'new_password';
+ $admin_user->save();
+
+ $this->drupalCreateContentType(['type' => 'page']);
+ $this->drupalCreateNode(['promote' => 1]);
+
+ $this->drupalLogin($admin_user);
+ $this->drupalGet('node');
+ }
+
+}