diff --git a/browsertest-convert.php b/browsertest-convert.php new file mode 100644 index 0000000..6869330 --- /dev/null +++ b/browsertest-convert.php @@ -0,0 +1,255 @@ +profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + + $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer news feeds', 'access news feeds', 'create article content')); + $this->drupalLogin($this->adminUser); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Creates an aggregator feed. + * + * This method simulates the form submission on path aggregator/sources/add. + * + * @param string $feed_url + * (optional) If given, feed will be created with this URL, otherwise + * /rss.xml will be used. Defaults to NULL. + * @param array $edit + * Array with additional form fields. + * + * @return \Drupal\aggregator\FeedInterface + * Full feed object if possible. + * + * @see getFeedEditArray() + */ + public function createFeed($feed_url = NULL, array $edit = array()) { + $edit = $this->getFeedEditArray($feed_url, $edit); + $this->drupalPostForm('aggregator/sources/add', $edit, t('Save')); + $this->assertText(t('The feed @name has been added.', array('@name' => $edit['title[0][value]'])), format_string('The feed @name has been added.', array('@name' => $edit['title[0][value]']))); + + // Verify that the creation message contains a link to a feed. + $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', array(':href' => 'aggregator/sources/')); + $this->assert(isset($view_link), 'The message area contains a link to a feed'); + + $fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']))->fetchField(); + $this->assertTrue(!empty($fid), 'The feed found in database.'); + return Feed::load($fid); + } + + /** + * Deletes an aggregator feed. + * + * @param \Drupal\aggregator\FeedInterface $feed + * Feed object representing the feed. + */ + public function deleteFeed(FeedInterface $feed) { + $this->drupalPostForm('aggregator/sources/' . $feed->id() . '/delete', array(), t('Delete')); + $this->assertRaw(t('The feed %title has been deleted.', array('%title' => $feed->label())), 'Feed deleted successfully.'); + } + + /** + * Returns a randomly generated feed edit array. + * + * @param string $feed_url + * (optional) If given, feed will be created with this URL, otherwise + * /rss.xml will be used. Defaults to NULL. + * @param array $edit + * Array with additional form fields. + * + * @return array + * A feed array. + */ + public function getFeedEditArray($feed_url = NULL, array $edit = array()) { + $feed_name = $this->randomMachineName(10); + if (!$feed_url) { + $feed_url = \Drupal::url('view.frontpage.feed_1', array(), array( + 'query' => array('feed' => $feed_name), + 'absolute' => TRUE, + )); + } + $edit += array( + 'title[0][value]' => $feed_name, + 'url[0][value]' => $feed_url, + 'refresh' => '900', + ); + return $edit; + } + + /** + * Returns a randomly generated feed edit object. + * + * @param string $feed_url + * (optional) If given, feed will be created with this URL, otherwise + * /rss.xml will be used. Defaults to NULL. + * @param array $values + * (optional) Default values to initialize object properties with. + * + * @return \Drupal\aggregator\FeedInterface + * A feed object. + */ + public function getFeedEditObject($feed_url = NULL, array $values = array()) { + $feed_name = $this->randomMachineName(10); + if (!$feed_url) { + $feed_url = \Drupal::url('view.frontpage.feed_1', array( + 'query' => array('feed' => $feed_name), + 'absolute' => TRUE, + )); + } + $values += array( + 'title' => $feed_name, + 'url' => $feed_url, + 'refresh' => '900', + ); + return Feed::create($values); + } + + /** + * Returns the count of the randomly created feed array. + * + * @return int + * Number of feed items on default feed created by createFeed(). + */ + public function getDefaultFeedItemCount() { + // Our tests are based off of rss.xml, so let's find out how many elements should be related. + $feed_count = db_query_range('SELECT COUNT(DISTINCT nid) FROM {node_field_data} n WHERE n.promote = 1 AND n.status = 1', 0, $this->config('system.rss')->get('items.limit'))->fetchField(); + return $feed_count > 10 ? 10 : $feed_count; + } + + /** + * Updates the feed items. + * + * This method simulates a click to + * admin/config/services/aggregator/update/$fid. + * + * @param \Drupal\aggregator\FeedInterface $feed + * Feed object representing the feed. + * @param int|null $expected_count + * Expected number of feed items. If omitted no check will happen. + */ + public function updateFeedItems(FeedInterface $feed, $expected_count = NULL) { + // First, let's ensure we can get to the rss xml. + $this->drupalGet($feed->getUrl()); + $this->assertResponse(200, format_string(':url is reachable.', array(':url' => $feed->getUrl()))); + + // Attempt to access the update link directly without an access token. + $this->drupalGet('admin/config/services/aggregator/update/' . $feed->id()); + $this->assertResponse(403); + + // Refresh the feed (simulated link click). + $this->drupalGet('admin/config/services/aggregator'); + $this->clickLink('Update items'); + + // Ensure we have the right number of items. + $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->id())); + $feed->items = array(); + foreach ($result as $item) { + $feed->items[] = $item->iid; + } + + if ($expected_count !== NULL) { + $feed->item_count = count($feed->items); + $this->assertEqual($expected_count, $feed->item_count, format_string('Total items in feed equal to the total items in database (@val1 != @val2)', array('@val1' => $expected_count, '@val2' => $feed->item_count))); + } + } + + /** + * Confirms an item removal from a feed. + * + * @param \Drupal\aggregator\FeedInterface $feed + * Feed object representing the feed. + */ + public function deleteFeedItems(FeedInterface $feed) { + $this->drupalPostForm('admin/config/services/aggregator/delete/' . $feed->id(), array(), t('Delete items')); + $this->assertRaw(t('The news items from %title have been deleted.', array('%title' => $feed->label())), 'Feed items deleted.'); + } + + /** + * Adds and deletes feed items and ensure that the count is zero. + * + * @param \Drupal\aggregator\FeedInterface $feed + * Feed object representing the feed. + * @param int $expected_count + * Expected number of feed items. + */ + public function updateAndDelete(FeedInterface $feed, $expected_count) { + $this->updateFeedItems($feed, $expected_count); + $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->id()))->fetchField(); + $this->assertTrue($count); + $this->deleteFeedItems($feed); + $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->id()))->fetchField(); + $this->assertTrue($count == 0); + } + + /** + * Checks whether the feed name and URL are unique. + * + * @param string $feed_name + * String containing the feed name to check. + * @param string $feed_url + * String containing the feed url to check. + * + * @return bool + * TRUE if feed is unique. + */ + public function uniqueFeed($feed_name, $feed_url) { + $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField(); + return (1 == $result); + } + + /** + * Creates a valid OPML file from an array of feeds. + * + * @param array $feeds + * An array of feeds. + * + * @return string + * Path to valid OPML file. + */ + public function getValidOpml(array $feeds) { + // Properly escape URLs so that XML parsers don't choke on them. + foreach ($feeds as &$feed) { + $feed['url[0][value]'] = Html::escape($feed['url[0][value]']); + } + /** + * Does not have an XML declaration, must pass the parser. + */ + $opml = << + + + + + + + + + + + + + + + + + +EOF; + + $path = 'public://valid-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + /** + * Creates an invalid OPML file. + * + * @return string + * Path to invalid OPML file. + */ + public function getInvalidOpml() { + $opml = << + + +EOF; + + $path = 'public://invalid-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + /** + * Creates a valid but empty OPML file. + * + * @return string + * Path to empty OPML file. + */ + public function getEmptyOpml() { + $opml = << + + + + + + + +EOF; + + $path = 'public://empty-opml.xml'; + return file_unmanaged_save_data($opml, $path); + } + + /** + * Returns a example RSS091 feed. + * + * @return string + * Path to the feed. + */ + public function getRSS091Sample() { + return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_rss091.xml'; + } + + /** + * Returns a example Atom feed. + * + * @return string + * Path to the feed. + */ + public function getAtomSample() { + // The content of this sample ATOM feed is based directly off of the + // example provided in RFC 4287. + return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_atom.xml'; + } + + /** + * Returns a example feed. + * + * @return string + * Path to the feed. + */ + public function getHtmlEntitiesSample() { + return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_title_entities.xml'; + } + + /** + * Creates sample article nodes. + * + * @param int $count + * (optional) The number of nodes to generate. Defaults to five. + */ + public function createSampleNodes($count = 5) { + // Post $count article nodes. + for ($i = 0; $i < $count; $i++) { + $edit = array(); + $edit['title[0][value]'] = $this->randomMachineName(); + $edit['body[0][value]'] = $this->randomMachineName(); + $this->drupalPostForm('node/add/article', $edit, t('Save')); + } + } + + /** + * Enable the plugins coming with aggregator_test module. + */ + public function enableTestPlugins() { + $this->config('aggregator.settings') + ->set('fetcher', 'aggregator_test_fetcher') + ->set('parser', 'aggregator_test_parser') + ->set('processors', array( + 'aggregator_test_processor' => 'aggregator_test_processor', + 'aggregator' => 'aggregator', + )) + ->save(); + } + +} diff --git a/core/modules/aggregator/src/Tests/DeleteFeedItemTest.php b/core/modules/aggregator/tests/src/Functional/DeleteFeedItemTest.php similarity index 96% rename from core/modules/aggregator/src/Tests/DeleteFeedItemTest.php rename to core/modules/aggregator/tests/src/Functional/DeleteFeedItemTest.php index 0261ed5..95202b8 100644 --- a/core/modules/aggregator/src/Tests/DeleteFeedItemTest.php +++ b/core/modules/aggregator/tests/src/Functional/DeleteFeedItemTest.php @@ -1,6 +1,6 @@ getRawContent(). + */ + protected function basicAuthGet($path, $username, $password, array $options = []) { + return $this->drupalGet($path, $options, $this->getBasicAuthHeaders($username, $password)); + } + + /** + * Executes a form submission using basic authentication. + * + * @param string $path + * Location of the post form. + * @param array $edit + * Field data in an associative array. + * @param string $submit + * Value of the submit button whose click is to be emulated. + * @param string $username + * The username to use for basic authentication. + * @param string $password + * The password to use for basic authentication. + * @param array $options + * Options to be forwarded to the url generator. + * @param string $form_html_id + * (optional) HTML ID of the form to be submitted. + * @param string $extra_post + * (optional) A string of additional data to append to the POST submission. + * + * @return string + * The retrieved HTML string. + * + * @see \Drupal\simpletest\WebTestBase::drupalPostForm() + */ + protected function basicAuthPostForm($path, $edit, $submit, $username, $password, array $options = array(), $form_html_id = NULL, $extra_post = NULL) { + return $this->drupalPostForm($path, $edit, $submit, $options, $this->getBasicAuthHeaders($username, $password), $form_html_id, $extra_post); + } + + /** + * Returns HTTP headers that can be used for basic authentication in Curl. + * + * @param string $username + * The username to use for basic authentication. + * @param string $password + * The password to use for basic authentication. + * + * @return array + * An array of raw request headers as used by curl_setopt(). + */ + protected function getBasicAuthHeaders($username, $password) { + // Set up Curl to use basic authentication with the test user's credentials. + return ['Authorization: Basic ' . base64_encode("$username:$password")]; + } + +} diff --git a/core/modules/block/src/Tests/BlockCacheTest.php b/core/modules/block/tests/src/Functional/BlockCacheTest.php similarity index 98% rename from core/modules/block/src/Tests/BlockCacheTest.php rename to core/modules/block/tests/src/Functional/BlockCacheTest.php index 8988580..9285c64 100644 --- a/core/modules/block/src/Tests/BlockCacheTest.php +++ b/core/modules/block/tests/src/Functional/BlockCacheTest.php @@ -1,16 +1,16 @@ autoCreateBasicBlockType) { + $this->createBlockContentType('basic', TRUE); + } + + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalPlaceBlock('local_actions_block'); + } + + /** + * Creates a custom block. + * + * @param bool|string $title + * (optional) Title of block. When no value is given uses a random name. + * Defaults to FALSE. + * @param string $bundle + * (optional) Bundle name. Defaults to 'basic'. + * @param bool $save + * (optional) Whether to save the block. Defaults to TRUE. + * + * @return \Drupal\block_content\Entity\BlockContent + * Created custom block. + */ + protected function createBlockContent($title = FALSE, $bundle = 'basic', $save = TRUE) { + $title = $title ?: $this->randomMachineName(); + $block_content = BlockContent::create(array( + 'info' => $title, + 'type' => $bundle, + 'langcode' => 'en' + )); + if ($block_content && $save === TRUE) { + $block_content->save(); + } + return $block_content; + } + + /** + * Creates a custom block type (bundle). + * + * @param string $label + * The block type label. + * @param bool $create_body + * Whether or not to create the body field + * + * @return \Drupal\block_content\Entity\BlockContentType + * Created custom block type. + */ + protected function createBlockContentType($label, $create_body = FALSE) { + $bundle = BlockContentType::create(array( + 'id' => $label, + 'label' => $label, + 'revision' => FALSE, + )); + $bundle->save(); + if ($create_body) { + block_content_add_body_field($bundle->id()); + } + return $bundle; + } + +} diff --git a/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php b/core/modules/block_content/tests/src/Functional/BlockContentTranslationUITest.php similarity index 99% rename from core/modules/block_content/src/Tests/BlockContentTranslationUITest.php rename to core/modules/block_content/tests/src/Functional/BlockContentTranslationUITest.php index 332bf3d..2628079 100644 --- a/core/modules/block_content/src/Tests/BlockContentTranslationUITest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentTranslationUITest.php @@ -1,6 +1,6 @@ drupalCreateContentType(array('type' => 'article', 'name' => t('Article'))); + } + + // Create two test users. + $this->adminUser = $this->drupalCreateUser(array( + 'administer content types', + 'administer comments', + 'administer comment types', + 'administer comment fields', + 'administer comment display', + 'skip comment approval', + 'post comments', + 'access comments', + // Usernames aren't shown in comment edit form autocomplete unless this + // permission is granted. + 'access user profiles', + 'access content', + )); + $this->webUser = $this->drupalCreateUser(array( + 'access comments', + 'post comments', + 'create article content', + 'edit own comments', + 'skip comment approval', + 'access content', + )); + + // Create comment field on article. + $this->addDefaultCommentField('node', 'article'); + + // Create a test node authored by the web user. + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->webUser->id())); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Posts a comment. + * + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * Node to post comment on or NULL to post to the previously loaded page. + * @param string $comment + * Comment body. + * @param string $subject + * Comment subject. + * @param string $contact + * Set to NULL for no contact info, TRUE to ignore success checking, and + * array of values to set contact info. + * @param string $field_name + * (optional) Field name through which the comment should be posted. + * Defaults to 'comment'. + * + * @return \Drupal\comment\CommentInterface|null + * The posted comment or NULL when posted comment was not found. + */ + public function postComment($entity, $comment, $subject = '', $contact = NULL, $field_name = 'comment') { + $edit = array(); + $edit['comment_body[0][value]'] = $comment; + + if ($entity !== NULL) { + $field = FieldConfig::loadByName($entity->getEntityTypeId(), $entity->bundle(), $field_name); + } + else { + $field = FieldConfig::loadByName('node', 'article', $field_name); + } + $preview_mode = $field->getSetting('preview'); + + // Must get the page before we test for fields. + if ($entity !== NULL) { + $this->drupalGet('comment/reply/' . $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $field_name); + } + + // Determine the visibility of subject form field. + if (entity_get_form_display('comment', 'comment', 'default')->getComponent('subject')) { + // Subject input allowed. + $edit['subject[0][value]'] = $subject; + } + else { + $this->assertNoFieldByName('subject[0][value]', '', 'Subject field not found.'); + } + + if ($contact !== NULL && is_array($contact)) { + $edit += $contact; + } + switch ($preview_mode) { + case DRUPAL_REQUIRED: + // Preview required so no save button should be found. + $this->assertNoFieldByName('op', t('Save'), 'Save button not found.'); + $this->drupalPostForm(NULL, $edit, t('Preview')); + // Don't break here so that we can test post-preview field presence and + // function below. + case DRUPAL_OPTIONAL: + $this->assertFieldByName('op', t('Preview'), 'Preview button found.'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->drupalPostForm(NULL, $edit, t('Save')); + break; + + case DRUPAL_DISABLED: + $this->assertNoFieldByName('op', t('Preview'), 'Preview button not found.'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->drupalPostForm(NULL, $edit, t('Save')); + break; + } + $match = array(); + // Get comment ID + preg_match('/#comment-([0-9]+)/', $this->getURL(), $match); + + // Get comment. + if ($contact !== TRUE) { // If true then attempting to find error message. + if ($subject) { + $this->assertText($subject, 'Comment subject posted.'); + } + $this->assertText($comment, 'Comment body posted.'); + $this->assertTrue((!empty($match) && !empty($match[1])), 'Comment id found.'); + } + + if (isset($match[1])) { + \Drupal::entityManager()->getStorage('comment')->resetCache(array($match[1])); + return Comment::load($match[1]); + } + } + + /** + * Checks current page for specified comment. + * + * @param \Drupal\comment\CommentInterface $comment + * The comment object. + * @param bool $reply + * Boolean indicating whether the comment is a reply to another comment. + * + * @return bool + * Boolean indicating whether the comment was found. + */ + function commentExists(CommentInterface $comment = NULL, $reply = FALSE) { + if ($comment) { + $comment_element = $this->cssSelect('.comment-wrapper ' . ($reply ? '.indented ' : '') . '#comment-' . $comment->id() . ' ~ article'); + if (empty($comment_element)) { + return FALSE; + } + + $comment_title = $comment_element[0]->xpath('div/h3/a'); + if (empty($comment_title) || ((string)$comment_title[0]) !== $comment->getSubject()) { + return FALSE; + } + + $comment_body = $comment_element[0]->xpath('div/div/p'); + if (empty($comment_body) || ((string)$comment_body[0]) !== $comment->comment_body->value) { + return FALSE; + } + + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Deletes a comment. + * + * @param \Drupal\comment\CommentInterface $comment + * Comment to delete. + */ + function deleteComment(CommentInterface $comment) { + $this->drupalPostForm('comment/' . $comment->id() . '/delete', array(), t('Delete')); + $this->assertText(t('The comment and all its replies have been deleted.'), 'Comment deleted.'); + } + + /** + * Sets the value governing whether the subject field should be enabled. + * + * @param bool $enabled + * Boolean specifying whether the subject field should be enabled. + */ + public function setCommentSubject($enabled) { + $form_display = entity_get_form_display('comment', 'comment', 'default'); + if ($enabled) { + $form_display->setComponent('subject', array( + 'type' => 'string_textfield', + )); + } + else { + $form_display->removeComponent('subject'); + } + $form_display->save(); + // Display status message. + $this->pass('Comment subject ' . ($enabled ? 'enabled' : 'disabled') . '.'); + } + + /** + * Sets the value governing the previewing mode for the comment form. + * + * @param int $mode + * The preview mode: DRUPAL_DISABLED, DRUPAL_OPTIONAL or DRUPAL_REQUIRED. + * @param string $field_name + * (optional) Field name through which the comment should be posted. + * Defaults to 'comment'. + */ + public function setCommentPreview($mode, $field_name = 'comment') { + switch ($mode) { + case DRUPAL_DISABLED: + $mode_text = 'disabled'; + break; + + case DRUPAL_OPTIONAL: + $mode_text = 'optional'; + break; + + case DRUPAL_REQUIRED: + $mode_text = 'required'; + break; + } + $this->setCommentSettings('preview', $mode, format_string('Comment preview @mode_text.', array('@mode_text' => $mode_text)), $field_name); + } + + /** + * Sets the value governing whether the comment form is on its own page. + * + * @param bool $enabled + * TRUE if the comment form should be displayed on the same page as the + * comments; FALSE if it should be displayed on its own page. + * @param string $field_name + * (optional) Field name through which the comment should be posted. + * Defaults to 'comment'. + */ + public function setCommentForm($enabled, $field_name = 'comment') { + $this->setCommentSettings('form_location', ($enabled ? CommentItemInterface::FORM_BELOW : CommentItemInterface::FORM_SEPARATE_PAGE), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.', $field_name); + } + + /** + * Sets the value governing restrictions on anonymous comments. + * + * @param int $level + * The level of the contact information allowed for anonymous comments: + * - 0: No contact information allowed. + * - 1: Contact information allowed but not required. + * - 2: Contact information required. + */ + function setCommentAnonymous($level) { + $this->setCommentSettings('anonymous', $level, format_string('Anonymous commenting set to level @level.', array('@level' => $level))); + } + + /** + * Sets the value specifying the default number of comments per page. + * + * @param int $number + * Comments per page value. + * @param string $field_name + * (optional) Field name through which the comment should be posted. + * Defaults to 'comment'. + */ + public function setCommentsPerPage($number, $field_name = 'comment') { + $this->setCommentSettings('per_page', $number, format_string('Number of comments per page set to @number.', array('@number' => $number)), $field_name); + } + + /** + * Sets a comment settings variable for the article content type. + * + * @param string $name + * Name of variable. + * @param string $value + * Value of variable. + * @param string $message + * Status message to display. + * @param string $field_name + * (optional) Field name through which the comment should be posted. + * Defaults to 'comment'. + */ + public function setCommentSettings($name, $value, $message, $field_name = 'comment') { + $field = FieldConfig::loadByName('node', 'article', $field_name); + $field->setSetting($name, $value); + $field->save(); + // Display status message. + $this->pass($message); + } + + /** + * Checks whether the commenter's contact information is displayed. + * + * @return bool + * Contact info is available. + */ + function commentContactInfoAvailable() { + return preg_match('/(input).*?(name="name").*?(input).*?(name="mail").*?(input).*?(name="homepage")/s', $this->getRawContent()); + } + + /** + * Performs the specified operation on the specified comment. + * + * @param \Drupal\comment\CommentInterface $comment + * Comment to perform operation on. + * @param string $operation + * Operation to perform. + * @param bool $approval + * Operation is found on approval page. + */ + function performCommentOperation(CommentInterface $comment, $operation, $approval = FALSE) { + $edit = array(); + $edit['operation'] = $operation; + $edit['comments[' . $comment->id() . ']'] = TRUE; + $this->drupalPostForm('admin/content/comment' . ($approval ? '/approval' : ''), $edit, t('Update')); + + if ($operation == 'delete') { + $this->drupalPostForm(NULL, array(), t('Delete comments')); + $this->assertRaw(\Drupal::translation()->formatPlural(1, 'Deleted 1 comment.', 'Deleted @count comments.'), format_string('Operation "@operation" was performed on comment.', array('@operation' => $operation))); + } + else { + $this->assertText(t('The update has been performed.'), format_string('Operation "@operation" was performed on comment.', array('@operation' => $operation))); + } + } + + /** + * Gets the comment ID for an unapproved comment. + * + * @param string $subject + * Comment subject to find. + * + * @return int + * Comment id. + */ + function getUnapprovedComment($subject) { + $this->drupalGet('admin/content/comment/approval'); + preg_match('/href="(.*?)#comment-([^"]+)"(.*?)>(' . $subject . ')/', $this->getRawContent(), $match); + + return $match[2]; + } + + /** + * Creates a comment comment type (bundle). + * + * @param string $label + * The comment type label. + * + * @return \Drupal\comment\Entity\CommentType + * Created comment type. + */ + protected function createCommentType($label) { + $bundle = CommentType::create(array( + 'id' => $label, + 'label' => $label, + 'description' => '', + 'target_entity_type_id' => 'node', + )); + $bundle->save(); + return $bundle; + } + +} diff --git a/core/modules/comment/tests/src/Functional/CommentTestTrait.php b/core/modules/comment/tests/src/Functional/CommentTestTrait.php new file mode 100644 index 0000000..7536a039 --- /dev/null +++ b/core/modules/comment/tests/src/Functional/CommentTestTrait.php @@ -0,0 +1,125 @@ +getStorage('comment_type'); + if ($comment_type = $comment_type_storage->load($comment_type_id)) { + if ($comment_type->getTargetEntityTypeId() !== $entity_type) { + throw new \InvalidArgumentException("The given comment type id $comment_type_id can only be used with the $entity_type entity type"); + } + } + else { + $comment_type_storage->create(array( + 'id' => $comment_type_id, + 'label' => Unicode::ucfirst($comment_type_id), + 'target_entity_type_id' => $entity_type, + 'description' => 'Default comment field', + ))->save(); + } + // Add a body field to the comment type. + \Drupal::service('comment.manager')->addBodyField($comment_type_id); + + // Add a comment field to the host entity type. Create the field storage if + // needed. + if (!array_key_exists($field_name, $entity_manager->getFieldStorageDefinitions($entity_type))) { + $entity_manager->getStorage('field_storage_config')->create(array( + 'entity_type' => $entity_type, + 'field_name' => $field_name, + 'type' => 'comment', + 'translatable' => TRUE, + 'settings' => array( + 'comment_type' => $comment_type_id, + ), + ))->save(); + } + // Create the field if needed, and configure its form and view displays. + if (!array_key_exists($field_name, $entity_manager->getFieldDefinitions($entity_type, $bundle))) { + $entity_manager->getStorage('field_config')->create(array( + 'label' => 'Comments', + 'description' => '', + 'field_name' => $field_name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'required' => 1, + 'default_value' => array( + array( + 'status' => $default_value, + 'cid' => 0, + 'last_comment_name' => '', + 'last_comment_timestamp' => 0, + 'last_comment_uid' => 0, + ), + ), + ))->save(); + + // Entity form displays: assign widget settings for the 'default' form + // mode, and hide the field in all other form modes. + entity_get_form_display($entity_type, $bundle, 'default') + ->setComponent($field_name, array( + 'type' => 'comment_default', + 'weight' => 20, + )) + ->save(); + foreach ($entity_manager->getFormModes($entity_type) as $id => $form_mode) { + $display = entity_get_form_display($entity_type, $bundle, $id); + // Only update existing displays. + if ($display && !$display->isNew()) { + $display->removeComponent($field_name)->save(); + } + } + + // Entity view displays: assign widget settings for the 'default' view + // mode, and hide the field in all other view modes. + entity_get_display($entity_type, $bundle, 'default') + ->setComponent($field_name, array( + 'label' => 'above', + 'type' => 'comment_default', + 'weight' => 20, + 'settings' => array('view_mode' => $comment_view_mode), + )) + ->save(); + foreach ($entity_manager->getViewModes($entity_type) as $id => $view_mode) { + $display = entity_get_display($entity_type, $bundle, $id); + // Only update existing displays. + if ($display && !$display->isNew()) { + $display->removeComponent($field_name)->save(); + } + } + } + } + +} diff --git a/core/modules/comment/src/Tests/CommentThreadingTest.php b/core/modules/comment/tests/src/Functional/CommentThreadingTest.php similarity index 99% rename from core/modules/comment/src/Tests/CommentThreadingTest.php rename to core/modules/comment/tests/src/Functional/CommentThreadingTest.php index c8ea93e..82b4342 100644 --- a/core/modules/comment/src/Tests/CommentThreadingTest.php +++ b/core/modules/comment/tests/src/Functional/CommentThreadingTest.php @@ -1,6 +1,6 @@ uuid(); + $entity_type_id = $entity->getEntityTypeId(); + $original_data = $entity->toArray(); + // Copy everything to sync. + $this->copyConfig(\Drupal::service('config.storage'), \Drupal::service('config.storage.sync')); + // Delete the configuration from active. Don't worry about side effects of + // deleting config like fields cleaning up field storages. The coming import + // should recreate everything as necessary. + $entity->delete(); + $this->configImporter()->reset()->import(); + $imported_entity = \Drupal::entityManager()->loadEntityByUuid($entity_type_id, $entity_uuid); + $this->assertIdentical($original_data, $imported_entity->toArray()); + } + +} diff --git a/core/modules/config/src/Tests/CacheabilityMetadataConfigOverrideIntegrationTest.php b/core/modules/config/tests/src/Functional/CacheabilityMetadataConfigOverrideIntegrationTest.php similarity index 94% rename from core/modules/config/src/Tests/CacheabilityMetadataConfigOverrideIntegrationTest.php rename to core/modules/config/tests/src/Functional/CacheabilityMetadataConfigOverrideIntegrationTest.php index 3ef0754..09aea50 100644 --- a/core/modules/config/src/Tests/CacheabilityMetadataConfigOverrideIntegrationTest.php +++ b/core/modules/config/tests/src/Functional/CacheabilityMetadataConfigOverrideIntegrationTest.php @@ -1,15 +1,15 @@ checkConfigSchema($typed_config, $config_name, $config_data); + if ($errors === FALSE) { + // @todo Since the use of this trait is under TestBase, it works. + // Can be fixed as part of https://www.drupal.org/node/2260053. + $this->fail(SafeMarkup::format('No schema for @config_name', array('@config_name' => $config_name))); + return; + } + elseif ($errors === TRUE) { + // @todo Since the use of this trait is under TestBase, it works. + // Can be fixed as part of https://www.drupal.org/node/2260053. + $this->pass(SafeMarkup::format('Schema found for @config_name and values comply with schema.', array('@config_name' => $config_name))); + } + else { + foreach ($errors as $key => $error) { + // @todo Since the use of this trait is under TestBase, it works. + // Can be fixed as part of https://www.drupal.org/node/2260053. + $this->fail(SafeMarkup::format('Schema key @key failed with: @error', array('@key' => $key, '@error' => $error))); + } + } + } + + /** + * Asserts configuration, specified by name, has a valid schema. + * + * @param string $config_name + * The configuration name. + */ + public function assertConfigSchemaByName($config_name) { + $config = $this->config($config_name); + $this->assertConfigSchema(\Drupal::service('config.typed'), $config->getName(), $config->get()); + } + +} diff --git a/core/modules/config/src/Tests/SchemaConfigListenerWebTest.php b/core/modules/config/tests/src/Functional/SchemaConfigListenerWebTest.php similarity index 93% rename from core/modules/config/src/Tests/SchemaConfigListenerWebTest.php rename to core/modules/config/tests/src/Functional/SchemaConfigListenerWebTest.php index 45aabc8..4104606 100644 --- a/core/modules/config/src/Tests/SchemaConfigListenerWebTest.php +++ b/core/modules/config/tests/src/Functional/SchemaConfigListenerWebTest.php @@ -1,16 +1,16 @@ adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']); + $this->drupalPlaceBlock('page_title_block'); + $this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']); + } + + /** + * Creates a content-type from the UI. + * + * @param string $content_type_name + * Content type human name. + * @param string $content_type_id + * Machine name. + * @param bool $moderated + * TRUE if should be moderated. + * @param string[] $allowed_states + * Array of allowed state IDs. + * @param string $default_state + * Default state. + */ + protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) { + $this->drupalGet('admin/structure/types'); + $this->clickLink('Add content type'); + $edit = [ + 'name' => $content_type_name, + 'type' => $content_type_id, + ]; + $this->drupalPostForm(NULL, $edit, t('Save content type')); + + if ($moderated) { + $this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state); + } + } + + /** + * Enable moderation for a specified content type, using the UI. + * + * @param string $content_type_id + * Machine name. + * @param string[] $allowed_states + * Array of allowed state IDs. + * @param string $default_state + * Default state. + */ + protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) { + $this->drupalGet('admin/structure/types'); + $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation'); + $this->drupalGet('admin/structure/types/manage/' . $content_type_id); + $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation'); + $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation'); + $this->assertFieldByName('enable_moderation_state'); + $this->assertNoFieldChecked('edit-enable-moderation-state'); + + $edit['enable_moderation_state'] = 1; + + /** @var ModerationState $state */ + foreach (ModerationState::loadMultiple() as $state) { + $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']'; + $edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE; + } + + $edit['default_moderation_state'] = $default_state; + + $this->drupalPostForm(NULL, $edit, t('Save')); + } + + /** + * Grants given user permission to create content of given type. + * + * @param \Drupal\Core\Session\AccountInterface $account + * User to grant permission to. + * @param string $content_type_id + * Content type ID. + */ + protected function grantUserPermissionToCreateContentOfType(AccountInterface $account, $content_type_id) { + $role_ids = $account->getRoles(TRUE); + /* @var \Drupal\user\RoleInterface $role */ + $role_id = reset($role_ids); + $role = Role::load($role_id); + $role->grantPermission(sprintf('create %s content', $content_type_id)); + $role->grantPermission(sprintf('edit any %s content', $content_type_id)); + $role->grantPermission(sprintf('delete any %s content', $content_type_id)); + $role->save(); + } + +} diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateTransitionsTest.php similarity index 98% rename from core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php rename to core/modules/content_moderation/tests/src/Functional/ModerationStateTransitionsTest.php index 0495e48..e7b0842 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateTransitionsTest.php @@ -1,6 +1,6 @@ setupLanguages(); + $this->setupBundle(); + $this->enableTranslation(); + $this->setupUsers(); + $this->setupTestFields(); + + $this->manager = $this->container->get('content_translation.manager'); + $this->controller = $this->manager->getTranslationHandler($this->entityTypeId); + + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + } + + /** + * Adds additional languages. + */ + protected function setupLanguages() { + $this->langcodes = array('it', 'fr'); + foreach ($this->langcodes as $langcode) { + ConfigurableLanguage::createFromLangcode($langcode)->save(); + } + array_unshift($this->langcodes, \Drupal::languageManager()->getDefaultLanguage()->getId()); + } + + /** + * Returns an array of permissions needed for the translator. + */ + protected function getTranslatorPermissions() { + return array_filter(array($this->getTranslatePermission(), 'create content translations', 'update content translations', 'delete content translations')); + } + + /** + * Returns the translate permissions for the current entity and bundle. + */ + protected function getTranslatePermission() { + $entity_type = \Drupal::entityManager()->getDefinition($this->entityTypeId); + if ($permission_granularity = $entity_type->getPermissionGranularity()) { + return $permission_granularity == 'bundle' ? "translate {$this->bundle} {$this->entityTypeId}" : "translate {$this->entityTypeId}"; + } + } + + /** + * Returns an array of permissions needed for the editor. + */ + protected function getEditorPermissions() { + // Every entity-type-specific test needs to define these. + return array(); + } + + /** + * Returns an array of permissions needed for the administrator. + */ + protected function getAdministratorPermissions() { + return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), array('administer content translation')); + } + + /** + * Creates and activates translator, editor and admin users. + */ + protected function setupUsers() { + $this->translator = $this->drupalCreateUser($this->getTranslatorPermissions(), 'translator'); + $this->editor = $this->drupalCreateUser($this->getEditorPermissions(), 'editor'); + $this->administrator = $this->drupalCreateUser($this->getAdministratorPermissions(), 'administrator'); + $this->drupalLogin($this->translator); + } + + /** + * Creates or initializes the bundle date if needed. + */ + protected function setupBundle() { + if (empty($this->bundle)) { + $this->bundle = $this->entityTypeId; + } + } + + /** + * Enables translation for the current entity type and bundle. + */ + protected function enableTranslation() { + // Enable translation for the current entity type and ensure the change is + // picked up. + \Drupal::service('content_translation.manager')->setEnabled($this->entityTypeId, $this->bundle, TRUE); + drupal_static_reset(); + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('router.builder')->rebuild(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + } + + /** + * Creates the test fields. + */ + protected function setupTestFields() { + if (empty($this->fieldName)) { + $this->fieldName = 'field_test_et_ui_test'; + } + FieldStorageConfig::create(array( + 'field_name' => $this->fieldName, + 'type' => 'string', + 'entity_type' => $this->entityTypeId, + 'cardinality' => 1, + ))->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => $this->fieldName, + 'bundle' => $this->bundle, + 'label' => 'Test translatable text-field', + ])->save(); + entity_get_form_display($this->entityTypeId, $this->bundle, 'default') + ->setComponent($this->fieldName, array( + 'type' => 'string_textfield', + 'weight' => 0, + )) + ->save(); + } + + /** + * Creates the entity to be translated. + * + * @param array $values + * An array of initial values for the entity. + * @param string $langcode + * The initial language code of the entity. + * @param string $bundle_name + * (optional) The entity bundle, if the entity uses bundles. Defaults to + * NULL. If left NULL, $this->bundle will be used. + * + * @return string + * The entity id. + */ + protected function createEntity($values, $langcode, $bundle_name = NULL) { + $entity_values = $values; + $entity_values['langcode'] = $langcode; + $entity_type = \Drupal::entityManager()->getDefinition($this->entityTypeId); + if ($bundle_key = $entity_type->getKey('bundle')) { + $entity_values[$bundle_key] = $bundle_name ?: $this->bundle; + } + $controller = $this->container->get('entity.manager')->getStorage($this->entityTypeId); + if (!($controller instanceof SqlContentEntityStorage)) { + foreach ($values as $property => $value) { + if (is_array($value)) { + $entity_values[$property] = array($langcode => $value); + } + } + } + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId) + ->create($entity_values); + $entity->save(); + return $entity->id(); + } + +} diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUISkipTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationUISkipTest.php similarity index 85% rename from core/modules/content_translation/src/Tests/ContentTranslationUISkipTest.php rename to core/modules/content_translation/tests/src/Functional/ContentTranslationUISkipTest.php index 2f8be23..74d5de5 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationUISkipTest.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUISkipTest.php @@ -1,15 +1,15 @@ doTestBasicTranslation(); + $this->doTestTranslationOverview(); + $this->doTestOutdatedStatus(); + $this->doTestPublishedStatus(); + $this->doTestAuthoringInfo(); + $this->doTestTranslationEdit(); + $this->doTestTranslationChanged(); + $this->doTestChangedTimeAfterSaveWithoutChanges(); + $this->doTestTranslationDeletion(); + } + + /** + * Tests the basic translation workflow. + */ + protected function doTestBasicTranslation() { + // Create a new test entity with original values in the default language. + $default_langcode = $this->langcodes[0]; + $values[$default_langcode] = $this->getNewEntityValues($default_langcode); + // Create the entity with the editor as owner, so that afterwards a new + // translation is created by the translator and the translation author is + // tested. + $this->drupalLogin($this->editor); + $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); + $this->drupalLogin($this->translator); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertTrue($entity, 'Entity found in the database.'); + $this->drupalGet($entity->urlInfo()); + $this->assertResponse(200, 'Entity URL is valid.'); + + // Ensure that the content language cache context is not yet added to the + // page. + $this->assertCacheContexts($this->defaultCacheContexts); + + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertNoText('Source language', 'Source language column correctly hidden.'); + + $translation = $this->getTranslation($entity, $default_langcode); + foreach ($values[$default_langcode] as $property => $value) { + $stored_value = $this->getValue($translation, $property, $default_langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('@property correctly stored in the default language.', array('@property' => $property)); + $this->assertEqual($stored_value, $value, $message); + } + + // Add a content translation. + $langcode = 'it'; + $language = ConfigurableLanguage::load($langcode); + $values[$langcode] = $this->getNewEntityValues($langcode); + + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $default_langcode, + 'target' => $langcode + ], array('language' => $language)); + $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode)); + + // Assert that HTML is escaped in "all languages" in UI after SafeMarkup + // change. + if ($this->testHTMLEscapeForAllLanguages) { + $this->assertNoRaw('<span class="translation-entity-all-languages">(all languages)</span>'); + $this->assertRaw('(all languages)'); + } + + // Ensure that the content language cache context is not yet added to the + // page. + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo()); + $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts)); + + // Reset the cache of the entity, so that the new translation gets the + // updated values. + $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode)); + $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + + $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid'; + if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) { + $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(), + SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', array('@langcode' => $langcode))); + + $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(), + SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.', + array('@target' => $langcode, '@source' => $default_langcode))); + } + else { + $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), 'Author of the entity remained untouched after translation for non translatable owner field.'); + } + + $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created'; + if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) { + $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(), + SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.', + array('@target' => $langcode, '@source' => $default_langcode))); + } + else { + $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.'); + } + + if ($this->testLanguageSelector) { + $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.'); + } + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertNoText('Source language', 'Source language column correctly hidden.'); + + // Switch the source language. + $langcode = 'fr'; + $language = ConfigurableLanguage::load($langcode); + $source_langcode = 'it'; + $edit = array('source_langcode[source]' => $source_langcode); + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $default_langcode, + 'target' => $langcode + ], array('language' => $language)); + // This does not save anything, it merely reloads the form and fills in the + // fields with the values from the different source language. + $this->drupalPostForm($add_url, $edit, t('Change')); + $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.'); + + // Add another translation and mark the other ones as outdated. + $values[$langcode] = $this->getNewEntityValues($langcode); + $edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE); + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $source_langcode, + 'target' => $langcode + ], array('language' => $language)); + $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode)); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertText('Source language', 'Source language column correctly shown.'); + + // Check that the entered values have been correctly stored. + foreach ($values as $langcode => $property_values) { + $translation = $this->getTranslation($entity, $langcode); + foreach ($property_values as $property => $value) { + $stored_value = $this->getValue($translation, $property, $langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode)); + $this->assertEqual($stored_value, $value, $message); + } + } + } + + /** + * Tests that the translation overview shows the correct values. + */ + protected function doTestTranslationOverview() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $translate_url = $entity->urlInfo('drupal:content-translation-overview'); + $this->drupalGet($translate_url); + $translate_url->setAbsolute(FALSE); + + foreach ($this->langcodes as $langcode) { + if ($entity->hasTranslation($langcode)) { + $language = new Language(array('id' => $langcode)); + $view_url = $entity->url('canonical', ['language' => $language]); + $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]); + $this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode))); + $edit_path = $entity->url('edit-form', array('language' => $language)); + $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path)); + $this->assertEqual((string) $elements[0], t('Edit'), format_string('Edit link correct for %language translation.', array('%language' => $langcode))); + } + } + } + + /** + * Tests up-to-date status tracking. + */ + protected function doTestOutdatedStatus() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $langcode = 'fr'; + $languages = \Drupal::languageManager()->getLanguages(); + + // Mark translations as outdated. + $edit = array('content_translation[retranslate]' => TRUE); + $edit_path = $entity->urlInfo('edit-form', array('language' => $languages[$langcode])); + $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode)); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + + // Check that every translation has the correct "outdated" status, and that + // the Translation fieldset is open if the translation is "outdated". + foreach ($this->langcodes as $added_langcode) { + $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($added_langcode))); + $this->drupalGet($url); + if ($added_langcode == $langcode) { + $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.'); + $this->assertFalse($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab should be collapsed by default.'); + } + else { + $this->assertFieldByXPath('//input[@name="content_translation[outdated]"]', TRUE, 'The translate flag is checked by default.'); + $this->assertTrue($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab is correctly expanded when the translation is outdated.'); + $edit = array('content_translation[outdated]' => FALSE); + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $added_langcode)); + $this->drupalGet($url); + $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.'); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.'); + } + } + } + + /** + * Tests the translation publishing status. + */ + protected function doTestPublishedStatus() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + + // Unpublish translations. + foreach ($this->langcodes as $index => $langcode) { + if ($index > 0) { + $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode))); + $edit = array('content_translation[status]' => FALSE); + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode)); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.'); + } + } + + // Check that the last published translation cannot be unpublished. + $this->drupalGet($entity->urlInfo('edit-form')); + $this->assertFieldByXPath('//input[@name="content_translation[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.'); + } + + /** + * Tests the translation authoring information. + */ + protected function doTestAuthoringInfo() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $values = array(); + + // Post different authoring information for each translation. + foreach ($this->langcodes as $index => $langcode) { + $user = $this->drupalCreateUser(); + $values[$langcode] = array( + 'uid' => $user->id(), + 'created' => REQUEST_TIME - mt_rand(0, 1000), + ); + $edit = array( + 'content_translation[uid]' => $user->getUsername(), + 'content_translation[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'), + ); + $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode))); + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode)); + } + + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + foreach ($this->langcodes as $langcode) { + $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.'); + $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.'); + } + + // Try to post non valid values and check that they are rejected. + $langcode = 'en'; + $edit = array( + // User names have by default length 8. + 'content_translation[uid]' => $this->randomMachineName(12), + 'content_translation[created]' => '19/11/1978', + ); + $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode)); + $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.'); + $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.'); + $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.'); + } + + /** + * Tests translation deletion. + */ + protected function doTestTranslationDeletion() { + // Confirm and delete a translation. + $this->drupalLogin($this->translator); + $langcode = 'fr'; + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $language = ConfigurableLanguage::load($langcode); + $url = $entity->urlInfo('edit-form', array('language' => $language)); + $this->drupalPostForm($url, array(), t('Delete translation')); + $this->drupalPostForm(NULL, array(), t('Delete @language translation', array('@language' => $language->getName()))); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId, TRUE); + if ($this->assertTrue(is_object($entity), 'Entity found')) { + $translations = $entity->getTranslationLanguages(); + $this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.'); + } + + // Check that the translator cannot delete the original translation. + $args = [$this->entityTypeId => $entity->id(), 'language' => 'en']; + $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args)); + $this->assertResponse(403); + } + + /** + * Returns an array of entity field values to be tested. + */ + protected function getNewEntityValues($langcode) { + return array($this->fieldName => array(array('value' => $this->randomMachineName(16)))); + } + + /** + * Returns an edit array containing the values to be posted. + */ + protected function getEditValues($values, $langcode, $new = FALSE) { + $edit = $values[$langcode]; + $langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode; + foreach ($values[$langcode] as $property => $value) { + if (is_array($value)) { + $edit["{$property}[0][value]"] = $value[0]['value']; + unset($edit[$property]); + } + } + return $edit; + } + + /** + * Returns the form action value when submitting a new translation. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Name of the button to hit. + */ + protected function getFormSubmitActionForNewTranslation(EntityInterface $entity, $langcode) { + $entity->addTranslation($langcode, $entity->toArray()); + return $this->getFormSubmitAction($entity, $langcode); + } + + /** + * Returns the form action value to be used to submit the entity form. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Name of the button to hit. + */ + protected function getFormSubmitAction(EntityInterface $entity, $langcode) { + return t('Save') . $this->getFormSubmitSuffix($entity, $langcode); + } + + /** + * Returns appropriate submit button suffix based on translatability. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Submit button suffix based on translatability. + */ + protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) { + return ''; + } + + /** + * Returns the translation object to use to retrieve the translated values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * The language code identifying the translation to be retrieved. + * + * @return \Drupal\Core\TypedData\TranslatableInterface + * The translation object to act on. + */ + protected function getTranslation(EntityInterface $entity, $langcode) { + return $entity->getTranslation($langcode); + } + + /** + * Returns the value for the specified property in the given language. + * + * @param \Drupal\Core\Entity\EntityInterface $translation + * The translation object the property value should be retrieved from. + * @param string $property + * The property name. + * @param string $langcode + * The property value. + * + * @return + * The property value. + */ + protected function getValue(EntityInterface $translation, $property, $langcode) { + $key = $property == 'user_id' ? 'target_id' : 'value'; + return $translation->get($property)->{$key}; + } + + /** + * Returns the name of the field that implements the changed timestamp. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * + * @return string + * The field name. + */ + protected function getChangedFieldName($entity) { + return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed'; + } + + /** + * Tests edit content translation. + */ + protected function doTestTranslationEdit() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $languages = $this->container->get('language_manager')->getLanguages(); + + foreach ($this->langcodes as $langcode) { + // We only want to test the title for non-english translations. + if ($langcode != 'en') { + $options = array('language' => $languages[$langcode]); + $url = $entity->urlInfo('edit-form', $options); + $this->drupalGet($url); + + $this->assertRaw($entity->getTranslation($langcode)->label()); + } + } + } + + /** + * Tests the basic translation workflow. + */ + protected function doTestTranslationChanged() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $changed_field_name = $this->getChangedFieldName($entity); + $definition = $entity->getFieldDefinition($changed_field_name); + $config = $definition->getConfig($entity->bundle()); + + foreach ([FALSE, TRUE] as $translatable_changed_field) { + if ($definition->isTranslatable()) { + // For entities defining a translatable changed field we want to test + // the correct behavior of that field even if the translatability is + // revoked. In that case the changed timestamp should be synchronized + // across all translations. + $config->setTranslatable($translatable_changed_field); + $config->save(); + } + elseif ($translatable_changed_field) { + // For entities defining a non-translatable changed field we cannot + // declare the field as translatable on the fly by modifying its config + // because the schema doesn't support this. + break; + } + + foreach ($entity->getTranslationLanguages() as $language) { + // Ensure different timestamps. + sleep(1); + + $langcode = $language->getId(); + + $edit = array( + $this->fieldName . '[0][value]' => $this->randomString(), + ); + $edit_path = $entity->urlInfo('edit-form', array('language' => $language)); + $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode)); + + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertEqual( + $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(), + format_string('Changed time for language %language is the latest change over all languages.', array('%language' => $language->getName())) + ); + } + + $timestamps = array(); + foreach ($entity->getTranslationLanguages() as $language) { + $next_timestamp = $entity->getTranslation($language->getId())->getChangedTime(); + if (!in_array($next_timestamp, $timestamps)) { + $timestamps[] = $next_timestamp; + } + } + + if ($translatable_changed_field) { + $this->assertEqual( + count($timestamps), count($entity->getTranslationLanguages()), + 'All timestamps from all languages are different.' + ); + } + else { + $this->assertEqual( + count($timestamps), 1, + 'All timestamps from all languages are identical.' + ); + } + } + } + + /** + * Test the changed time after API and FORM save without changes. + */ + public function doTestChangedTimeAfterSaveWithoutChanges() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + // Test only entities, which implement the EntityChangedInterface. + if ($entity->getEntityType()->isSubclassOf('Drupal\Core\Entity\EntityChangedInterface')) { + $changed_timestamp = $entity->getChangedTime(); + + $entity->save(); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.'); + + // Ensure different save timestamps. + sleep(1); + + // Save the entity on the regular edit form. + $language = $entity->language(); + $edit_path = $entity->urlInfo('edit-form', array('language' => $language)); + $this->drupalPostForm($edit_path, [], $this->getFormSubmitAction($entity, $language->getId())); + + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertNotEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.'); + } + } + +} diff --git a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationWorkflowsTest.php similarity index 99% rename from core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php rename to core/modules/content_translation/tests/src/Functional/ContentTranslationWorkflowsTest.php index b1524ee..c945081 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationWorkflowsTest.php @@ -1,6 +1,6 @@ drupalCreateUser([ + 'access content', + 'view test entity', + 'administer entity_test content', + 'administer entity_test form display', + 'administer content types', + 'administer node fields', + ]); + $this->drupalLogin($web_user); + + // Create a field with settings to validate. + $this->createField(); + + $this->dateFormatter = $this->container->get('date.formatter'); + } + + /** + * Creates a date test field. + */ + protected function createField() { + $field_name = Unicode::strtolower($this->randomMachineName()); + $type = $this->getTestFieldType(); + $widget_type = $formatter_type = $type . '_default'; + + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => $type, + 'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE], + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_storage' => $this->fieldStorage, + 'bundle' => 'entity_test', + 'required' => TRUE, + ]); + $this->field->save(); + + EntityFormDisplay::load('entity_test.entity_test.default') + ->setComponent($field_name, ['type' => $widget_type]) + ->save(); + + $this->displayOptions = [ + 'type' => $formatter_type, + 'label' => 'hidden', + 'settings' => ['format_type' => 'medium'] + $this->defaultSettings, + ]; + EntityViewDisplay::create([ + 'targetEntityType' => $this->field->getTargetEntityTypeId(), + 'bundle' => $this->field->getTargetBundle(), + 'mode' => 'full', + 'status' => TRUE, + ])->setComponent($field_name, $this->displayOptions) + ->save(); + } + + /** + * Renders a entity_test and sets the output in the internal browser. + * + * @param int $id + * The entity_test ID to render. + * @param string $view_mode + * (optional) The view mode to use for rendering. Defaults to 'full'. + * @param bool $reset + * (optional) Whether to reset the entity_test controller cache. Defaults to + * TRUE to simplify testing. + */ + protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) { + if ($reset) { + $this->container->get('entity_type.manager')->getStorage('entity_test')->resetCache([$id]); + } + $entity = EntityTest::load($id); + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + $build = $display->build($entity); + $output = $this->container->get('renderer')->renderRoot($build); + $this->setRawContent($output); + $this->verbose($output); + } + + /** + * Sets the site timezone to a given timezone. + * + * @param string $timezone + * The timezone identifier to set. + */ + protected function setSiteTimezone($timezone) { + // Set an explicit site timezone, and disallow per-user timezones. + $this->config('system.date') + ->set('timezone.user.configurable', 0) + ->set('timezone.default', $timezone) + ->save(); + } + +} diff --git a/core/modules/dblog/src/Tests/ConnectionFailureTest.php b/core/modules/dblog/tests/src/Functional/ConnectionFailureTest.php similarity index 91% rename from core/modules/dblog/src/Tests/ConnectionFailureTest.php rename to core/modules/dblog/tests/src/Functional/ConnectionFailureTest.php index d800224..df088a3 100644 --- a/core/modules/dblog/src/Tests/ConnectionFailureTest.php +++ b/core/modules/dblog/tests/src/Functional/ConnectionFailureTest.php @@ -1,16 +1,16 @@ container->get('entity_type.manager') + ->getStorage($entity->getEntityTypeId()); + $storage->resetCache([$entity->id()]); + $e = $storage->load($entity->id()); + + $field = $values = $e->getTranslation($langcode)->$field_name; + // Filter out empty values so that they don't mess with the assertions. + $field->filterEmptyItems(); + $values = $field->getValue(); + $this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.'); + foreach ($expected_values as $key => $value) { + $this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value))); + } + } + +} diff --git a/core/modules/field/src/Tests/TranslationWebTest.php b/core/modules/field/tests/src/Functional/TranslationWebTest.php similarity index 99% rename from core/modules/field/src/Tests/TranslationWebTest.php rename to core/modules/field/tests/src/Functional/TranslationWebTest.php index eed78a2..3e0f849 100644 --- a/core/modules/field/src/Tests/TranslationWebTest.php +++ b/core/modules/field/tests/src/Functional/TranslationWebTest.php @@ -1,6 +1,6 @@ randomString(); + $initial_edit = array( + 'new_storage_type' => $field_type, + 'label' => $label, + 'field_name' => $field_name, + ); + + // Allow the caller to set a NULL path in case they navigated to the right + // page before calling this method. + if ($bundle_path !== NULL) { + $bundle_path = "$bundle_path/fields/add-field"; + } + + // First step: 'Add field' page. + $this->drupalPostForm($bundle_path, $initial_edit, t('Save and continue')); + $this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), 'Storage settings page was displayed.'); + // Test Breadcrumbs. + $this->assertLink($label, 0, 'Field label is correct in the breadcrumb of the storage settings page.'); + + // Second step: 'Storage settings' form. + $this->drupalPostForm(NULL, $storage_edit, t('Save field settings')); + $this->assertRaw(t('Updated field %label field settings.', array('%label' => $label)), 'Redirected to field settings page.'); + + // Third step: 'Field settings' form. + $this->drupalPostForm(NULL, $field_edit, t('Save settings')); + $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.'); + + // Check that the field appears in the overview form. + $this->assertFieldByXPath('//table[@id="field-overview"]//tr/td[1]', $label, 'Field was created and appears in the overview page.'); + } + + /** + * Adds an existing field through the Field UI. + * + * @param string $bundle_path + * Admin path of the bundle that the field is to be attached to. + * @param string $existing_storage_name + * The name of the existing field storage for which we want to add a new + * field. + * @param string $label + * (optional) The label of the new field. Defaults to a random string. + * @param array $field_edit + * (optional) $edit parameter for drupalPostForm() on the second step + * ('Field settings' form). + */ + public function fieldUIAddExistingField($bundle_path, $existing_storage_name, $label = NULL, array $field_edit = array()) { + $label = $label ?: $this->randomString(); + $initial_edit = array( + 'existing_storage_name' => $existing_storage_name, + 'existing_storage_label' => $label, + ); + + // First step: 'Re-use existing field' on the 'Add field' page. + $this->drupalPostForm("$bundle_path/fields/add-field", $initial_edit, t('Save and continue')); + // Set the main content to only the content region because the label can + // contain HTML which will be auto-escaped by Twig. + $main_content = $this->cssSelect('.region-content'); + $this->setRawContent(reset($main_content)->asXml()); + $this->assertRaw('field-config-edit-form', 'The field config edit form is present.'); + $this->assertNoRaw('&lt;', 'The page does not have double escaped HTML tags.'); + + // Second step: 'Field settings' form. + $this->drupalPostForm(NULL, $field_edit, t('Save settings')); + $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.'); + + // Check that the field appears in the overview form. + $this->assertFieldByXPath('//table[@id="field-overview"]//tr/td[1]', $label, 'Field was created and appears in the overview page.'); + } + + /** + * Deletes a field through the Field UI. + * + * @param string $bundle_path + * Admin path of the bundle that the field is to be deleted from. + * @param string $field_name + * The name of the field. + * @param string $label + * The label of the field. + * @param string $bundle_label + * The label of the bundle. + */ + public function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label) { + // Display confirmation form. + $this->drupalGet("$bundle_path/fields/$field_name/delete"); + $this->assertRaw(t('Are you sure you want to delete the field %label', array('%label' => $label)), 'Delete confirmation was found.'); + + // Test Breadcrumbs. + $this->assertLink($label, 0, 'Field label is correct in the breadcrumb of the field delete page.'); + + // Submit confirmation form. + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertRaw(t('The field %label has been deleted from the %type content type.', array('%label' => $label, '%type' => $bundle_label)), 'Delete message was found.'); + + // Check that the field does not appear in the overview form. + $this->assertNoFieldByXPath('//table[@id="field-overview"]//span[@class="label-field"]', $label, 'Field does not appear in the overview page.'); + } + +} diff --git a/core/modules/field_ui/src/Tests/ManageFieldsTest.php b/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php similarity index 99% rename from core/modules/field_ui/src/Tests/ManageFieldsTest.php rename to core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php index cbe9f53..b84f56c 100644 --- a/core/modules/field_ui/src/Tests/ManageFieldsTest.php +++ b/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php @@ -1,6 +1,6 @@ adminUser = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer node fields', 'administer node display', 'administer nodes', 'bypass node access')); + $this->drupalLogin($this->adminUser); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + + /** + * Retrieves a sample file of the specified type. + * + * @return \Drupal\file\FileInterface + */ + function getTestFile($type_name, $size = NULL) { + // Get a file to upload. + $file = current($this->drupalGetTestFiles($type_name, $size)); + + // Add a filesize property to files as would be read by + // \Drupal\file\Entity\File::load(). + $file->filesize = filesize($file->uri); + + return File::create((array) $file); + } + + /** + * Retrieves the fid of the last inserted file. + */ + function getLastFileId() { + return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField(); + } + + /** + * Creates a new file field. + * + * @param string $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param string $entity_type + * The entity type. + * @param string $bundle + * The bundle that this field will be added to. + * @param array $storage_settings + * A list of field storage settings that will be added to the defaults. + * @param array $field_settings + * A list of instance settings that will be added to the instance defaults. + * @param array $widget_settings + * A list of widget settings that will be added to the widget defaults. + */ + function createFileField($name, $entity_type, $bundle, $storage_settings = array(), $field_settings = array(), $widget_settings = array()) { + $field_storage = FieldStorageConfig::create(array( + 'entity_type' => $entity_type, + 'field_name' => $name, + 'type' => 'file', + 'settings' => $storage_settings, + 'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1, + )); + $field_storage->save(); + + $this->attachFileField($name, $entity_type, $bundle, $field_settings, $widget_settings); + return $field_storage; + } + + /** + * Attaches a file field to an entity. + * + * @param string $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param string $entity_type + * The entity type this field will be added to. + * @param string $bundle + * The bundle this field will be added to. + * @param array $field_settings + * A list of field settings that will be added to the defaults. + * @param array $widget_settings + * A list of widget settings that will be added to the widget defaults. + */ + function attachFileField($name, $entity_type, $bundle, $field_settings = array(), $widget_settings = array()) { + $field = array( + 'field_name' => $name, + 'label' => $name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'required' => !empty($field_settings['required']), + 'settings' => $field_settings, + ); + FieldConfig::create($field)->save(); + + entity_get_form_display($entity_type, $bundle, 'default') + ->setComponent($name, array( + 'type' => 'file_generic', + 'settings' => $widget_settings, + )) + ->save(); + // Assign display settings. + entity_get_display($entity_type, $bundle, 'default') + ->setComponent($name, array( + 'label' => 'hidden', + 'type' => 'file_default', + )) + ->save(); + } + + /** + * Updates an existing file field with new settings. + */ + function updateFileField($name, $type_name, $field_settings = array(), $widget_settings = array()) { + $field = FieldConfig::loadByName('node', $type_name, $name); + $field->setSettings(array_merge($field->getSettings(), $field_settings)); + $field->save(); + + entity_get_form_display('node', $type_name, 'default') + ->setComponent($name, array( + 'settings' => $widget_settings, + )) + ->save(); + } + + /** + * Uploads a file to a node. + * + * @param \Drupal\file\FileInterface $file + * The File to be uploaded. + * @param string $field_name + * The name of the field on which the files should be saved. + * @param $nid_or_type + * A numeric node id to upload files to an existing node, or a string + * indicating the desired bundle for a new node. + * @param bool $new_revision + * The revision number. + * @param array $extras + * Additional values when a new node is created. + * + * @return int + * The node id. + */ + function uploadNodeFile(FileInterface $file, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = array()) { + return $this->uploadNodeFiles([$file], $field_name, $nid_or_type, $new_revision, $extras); + } + + /** + * Uploads multiple files to a node. + * + * @param \Drupal\file\FileInterface[] $files + * The files to be uploaded. + * @param string $field_name + * The name of the field on which the files should be saved. + * @param $nid_or_type + * A numeric node id to upload files to an existing node, or a string + * indicating the desired bundle for a new node. + * @param bool $new_revision + * The revision number. + * @param array $extras + * Additional values when a new node is created. + * + * @return int + * The node id. + */ + function uploadNodeFiles(array $files, $field_name, $nid_or_type, $new_revision = TRUE, array $extras = array()) { + $edit = array( + 'title[0][value]' => $this->randomMachineName(), + 'revision' => (string) (int) $new_revision, + ); + + $node_storage = $this->container->get('entity.manager')->getStorage('node'); + if (is_numeric($nid_or_type)) { + $nid = $nid_or_type; + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + } + else { + // Add a new node. + $extras['type'] = $nid_or_type; + $node = $this->drupalCreateNode($extras); + $nid = $node->id(); + // Save at least one revision to better simulate a real site. + $node->setNewRevision(); + $node->save(); + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + $this->assertNotEqual($nid, $node->getRevisionId(), 'Node revision exists.'); + } + + // Attach files to the node. + $field_storage = FieldStorageConfig::loadByName('node', $field_name); + // File input name depends on number of files already uploaded. + $field_num = count($node->{$field_name}); + $name = 'files[' . $field_name . "_$field_num]"; + if ($field_storage->getCardinality() != 1) { + $name .= '[]'; + } + foreach ($files as $file) { + $file_path = $this->container->get('file_system')->realpath($file->getFileUri()); + if (count($files) == 1) { + $edit[$name] = $file_path; + } + else { + $edit[$name][] = $file_path; + } + } + $this->drupalPostForm("node/$nid/edit", $edit, t('Save and keep published')); + + return $nid; + } + + /** + * Removes a file from a node. + * + * Note that if replacing a file, it must first be removed then added again. + */ + function removeNodeFile($nid, $new_revision = TRUE) { + $edit = array( + 'revision' => (string) (int) $new_revision, + ); + + $this->drupalPostForm('node/' . $nid . '/edit', array(), t('Remove')); + $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + } + + /** + * Replaces a file within a node. + */ + function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) { + $edit = array( + 'files[' . $field_name . '_0]' => drupal_realpath($file->getFileUri()), + 'revision' => (string) (int) $new_revision, + ); + + $this->drupalPostForm('node/' . $nid . '/edit', array(), t('Remove')); + $this->drupalPostForm(NULL, $edit, t('Save and keep published')); + } + + /** + * Asserts that a file exists physically on disk. + */ + function assertFileExists($file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->getFileUri())); + $this->assertTrue(is_file($file->getFileUri()), $message); + } + + /** + * Asserts that a file exists in the database. + */ + function assertFileEntryExists($file, $message = NULL) { + $this->container->get('entity.manager')->getStorage('file')->resetCache(); + $db_file = File::load($file->id()); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->getFileUri())); + $this->assertEqual($db_file->getFileUri(), $file->getFileUri(), $message); + } + + /** + * Asserts that a file does not exist on disk. + */ + function assertFileNotExists($file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->getFileUri())); + $this->assertFalse(is_file($file->getFileUri()), $message); + } + + /** + * Asserts that a file does not exist in the database. + */ + function assertFileEntryNotExists($file, $message) { + $this->container->get('entity.manager')->getStorage('file')->resetCache(); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->getFileUri())); + $this->assertFalse(File::load($file->id()), $message); + } + + /** + * Asserts that a file's status is set to permanent in the database. + */ + function assertFileIsPermanent(FileInterface $file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file is permanent.', array('%file' => $file->getFileUri())); + $this->assertTrue($file->isPermanent(), $message); + } + +} diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php similarity index 99% rename from core/modules/file/src/Tests/FileFieldValidateTest.php rename to core/modules/file/tests/src/Functional/FileFieldValidateTest.php index 9d43060..e1f4332 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php @@ -1,6 +1,6 @@ resetCache(); + + // Determine which hooks were called. + $actual = array_keys(array_filter(file_test_get_all_calls())); + + // Determine if there were any expected that were not called. + $uncalled = array_diff($expected, $actual); + if (count($uncalled)) { + $this->assertTrue(FALSE, format_string('Expected hooks %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)))); + } + else { + $this->assertTrue(TRUE, format_string('All the expected hooks were called: %expected', array('%expected' => empty($expected) ? '(none)' : implode(', ', $expected)))); + } + + // Determine if there were any unexpected calls. + $unexpected = array_diff($actual, $expected); + if (count($unexpected)) { + $this->assertTrue(FALSE, format_string('Unexpected hooks were called: %unexpected.', array('%unexpected' => empty($unexpected) ? '(none)' : implode(', ', $unexpected)))); + } + else { + $this->assertTrue(TRUE, 'No unexpected hooks were called.'); + } + } + + /** + * Assert that a hook_file_* hook was called a certain number of times. + * + * @param string $hook + * String with the hook name; for instance, 'load', 'save', 'insert', etc. + * @param int $expected_count + * Optional integer count. + * @param string|null $message + * Optional translated string message. + */ + function assertFileHookCalled($hook, $expected_count = 1, $message = NULL) { + $actual_count = count(file_test_get_calls($hook)); + + if (!isset($message)) { + if ($actual_count == $expected_count) { + $message = format_string('hook_file_@name was called correctly.', array('@name' => $hook)); + } + elseif ($expected_count == 0) { + $message = \Drupal::translation()->formatPlural($actual_count, 'hook_file_@name was not expected to be called but was actually called once.', 'hook_file_@name was not expected to be called but was actually called @count times.', array('@name' => $hook, '@count' => $actual_count)); + } + else { + $message = format_string('hook_file_@name was expected to be called %expected times but was called %actual times.', array('@name' => $hook, '%expected' => $expected_count, '%actual' => $actual_count)); + } + } + $this->assertEqual($actual_count, $expected_count, $message); + } + + /** + * Asserts that two files have the same values (except timestamp). + * + * @param \Drupal\file\FileInterface $before + * File object to compare. + * @param \Drupal\file\FileInterface $after + * File object to compare. + */ + function assertFileUnchanged(FileInterface $before, FileInterface $after) { + $this->assertEqual($before->id(), $after->id(), t('File id is the same: %file1 == %file2.', array('%file1' => $before->id(), '%file2' => $after->id())), 'File unchanged'); + $this->assertEqual($before->getOwner()->id(), $after->getOwner()->id(), t('File owner is the same: %file1 == %file2.', array('%file1' => $before->getOwner()->id(), '%file2' => $after->getOwner()->id())), 'File unchanged'); + $this->assertEqual($before->getFilename(), $after->getFilename(), t('File name is the same: %file1 == %file2.', array('%file1' => $before->getFilename(), '%file2' => $after->getFilename())), 'File unchanged'); + $this->assertEqual($before->getFileUri(), $after->getFileUri(), t('File path is the same: %file1 == %file2.', array('%file1' => $before->getFileUri(), '%file2' => $after->getFileUri())), 'File unchanged'); + $this->assertEqual($before->getMimeType(), $after->getMimeType(), t('File MIME type is the same: %file1 == %file2.', array('%file1' => $before->getMimeType(), '%file2' => $after->getMimeType())), 'File unchanged'); + $this->assertEqual($before->getSize(), $after->getSize(), t('File size is the same: %file1 == %file2.', array('%file1' => $before->getSize(), '%file2' => $after->getSize())), 'File unchanged'); + $this->assertEqual($before->isPermanent(), $after->isPermanent(), t('File status is the same: %file1 == %file2.', array('%file1' => $before->isPermanent(), '%file2' => $after->isPermanent())), 'File unchanged'); + } + + /** + * Asserts that two files are not the same by comparing the fid and filepath. + * + * @param \Drupal\file\FileInterface $file1 + * File object to compare. + * @param \Drupal\file\FileInterface $file2 + * File object to compare. + */ + function assertDifferentFile(FileInterface $file1, FileInterface $file2) { + $this->assertNotEqual($file1->id(), $file2->id(), t('Files have different ids: %file1 != %file2.', array('%file1' => $file1->id(), '%file2' => $file2->id())), 'Different file'); + $this->assertNotEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have different paths: %file1 != %file2.', array('%file1' => $file1->getFileUri(), '%file2' => $file2->getFileUri())), 'Different file'); + } + + /** + * Asserts that two files are the same by comparing the fid and filepath. + * + * @param \Drupal\file\FileInterface $file1 + * File object to compare. + * @param \Drupal\file\FileInterface $file2 + * File object to compare. + */ + function assertSameFile(FileInterface $file1, FileInterface $file2) { + $this->assertEqual($file1->id(), $file2->id(), t('Files have the same ids: %file1 == %file2.', array('%file1' => $file1->id(), '%file2-fid' => $file2->id())), 'Same file'); + $this->assertEqual($file1->getFileUri(), $file2->getFileUri(), t('Files have the same path: %file1 == %file2.', array('%file1' => $file1->getFileUri(), '%file2' => $file2->getFileUri())), 'Same file'); + } + + /** + * Create a file and save it to the files table and assert that it occurs + * correctly. + * + * @param string $filepath + * Optional string specifying the file path. If none is provided then a + * randomly named file will be created in the site's files directory. + * @param string $contents + * Optional contents to save into the file. If a NULL value is provided an + * arbitrary string will be used. + * @param string $scheme + * Optional string indicating the stream scheme to use. Drupal core includes + * public, private, and temporary. The public wrapper is the default. + * @return \Drupal\file\FileInterface + * File entity. + */ + function createFile($filepath = NULL, $contents = NULL, $scheme = NULL) { + // Don't count hook invocations caused by creating the file. + \Drupal::state()->set('file_test.count_hook_invocations', FALSE); + $file = File::create([ + 'uri' => $this->createUri($filepath, $contents, $scheme), + 'uid' => 1, + ]); + $file->save(); + // Write the record directly rather than using the API so we don't invoke + // the hooks. + $this->assertTrue($file->id() > 0, 'The file was added to the database.', 'Create test file'); + + \Drupal::state()->set('file_test.count_hook_invocations', TRUE); + return $file; + } + + /** + * Creates a file and returns its URI. + * + * @param string $filepath + * Optional string specifying the file path. If none is provided then a + * randomly named file will be created in the site's files directory. + * @param string $contents + * Optional contents to save into the file. If a NULL value is provided an + * arbitrary string will be used. + * @param string $scheme + * Optional string indicating the stream scheme to use. Drupal core includes + * public, private, and temporary. The public wrapper is the default. + * + * @return string + * File URI. + */ + function createUri($filepath = NULL, $contents = NULL, $scheme = NULL) { + if (!isset($filepath)) { + // Prefix with non-latin characters to ensure that all file-related + // tests work with international filenames. + $filepath = 'Файл для тестирования ' . $this->randomMachineName(); + } + if (!isset($scheme)) { + $scheme = file_default_scheme(); + } + $filepath = $scheme . '://' . $filepath; + + if (!isset($contents)) { + $contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data."; + } + + file_put_contents($filepath, $contents); + $this->assertTrue(is_file($filepath), t('The test file exists on the disk.'), 'Create test file'); + return $filepath; + } + +} diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/tests/src/Functional/FilePrivateTest.php similarity index 99% rename from core/modules/file/src/Tests/FilePrivateTest.php rename to core/modules/file/tests/src/Functional/FilePrivateTest.php index 2705ef2..e77d72f 100644 --- a/core/modules/file/src/Tests/FilePrivateTest.php +++ b/core/modules/file/tests/src/Functional/FilePrivateTest.php @@ -1,6 +1,6 @@ profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + + $this->adminUser = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer node fields', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles', 'administer node display')); + $this->drupalLogin($this->adminUser); + } + + /** + * Create a new image field. + * + * @param string $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param string $type_name + * The node type that this field will be added to. + * @param array $storage_settings + * A list of field storage settings that will be added to the defaults. + * @param array $field_settings + * A list of instance settings that will be added to the instance defaults. + * @param array $widget_settings + * Widget settings to be added to the widget defaults. + * @param array $formatter_settings + * Formatter settings to be added to the formatter defaults. + * @param string $description + * A description for the field. + */ + function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array(), $formatter_settings = array(), $description = '') { + FieldStorageConfig::create(array( + 'field_name' => $name, + 'entity_type' => 'node', + 'type' => 'image', + 'settings' => $storage_settings, + 'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1, + ))->save(); + + $field_config = FieldConfig::create([ + 'field_name' => $name, + 'label' => $name, + 'entity_type' => 'node', + 'bundle' => $type_name, + 'required' => !empty($field_settings['required']), + 'settings' => $field_settings, + 'description' => $description, + ]); + $field_config->save(); + + entity_get_form_display('node', $type_name, 'default') + ->setComponent($name, array( + 'type' => 'image_image', + 'settings' => $widget_settings, + )) + ->save(); + + entity_get_display('node', $type_name, 'default') + ->setComponent($name, array( + 'type' => 'image', + 'settings' => $formatter_settings, + )) + ->save(); + + return $field_config; + + } + + /** + * Preview an image in a node. + * + * @param \Drupal\Core\Image\ImageInterface $image + * A file object representing the image to upload. + * @param string $field_name + * Name of the image field the image should be attached to. + * @param string $type + * The type of node to create. + */ + function previewNodeImage($image, $field_name, $type) { + $edit = array( + 'title[0][value]' => $this->randomMachineName(), + ); + $edit['files[' . $field_name . '_0]'] = drupal_realpath($image->uri); + $this->drupalPostForm('node/add/' . $type, $edit, t('Preview')); + } + + /** + * Upload an image to a node. + * + * @param $image + * A file object representing the image to upload. + * @param $field_name + * Name of the image field the image should be attached to. + * @param $type + * The type of node to create. + * @param $alt + * The alt text for the image. Use if the field settings require alt text. + */ + function uploadNodeImage($image, $field_name, $type, $alt = '') { + $edit = array( + 'title[0][value]' => $this->randomMachineName(), + ); + $edit['files[' . $field_name . '_0]'] = drupal_realpath($image->uri); + $this->drupalPostForm('node/add/' . $type, $edit, t('Save and publish')); + if ($alt) { + // Add alt text. + $this->drupalPostForm(NULL, [$field_name . '[0][alt]' => $alt], t('Save and publish')); + } + + // Retrieve ID of the newly created node from the current URL. + $matches = array(); + preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches); + return isset($matches[1]) ? $matches[1] : FALSE; + } + + /** + * Retrieves the fid of the last inserted file. + */ + protected function getLastFileId() { + return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField(); + } + +} diff --git a/core/modules/image/src/Tests/ImageFieldWidgetTest.php b/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php similarity index 95% rename from core/modules/image/src/Tests/ImageFieldWidgetTest.php rename to core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php index 52d9e4d..f8f9601 100644 --- a/core/modules/image/src/Tests/ImageFieldWidgetTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldWidgetTest.php @@ -1,6 +1,6 @@ createStub($entity_type_id); + $this->assertTrue($entity_id, 'Stub successfully created'); + if ($entity_id) { + $violations = $this->validateStub($entity_type_id, $entity_id); + if (!$this->assertIdentical(count($violations), 0, 'Stub is a valid entity')) { + foreach ($violations as $violation) { + $this->fail((string) $violation->getMessage()); + } + } + } + } + + /** + * Create a stub of the given entity type. + * + * @param string $entity_type_id + * The entity type we are stubbing. + * + * @return int + * ID of the created entity. + */ + protected function createStub($entity_type_id) { + // Create a dummy migration to pass to the destination plugin. + $definition = [ + 'migration_tags' => ['Stub test'], + 'source' => ['plugin' => 'empty'], + 'process' => [], + 'destination' => ['plugin' => 'entity:' . $entity_type_id], + ]; + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + $destination_plugin = $migration->getDestinationPlugin(TRUE); + $stub_row = new Row([], [], TRUE); + $destination_ids = $destination_plugin->import($stub_row); + return reset($destination_ids); + } + + /** + * Perform validation on a stub entity. + * + * @param string $entity_type_id + * The entity type we are stubbing. + * @param string $entity_id + * ID of the stubbed entity to validate. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface + * List of constraint violations identified. + */ + protected function validateStub($entity_type_id, $entity_id) { + $controller = \Drupal::entityManager()->getStorage($entity_type_id); + /** @var \Drupal\Core\Entity\ContentEntityInterface $stub_entity */ + $stub_entity = $controller->load($entity_id); + return $stub_entity->validate(); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Tests/MigrateAccessTest.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateAccessTest.php similarity index 82% rename from core/modules/migrate_drupal_ui/src/Tests/MigrateAccessTest.php rename to core/modules/migrate_drupal_ui/tests/src/Functional/MigrateAccessTest.php index 2401d86..f4d7d05 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/MigrateAccessTest.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateAccessTest.php @@ -1,15 +1,15 @@ createMigrationConnection(); + $this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui'); + + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + } + + /** + * Loads a database fixture into the source database connection. + * + * @param string $path + * Path to the dump file. + */ + protected function loadFixture($path) { + $default_db = Database::getConnection()->getKey(); + Database::setActiveConnection($this->sourceDatabase->getKey()); + + if (substr($path, -3) == '.gz') { + $path = 'compress.zlib://' . $path; + } + require $path; + + Database::setActiveConnection($default_db); + } + + /** + * Changes the database connection to the prefixed one. + * + * @todo Remove when we don't use global. https://www.drupal.org/node/2552791 + */ + protected function createMigrationConnection() { + $connection_info = Database::getConnectionInfo('default')['default']; + if ($connection_info['driver'] === 'sqlite') { + // Create database file in the test site's public file directory so that + // \Drupal\simpletest\TestBase::restoreEnvironment() will delete this once + // the test is complete. + $file = $this->publicFilesDirectory . '/' . $this->testId . '-migrate.db.sqlite'; + touch($file); + $connection_info['database'] = $file; + $connection_info['prefix'] = ''; + } + else { + $prefix = is_array($connection_info['prefix']) ? $connection_info['prefix']['default'] : $connection_info['prefix']; + // Simpletest uses fixed length prefixes. Create a new prefix for the + // source database. Adding to the end of the prefix ensures that + // \Drupal\simpletest\TestBase::restoreEnvironment() will remove the + // additional tables. + $connection_info['prefix'] = $prefix . '0'; + } + + Database::addConnectionInfo('migrate_drupal_ui', 'default', $connection_info); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + Database::removeConnection('migrate_drupal_ui'); + parent::tearDown(); + } + + /** + * Executes all steps of migrations upgrade. + */ + protected function testMigrateUpgrade() { + $connection_options = $this->sourceDatabase->getConnectionOptions(); + $this->drupalGet('/upgrade'); + $this->assertText('Upgrade a site by importing it into a clean and empty new install of Drupal 8. You will lose any existing configuration once you import your site into it. See the online documentation for Drupal site upgrades for more detailed information.'); + + $this->drupalPostForm(NULL, [], t('Continue')); + $this->assertText('Provide credentials for the database of the Drupal site you want to upgrade.'); + $this->assertFieldByName('mysql[host]'); + + $driver = $connection_options['driver']; + $connection_options['prefix'] = $connection_options['prefix']['default']; + + // Use the driver connection form to get the correct options out of the + // database settings. This supports all of the databases we test against. + $drivers = drupal_get_database_types(); + $form = $drivers[$driver]->getFormOptions($connection_options); + $connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']); + $edit = [ + $driver => $connection_options, + 'source_base_path' => $this->getSourceBasePath(), + ]; + if (count($drivers) !== 1) { + $edit['driver'] = $driver; + } + $edits = $this->translatePostValues($edit); + + // Ensure submitting the form with invalid database credentials gives us a + // nice warning. + $this->drupalPostForm(NULL, [$driver . '[database]' => 'wrong'] + $edits, t('Review upgrade')); + $this->assertText('Resolve the issue below to continue the upgrade.'); + + $this->drupalPostForm(NULL, $edits, t('Review upgrade')); + $this->assertResponse(200); + $this->assertText('Are you sure?'); + $this->drupalPostForm(NULL, [], t('Perform upgrade')); + $this->assertText(t('Congratulations, you upgraded Drupal!')); + + // Have to reset all the statics after migration to ensure entities are + // loadable. + $this->resetAll(); + + $expected_counts = $this->getEntityCounts(); + foreach (array_keys(\Drupal::entityTypeManager()->getDefinitions()) as $entity_type) { + $real_count = count(\Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple()); + $expected_count = isset($expected_counts[$entity_type]) ? $expected_counts[$entity_type] : 0; + $this->assertEqual($expected_count, $real_count, "Found $real_count $entity_type entities, expected $expected_count."); + } + + $version_tag = 'Drupal ' . $this->getLegacyDrupalVersion($this->sourceDatabase); + $plugin_manager = \Drupal::service('plugin.manager.migration'); + /** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */ + $all_migrations = $plugin_manager->createInstancesByTag($version_tag); + foreach ($all_migrations as $migration) { + $id_map = $migration->getIdMap(); + foreach ($id_map as $source_id => $map) { + // Convert $source_id into a keyless array so that + // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as + // expected. + $source_id_values = array_values(unserialize($source_id)); + $row = $id_map->getRowBySource($source_id_values); + $destination = serialize($id_map->currentDestination()); + $message = "Successful migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status']; + // A completed migration should have maps with + // MigrateIdMapInterface::STATUS_IGNORED or + // MigrateIdMapInterface::STATUS_IMPORTED. + if ($row['source_row_status'] == MigrateIdMapInterface::STATUS_FAILED || $row['source_row_status'] == MigrateIdMapInterface::STATUS_NEEDS_UPDATE) { + $this->fail($message); + } + else { + $this->pass($message); + } + } + } + \Drupal::service('module_installer')->install(['forum']); + } + + /** + * Gets the source base path for the concrete test. + * + * @return string + * The source base path. + */ + abstract protected function getSourceBasePath(); + + /** + * Gets the expected number of entities per entity type after migration. + * + * @return int[] + * An array of expected counts keyed by entity type ID. + */ + abstract protected function getEntityCounts(); + +} diff --git a/core/modules/node/tests/src/Functional/AssertButtonsTrait.php b/core/modules/node/tests/src/Functional/AssertButtonsTrait.php new file mode 100644 index 0000000..3f3c87f --- /dev/null +++ b/core/modules/node/tests/src/Functional/AssertButtonsTrait.php @@ -0,0 +1,48 @@ +xpath('//input[@type="submit"][@value="Save"]'); + + // Verify that the number of buttons passed as parameters is + // available in the dropbutton widget. + if ($dropbutton) { + $i = 0; + $count = count($buttons); + + // Assert there is no save button. + $this->assertTrue(empty($save_button)); + + // Dropbutton elements. + $elements = $this->xpath('//div[@class="dropbutton-wrapper"]//input[@type="submit"]'); + $this->assertEqual($count, count($elements)); + foreach ($elements as $element) { + $value = isset($element['value']) ? (string) $element['value'] : ''; + $this->assertEqual($buttons[$i], $value); + $i++; + } + } + else { + // Assert there is a save button. + $this->assertTrue(!empty($save_button)); + $this->assertNoRaw('dropbutton-wrapper'); + } + } + +} diff --git a/core/modules/node/src/Tests/MultiStepNodeFormBasicOptionsTest.php b/core/modules/node/tests/src/Functional/MultiStepNodeFormBasicOptionsTest.php similarity index 97% rename from core/modules/node/src/Tests/MultiStepNodeFormBasicOptionsTest.php rename to core/modules/node/tests/src/Functional/MultiStepNodeFormBasicOptionsTest.php index c23986a..36ac519 100644 --- a/core/modules/node/src/Tests/MultiStepNodeFormBasicOptionsTest.php +++ b/core/modules/node/tests/src/Functional/MultiStepNodeFormBasicOptionsTest.php @@ -1,6 +1,6 @@ profile != 'standard') { + $this->drupalCreateContentType(array( + 'type' => 'page', + 'name' => 'Basic page', + 'display_submitted' => FALSE, + )); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + $this->accessHandler = \Drupal::entityManager()->getAccessControlHandler('node'); + } + + /** + * Asserts that node access correctly grants or denies access. + * + * @param array $ops + * An associative array of the expected node access grants for the node + * and account, with each key as the name of an operation (e.g. 'view', + * 'delete') and each value a Boolean indicating whether access to that + * operation should be granted. + * @param \Drupal\node\NodeInterface $node + * The node object to check. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account for which to check access. + */ + function assertNodeAccess(array $ops, NodeInterface $node, AccountInterface $account) { + foreach ($ops as $op => $result) { + $this->assertEqual($result, $this->accessHandler->access($node, $op, $account), $this->nodeAccessAssertMessage($op, $result, $node->language()->getId())); + } + } + + /** + * Asserts that node create access correctly grants or denies access. + * + * @param string $bundle + * The node bundle to check access to. + * @param bool $result + * Whether access should be granted or not. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account for which to check access. + * @param string|null $langcode + * (optional) The language code indicating which translation of the node + * to check. If NULL, the untranslated (fallback) access is checked. + */ + function assertNodeCreateAccess($bundle, $result, AccountInterface $account, $langcode = NULL) { + $this->assertEqual($result, $this->accessHandler->createAccess($bundle, $account, array( + 'langcode' => $langcode, + )), $this->nodeAccessAssertMessage('create', $result, $langcode)); + } + + /** + * Constructs an assert message to display which node access was tested. + * + * @param string $operation + * The operation to check access for. + * @param bool $result + * Whether access should be granted or not. + * @param string|null $langcode + * (optional) The language code indicating which translation of the node + * to check. If NULL, the untranslated (fallback) access is checked. + * + * @return string + * An assert message string which contains information in plain English + * about the node access permission test that was performed. + */ + function nodeAccessAssertMessage($operation, $result, $langcode = NULL) { + return format_string( + 'Node access returns @result with operation %op, language code %langcode.', + array( + '@result' => $result ? 'true' : 'false', + '%op' => $operation, + '%langcode' => !empty($langcode) ? $langcode : 'empty' + ) + ); + } + +} diff --git a/core/modules/node/src/Tests/NodeTitleTest.php b/core/modules/node/tests/src/Functional/NodeTitleTest.php similarity index 98% rename from core/modules/node/src/Tests/NodeTitleTest.php rename to core/modules/node/tests/src/Functional/NodeTitleTest.php index 59ee021..38d5752 100644 --- a/core/modules/node/src/Tests/NodeTitleTest.php +++ b/core/modules/node/tests/src/Functional/NodeTitleTest.php @@ -1,6 +1,6 @@ profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + } + +} diff --git a/core/modules/rdf/src/Tests/CommentAttributesTest.php b/core/modules/rdf/tests/src/Functional/CommentAttributesTest.php similarity index 99% rename from core/modules/rdf/src/Tests/CommentAttributesTest.php rename to core/modules/rdf/tests/src/Functional/CommentAttributesTest.php index 5febcb9..92e6dda 100644 --- a/core/modules/rdf/src/Tests/CommentAttributesTest.php +++ b/core/modules/rdf/tests/src/Functional/CommentAttributesTest.php @@ -1,6 +1,6 @@ defaultFormat = 'hal_json'; + $this->defaultMimeType = 'application/hal+json'; + $this->defaultAuth = array('cookie'); + $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); + // Create a test content type for node testing. + if (in_array('node', static::$modules)) { + $this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest')); + } + } + + /** + * Helper function to issue a HTTP request with simpletest's cURL. + * + * @param string|\Drupal\Core\Url $url + * A Url object or system path. + * @param string $method + * HTTP method, one of GET, POST, PUT or DELETE. + * @param string $body + * The body for POST and PUT. + * @param string $mime_type + * The MIME type of the transmitted content. + * @param bool $forget_xcsrf_token + * If TRUE, the CSRF token won't be included in request. + * + * @return string + * The content returned from the request. + */ + protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $forget_xcsrf_token = FALSE) { + if (!isset($mime_type)) { + $mime_type = $this->defaultMimeType; + } + if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) { + // GET the CSRF token first for writing requests. + $token = $this->drupalGet('session/token'); + } + + $url = $this->buildUrl($url); + + $curl_options = array(); + switch ($method) { + case 'GET': + // Set query if there are additional GET parameters. + $curl_options = array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type), + ); + break; + + case 'HEAD': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_CUSTOMREQUEST => 'HEAD', + CURLOPT_URL => $url, + CURLOPT_NOBODY => TRUE, + CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type), + ); + break; + + case 'POST': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $body, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array( + 'Content-Type: ' . $mime_type, + 'X-CSRF-Token: ' . $token, + ) : array( + 'Content-Type: ' . $mime_type, + ), + ); + break; + + case 'PUT': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $body, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array( + 'Content-Type: ' . $mime_type, + 'X-CSRF-Token: ' . $token, + ) : array( + 'Content-Type: ' . $mime_type, + ), + ); + break; + + case 'PATCH': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_POSTFIELDS => $body, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array( + 'Content-Type: ' . $mime_type, + 'X-CSRF-Token: ' . $token, + ) : array( + 'Content-Type: ' . $mime_type, + ), + ); + break; + + case 'DELETE': + $curl_options = array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array('X-CSRF-Token: ' . $token) : array(), + ); + break; + } + + if ($mime_type === 'none') { + unset($curl_options[CURLOPT_HTTPHEADER]['Content-Type']); + } + + $this->responseBody = $this->curlExec($curl_options); + + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + + $headers = $this->drupalGetHeaders(); + + $this->verbose($method . ' request to: ' . $url . + '
Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) . + '
Response headers: ' . nl2br(print_r($headers, TRUE)) . + '
Response body: ' . $this->responseBody); + + return $this->responseBody; + } + + /** + * Creates entity objects based on their types. + * + * @param string $entity_type + * The type of the entity that should be created. + * + * @return \Drupal\Core\Entity\EntityInterface + * The new entity object. + */ + protected function entityCreate($entity_type) { + return $this->container->get('entity_type.manager') + ->getStorage($entity_type) + ->create($this->entityValues($entity_type)); + } + + /** + * Provides an array of suitable property values for an entity type. + * + * Required properties differ from entity type to entity type, so we keep a + * minimum mapping here. + * + * @param string $entity_type_id + * The ID of the type of entity that should be created. + * + * @return array + * An array of values keyed by property name. + */ + protected function entityValues($entity_type_id) { + switch ($entity_type_id) { + case 'entity_test': + return array( + 'name' => $this->randomMachineName(), + 'user_id' => 1, + 'field_test_text' => array(0 => array( + 'value' => $this->randomString(), + 'format' => 'plain_text', + )), + ); + case 'config_test': + return [ + 'id' => $this->randomMachineName(), + 'label' => 'Test label', + ]; + case 'node': + return array('title' => $this->randomString(), 'type' => 'resttest'); + case 'node_type': + return array( + 'type' => 'article', + 'name' => $this->randomMachineName(), + ); + case 'user': + return array('name' => $this->randomMachineName()); + + case 'comment': + return [ + 'subject' => $this->randomMachineName(), + 'entity_type' => 'node', + 'comment_type' => 'comment', + 'comment_body' => $this->randomString(), + 'entity_id' => 'invalid', + 'field_name' => 'comment', + ]; + case 'taxonomy_vocabulary': + return [ + 'vid' => 'tags', + 'name' => $this->randomMachineName(), + ]; + case 'block': + // Block placements depend on themes, ensure Bartik is installed. + $this->container->get('theme_installer')->install(['bartik']); + return [ + 'id' => strtolower($this->randomMachineName(8)), + 'plugin' => 'system_powered_by_block', + 'theme' => 'bartik', + 'region' => 'header', + ]; + default: + if ($this->isConfigEntity($entity_type_id)) { + return $this->configEntityValues($entity_type_id); + } + return array(); + } + } + + /** + * Enables the REST service interface for a specific entity type. + * + * @param string|false $resource_type + * The resource type that should get REST API enabled or FALSE to disable all + * resource types. + * @param string $method + * The HTTP method to enable, e.g. GET, POST etc. + * @param string|array $format + * (Optional) The serialization format, e.g. hal_json, or a list of formats. + * @param array $auth + * (Optional) The list of valid authentication methods. + */ + protected function enableService($resource_type, $method = 'GET', $format = NULL, array $auth = []) { + if ($resource_type) { + // Enable REST API for this entity type. + $resource_config_id = str_replace(':', '.', $resource_type); + // get entity by id + /** @var \Drupal\rest\RestResourceConfigInterface $resource_config */ + $resource_config = $this->resourceConfigStorage->load($resource_config_id); + if (!$resource_config) { + $resource_config = $this->resourceConfigStorage->create([ + 'id' => $resource_config_id, + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [] + ]); + } + $configuration = $resource_config->get('configuration'); + + if (is_array($format)) { + for ($i = 0; $i < count($format); $i++) { + $configuration[$method]['supported_formats'][] = $format[$i]; + } + } + else { + if ($format == NULL) { + $format = $this->defaultFormat; + } + $configuration[$method]['supported_formats'][] = $format; + } + + if (!is_array($auth) || empty($auth)) { + $auth = $this->defaultAuth; + } + foreach ($auth as $auth_provider) { + $configuration[$method]['supported_auth'][] = $auth_provider; + } + + $resource_config->set('configuration', $configuration); + $resource_config->save(); + } + else { + foreach ($this->resourceConfigStorage->loadMultiple() as $resource_config) { + $resource_config->delete(); + } + } + $this->rebuildCache(); + } + + /** + * Rebuilds routing caches. + */ + protected function rebuildCache() { + // Rebuild routing cache, so that the REST API paths are available. + $this->container->get('router.builder')->rebuild(); + } + + /** + * {@inheritdoc} + * + * This method is overridden to deal with a cURL quirk: the usage of + * CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to + * override it every time it is omitted. + */ + protected function curlExec($curl_options, $redirect = FALSE) { + if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) { + if (!empty($curl_options[CURLOPT_HTTPGET])) { + $curl_options[CURLOPT_CUSTOMREQUEST] = 'GET'; + } + if (!empty($curl_options[CURLOPT_POST])) { + $curl_options[CURLOPT_CUSTOMREQUEST] = 'POST'; + } + } + return parent::curlExec($curl_options, $redirect); + } + + /** + * Provides the necessary user permissions for entity operations. + * + * @param string $entity_type_id + * The entity type. + * @param string $operation + * The operation, one of 'view', 'create', 'update' or 'delete'. + * + * @return array + * The set of user permission strings. + */ + protected function entityPermissions($entity_type_id, $operation) { + switch ($entity_type_id) { + case 'entity_test': + switch ($operation) { + case 'view': + return array('view test entity'); + case 'create': + case 'update': + case 'delete': + return array('administer entity_test content'); + } + case 'node': + switch ($operation) { + case 'view': + return array('access content'); + case 'create': + return array('create resttest content'); + case 'update': + return array('edit any resttest content'); + case 'delete': + return array('delete any resttest content'); + } + + case 'comment': + switch ($operation) { + case 'view': + return ['access comments']; + + case 'create': + return ['post comments', 'skip comment approval']; + + case 'update': + return ['edit own comments']; + + case 'delete': + return ['administer comments']; + } + break; + + case 'user': + switch ($operation) { + case 'view': + return ['access user profiles']; + + default: + return ['administer users']; + } + + default: + if ($this->isConfigEntity($entity_type_id)) { + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + if ($admin_permission = $entity_type->getAdminPermission()) { + return [$admin_permission]; + } + } + } + return []; + } + + /** + * Loads an entity based on the location URL returned in the location header. + * + * @param string $location_url + * The URL returned in the Location header. + * + * @return \Drupal\Core\Entity\Entity|false + * The entity or FALSE if there is no matching entity. + */ + protected function loadEntityFromLocationHeader($location_url) { + $url_parts = explode('/', $location_url); + $id = end($url_parts); + return $this->container->get('entity_type.manager') + ->getStorage($this->testEntityType)->load($id); + } + + /** + * Remove node fields that can only be written by an admin user. + * + * @param \Drupal\node\NodeInterface $node + * The node to remove fields where non-administrative users cannot write. + * + * @return \Drupal\node\NodeInterface + * The node with removed fields. + */ + protected function removeNodeFieldsForNonAdminUsers(NodeInterface $node) { + $node->set('status', NULL); + $node->set('created', NULL); + $node->set('changed', NULL); + $node->set('promote', NULL); + $node->set('sticky', NULL); + $node->set('revision_timestamp', NULL); + $node->set('revision_log', NULL); + $node->set('uid', NULL); + + return $node; + } + + /** + * Check to see if the HTTP request response body is identical to the expected + * value. + * + * @param $expected + * The first value to check. + * @param $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertResponseBody($expected, $message = '', $group = 'REST Response') { + return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', array('@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE))), $group); + } + + /** + * Checks if an entity type id is for a Config Entity. + * + * @param string $entity_type_id + * The entity type ID to check. + * + * @return bool + * TRUE if the entity is a Config Entity, FALSE otherwise. + */ + protected function isConfigEntity($entity_type_id) { + return \Drupal::entityTypeManager()->getDefinition($entity_type_id) instanceof ConfigEntityType; + } + + /** + * Provides an array of suitable property values for a config entity type. + * + * Config entities have some common keys that need to be created. Required + * properties differ among config entity types, so we keep a minimum mapping + * here. + * + * @param string $entity_type_id + * The ID of the type of entity that should be created. + * + * @return array + * An array of values keyed by property name. + */ + protected function configEntityValues($entity_type_id) { + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + $keys = $entity_type->getKeys(); + $values = []; + // Fill out known key values that are shared across entity types. + foreach ($keys as $key) { + if ($key === 'id' || $key === 'label') { + $values[$key] = $this->randomMachineName(); + } + } + // Add extra values for particular entity types. + switch ($entity_type_id) { + case 'block': + $values['plugin'] = 'system_powered_by_block'; + break; + } + return $values; + } + +} diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/tests/src/Functional/ReadTest.php similarity index 99% rename from core/modules/rest/src/Tests/ReadTest.php rename to core/modules/rest/tests/src/Functional/ReadTest.php index dc6f574..347f5ab 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/tests/src/Functional/ReadTest.php @@ -1,6 +1,6 @@ profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + } + + /** + * Simulates submission of a form using GET instead of POST. + * + * Forms that use the GET method cannot be submitted with + * WebTestBase::drupalPostForm(), which explicitly uses POST to submit the + * form. So this method finds the form, verifies that it has input fields and + * a submit button matching the inputs to this method, and then calls + * WebTestBase::drupalGet() to simulate the form submission to the 'action' + * URL of the form (if set, or the current URL if not). + * + * See WebTestBase::drupalPostForm() for more detailed documentation of the + * function parameters. + * + * @param string $path + * Location of the form to be submitted: either a Drupal path, absolute + * path, or NULL to use the current page. + * @param array $edit + * Form field data to submit. Unlike drupalPostForm(), this does not support + * file uploads. + * @param string $submit + * Value of the submit button to submit clicking. Unlike drupalPostForm(), + * this does not support AJAX. + * @param string $form_html_id + * (optional) HTML ID of the form, to disambiguate. + */ + protected function submitGetForm($path, $edit, $submit, $form_html_id = NULL) { + if (isset($path)) { + $this->drupalGet($path); + } + + if ($this->parse()) { + // Iterate over forms to find one that matches $edit and $submit. + $edit_save = $edit; + $xpath = '//form'; + if (!empty($form_html_id)) { + $xpath .= "[@id='" . $form_html_id . "']"; + } + $forms = $this->xpath($xpath); + foreach ($forms as $form) { + // Try to set the fields of this form as specified in $edit. + $edit = $edit_save; + $post = array(); + $upload = array(); + $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form); + if (!$edit && $submit_matches) { + // Everything matched, so "submit" the form. + $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : NULL; + $this->drupalGet($action, array('query' => $post)); + return; + } + } + + // We have not found a form which contained all fields of $edit and + // the submit button. + foreach ($edit as $name => $value) { + $this->fail(SafeMarkup::format('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); + } + $this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit))); + $this->fail(format_string('Found the requested form fields at @path', array('@path' => $path))); + } + } + +} diff --git a/core/modules/search/src/Tests/SearchTokenizerTest.php b/core/modules/search/tests/src/Functional/SearchTokenizerTest.php similarity index 99% rename from core/modules/search/src/Tests/SearchTokenizerTest.php rename to core/modules/search/tests/src/Functional/SearchTokenizerTest.php index f8b3ccb..e25bfc4 100644 --- a/core/modules/search/src/Tests/SearchTokenizerTest.php +++ b/core/modules/search/tests/src/Functional/SearchTokenizerTest.php @@ -1,6 +1,6 @@ installEntitySchema('entity_test_mulrev'); + $this->installEntitySchema('user'); + $this->installConfig(array('field')); + \Drupal::service('router.builder')->rebuild(); + \Drupal::moduleHandler()->invoke('rest', 'install'); + + // Auto-create a field for testing. + FieldStorageConfig::create(array( + 'entity_type' => 'entity_test_mulrev', + 'field_name' => 'field_test_text', + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => FALSE, + ))->save(); + FieldConfig::create(array( + 'entity_type' => 'entity_test_mulrev', + 'field_name' => 'field_test_text', + 'bundle' => 'entity_test_mulrev', + 'label' => 'Test text-field', + 'widget' => array( + 'type' => 'text_textfield', + 'weight' => 0, + ), + ))->save(); + } + +} diff --git a/core/modules/serialization/src/Tests/RegisterSerializationClassesCompilerPassTest.php b/core/modules/serialization/tests/src/Functional/RegisterSerializationClassesCompilerPassTest.php similarity index 97% rename from core/modules/serialization/src/Tests/RegisterSerializationClassesCompilerPassTest.php rename to core/modules/serialization/tests/src/Functional/RegisterSerializationClassesCompilerPassTest.php index 6406bbb..5f18927 100644 --- a/core/modules/serialization/src/Tests/RegisterSerializationClassesCompilerPassTest.php +++ b/core/modules/serialization/tests/src/Functional/RegisterSerializationClassesCompilerPassTest.php @@ -1,6 +1,6 @@ profile != 'standard') { + // Create Basic page and Article node types. + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + + // Populate the default shortcut set. + $shortcut = Shortcut::create(array( + 'shortcut_set' => 'default', + 'title' => t('Add content'), + 'weight' => -20, + 'link' => array( + 'uri' => 'internal:/node/add', + ), + )); + $shortcut->save(); + + $shortcut = Shortcut::create(array( + 'shortcut_set' => 'default', + 'title' => t('All content'), + 'weight' => -19, + 'link' => array( + 'uri' => 'internal:/admin/content', + ), + )); + $shortcut->save(); + } + + // Create users. + $this->adminUser = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users', 'link to any page', 'edit any article content')); + $this->shortcutUser = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets', 'access shortcuts', 'access content')); + + // Create a node. + $this->node = $this->drupalCreateNode(array('type' => 'article')); + + // Log in as admin and grab the default shortcut set. + $this->drupalLogin($this->adminUser); + $this->set = ShortcutSet::load('default'); + \Drupal::entityManager()->getStorage('shortcut_set')->assignUser($this->set, $this->adminUser); + } + + /** + * Creates a generic shortcut set. + */ + function generateShortcutSet($label = '', $id = NULL) { + $set = ShortcutSet::create(array( + 'id' => isset($id) ? $id : strtolower($this->randomMachineName()), + 'label' => empty($label) ? $this->randomString() : $label, + )); + $set->save(); + return $set; + } + + /** + * Extracts information from shortcut set links. + * + * @param \Drupal\shortcut\ShortcutSetInterface $set + * The shortcut set object to extract information from. + * @param string $key + * The array key indicating what information to extract from each link: + * - 'title': Extract shortcut titles. + * - 'link': Extract shortcut paths. + * - 'id': Extract the shortcut ID. + * + * @return array + * Array of the requested information from each link. + */ + function getShortcutInformation(ShortcutSetInterface $set, $key) { + $info = array(); + \Drupal::entityManager()->getStorage('shortcut')->resetCache(); + foreach ($set->getShortcuts() as $shortcut) { + if ($key == 'link') { + $info[] = $shortcut->link->uri; + } + else { + $info[] = $shortcut->{$key}->value; + } + } + return $info; + } + +} diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php similarity index 98% rename from core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php rename to core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php index 4be41c3..10eecb1 100644 --- a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php @@ -1,6 +1,6 @@ profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + } + + // Create user. + $this->blockingUser = $this->drupalCreateUser(array( + 'access administration pages', + 'access site reports', + 'ban IP addresses', + 'administer blocks', + 'administer statistics', + 'administer users', + )); + $this->drupalLogin($this->blockingUser); + + // Enable logging. + $this->config('statistics.settings') + ->set('count_content_views', 1) + ->save(); + } + +} diff --git a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php b/core/modules/statistics/tests/src/Functional/StatisticsTokenReplaceTest.php similarity index 97% rename from core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php rename to core/modules/statistics/tests/src/Functional/StatisticsTokenReplaceTest.php index b7d22b8..d586ea6 100644 --- a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php +++ b/core/modules/statistics/tests/src/Functional/StatisticsTokenReplaceTest.php @@ -1,6 +1,6 @@ drupalPlaceBlock('system_breadcrumb_block'); + + // Create Basic page and Article node types. + if ($this->profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyTestTrait.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyTestTrait.php new file mode 100644 index 0000000..b0eeb0d --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyTestTrait.php @@ -0,0 +1,60 @@ + $this->randomMachineName(), + 'description' => $this->randomMachineName(), + 'vid' => Unicode::strtolower($this->randomMachineName()), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'weight' => mt_rand(0, 10), + ]); + $vocabulary->save(); + return $vocabulary; + } + + /** + * Returns a new term with random properties in vocabulary $vid. + * + * @param \Drupal\taxonomy\Entity\Vocabulary $vocabulary + * The vocabulary object. + * @param array $values + * (optional) An array of values to set, keyed by property name. If the + * entity type has bundles, the bundle key has to be specified. + * + * @return \Drupal\taxonomy\Entity\Term + * The new taxonomy term object. + */ + function createTerm(Vocabulary $vocabulary, $values = array()) { + $filter_formats = filter_formats(); + $format = array_pop($filter_formats); + $term = Term::create($values + [ + 'name' => $this->randomMachineName(), + 'description' => [ + 'value' => $this->randomMachineName(), + // Use the first available text format. + 'format' => $format->id(), + ], + 'vid' => $vocabulary->id(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $term->save(); + return $term; + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyTranslationTestTrait.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyTranslationTestTrait.php new file mode 100644 index 0000000..0a40f79 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyTranslationTestTrait.php @@ -0,0 +1,105 @@ +translateToLangcode)->save(); + $this->rebuildContainer(); + } + + /** + * Enables translations where it needed. + */ + protected function enableTranslation() { + // Enable translation for the current entity type and ensure the change is + // picked up. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('taxonomy_term', $this->vocabulary->id(), TRUE); + drupal_static_reset(); + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('router.builder')->rebuild(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + } + + /** + * Adds term reference field for the article content type. + * + * @param bool $translatable + * (optional) If TRUE, create a translatable term reference field. Defaults + * to FALSE. + */ + protected function setUpTermReferenceField() { + $handler_settings = array( + 'target_bundles' => array( + $this->vocabulary->id() => $this->vocabulary->id(), + ), + 'auto_create' => TRUE, + ); + $this->createEntityReferenceField('node', 'article', $this->termFieldName, NULL, 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $field_storage = FieldStorageConfig::loadByName('node', $this->termFieldName); + $field_storage->setTranslatable(FALSE); + $field_storage->save(); + + entity_get_form_display('node', 'article', 'default') + ->setComponent($this->termFieldName, array( + 'type' => 'entity_reference_autocomplete_tags', + )) + ->save(); + entity_get_display('node', 'article', 'default') + ->setComponent($this->termFieldName, array( + 'type' => 'entity_reference_label', + )) + ->save(); + } + +} diff --git a/core/modules/taxonomy/src/Tests/TermCacheTagsTest.php b/core/modules/taxonomy/tests/src/Functional/TermCacheTagsTest.php similarity index 94% rename from core/modules/taxonomy/src/Tests/TermCacheTagsTest.php rename to core/modules/taxonomy/tests/src/Functional/TermCacheTagsTest.php index 8d7ddad..10f24e8 100644 --- a/core/modules/taxonomy/src/Tests/TermCacheTagsTest.php +++ b/core/modules/taxonomy/tests/src/Functional/TermCacheTagsTest.php @@ -1,6 +1,6 @@ assertTourTips(); + * + * // Advanced example. The following would be used for multipage or + * // targeting a specific subset of tips. + * $tips = array(); + * $tips[] = array('data-id' => 'foo'); + * $tips[] = array('data-id' => 'bar'); + * $tips[] = array('data-class' => 'baz'); + * $this->assertTourTips($tips); + * @endcode + */ + public function assertTourTips($tips = array()) { + // Get the rendered tips and their data-id and data-class attributes. + if (empty($tips)) { + // Tips are rendered as
  • elements inside
      . + $rendered_tips = $this->xpath('//ol[@id = "tour"]//li[starts-with(@class, "tip")]'); + foreach ($rendered_tips as $rendered_tip) { + $attributes = (array) $rendered_tip->attributes(); + $tips[] = $attributes['@attributes']; + } + } + + // If the tips are still empty we need to fail. + if (empty($tips)) { + $this->fail('Could not find tour tips on the current page.'); + } + else { + // Check for corresponding page elements. + $total = 0; + $modals = 0; + foreach ($tips as $tip) { + if (!empty($tip['data-id'])) { + $elements = \PHPUnit_Util_XML::cssSelect('#' . $tip['data-id'], TRUE, $this->content, TRUE); + $this->assertTrue(!empty($elements) && count($elements) === 1, format_string('Found corresponding page element for tour tip with id #%data-id', array('%data-id' => $tip['data-id']))); + } + elseif (!empty($tip['data-class'])) { + $elements = \PHPUnit_Util_XML::cssSelect('.' . $tip['data-class'], TRUE, $this->content, TRUE); + $this->assertFalse(empty($elements), format_string('Found corresponding page element for tour tip with class .%data-class', array('%data-class' => $tip['data-class']))); + } + else { + // It's a modal. + $modals++; + } + $total++; + } + $this->pass(format_string('Total %total Tips tested of which %modals modal(s).', array('%total' => $total, '%modals' => $modals))); + } + } + +} diff --git a/core/modules/tour/src/Tests/TourTestBasic.php b/core/modules/tour/tests/src/Functional/TourTestBasic.php similarity index 97% rename from core/modules/tour/src/Tests/TourTestBasic.php rename to core/modules/tour/tests/src/Functional/TourTestBasic.php index e79eb2b..ec92987 100644 --- a/core/modules/tour/src/Tests/TourTestBasic.php +++ b/core/modules/tour/tests/src/Functional/TourTestBasic.php @@ -1,6 +1,6 @@