Browse Source

forgot some core files

Bachir Soussi Chiadmi 5 years ago
parent
commit
cc9fe30fc5

+ 471 - 0
core/modules/node/src/Tests/NodeRevisionsTest.php

@@ -0,0 +1,471 @@
+<?php
+
+namespace Drupal\node\Tests;
+
+use Drupal\Core\Url;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\NodeInterface;
+use Drupal\Component\Serialization\Json;
+
+/**
+ * Create a node with revisions and test viewing, saving, reverting, and
+ * deleting revisions for users with access for this content type.
+ *
+ * @group node
+ */
+class NodeRevisionsTest extends NodeTestBase {
+
+  /**
+   * An array of node revisions.
+   *
+   * @var \Drupal\node\NodeInterface[]
+   */
+  protected $nodes;
+
+  /**
+   * Revision log messages.
+   *
+   * @var array
+   */
+  protected $revisionLogs;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'contextual', 'datetime', 'language', 'content_translation'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Enable additional languages.
+    ConfigurableLanguage::createFromLangcode('de')->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 = '<li class="entitynodeedit-form"><a href="' . base_path() . 'node/' . $node->id() . '/edit">Edit</a></li>';
+    $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.');
+    $expected = '<li class="entitynodedelete-form"><a href="' . base_path() . 'node/' . $node->id() . '/delete">Delete</a></li>';
+    $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]], '<li class="entitynodeedit-form">'), 'The "Edit" contextual link is not shown for a non-default revision.');
+    $this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodedelete-form">'), '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();
+    }
+  }
+
+}

+ 259 - 0
core/modules/node/src/Tests/NodeTypeTest.php

@@ -0,0 +1,259 @@
+<?php
+
+namespace Drupal\node\Tests;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\node\Entity\NodeType;
+use Drupal\Core\Url;
+use Drupal\system\Tests\Menu\AssertBreadcrumbTrait;
+
+/**
+ * Ensures that node type functions work correctly.
+ *
+ * @group node
+ */
+class NodeTypeTest extends NodeTestBase {
+
+  use AssertBreadcrumbTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['field_ui', 'block'];
+
+  /**
+   * Ensures that node type functions (node_type_get_*) work correctly.
+   *
+   * Load available node types and validate the returned data.
+   */
+  public function testNodeTypeGetFunctions() {
+    $node_types = NodeType::loadMultiple();
+    $node_names = node_type_get_names();
+
+    $this->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('<front>')->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. <a href=":link">Add content type</a>.', [
+        ':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.');
+  }
+
+}

+ 475 - 0
core/modules/node/src/Tests/PagePreviewTest.php

@@ -0,0 +1,475 @@
+<?php
+
+namespace Drupal\node\Tests;
+
+use Drupal\comment\Tests\CommentTestTrait;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Url;
+use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\NodeType;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+
+/**
+ * Tests the node entity preview functionality.
+ *
+ * @group node
+ */
+class PagePreviewTest extends NodeTestBase {
+
+  use EntityReferenceTestTrait;
+  use CommentTestTrait;
+
+  /**
+   * Enable the comment, node and taxonomy modules to test on the preview.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'taxonomy', 'comment', 'image', 'file', 'text', 'node_test', 'menu_ui'];
+
+  /**
+   * The name of the created field.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  protected function setUp() {
+    parent::setUp();
+    $this->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] = '<em>' . $this->randomMachineName(8) . '</em>';
+    $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('<front>');
+    // 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]);
+  }
+
+}

+ 118 - 0
core/modules/node/src/Tests/Views/NodeContextualLinksTest.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\node\Tests\Views;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\user\Entity\User;
+
+/**
+ * Tests views contextual links on nodes.
+ *
+ * @group node
+ */
+class NodeContextualLinksTest extends NodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['contextual'];
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_contextual_links'];
+
+  /**
+   * Tests contextual links.
+   */
+  public function testNodeContextualLinks() {
+    $this->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');
+  }
+
+}