Chris@0: entityTypeId = 'node'; Chris@0: $this->bundle = 'article'; Chris@0: parent::setUp(); Chris@0: Chris@0: // Ensure the help message is shown even with prefixed paths. Chris@0: $this->drupalPlaceBlock('help_block', ['region' => 'content']); Chris@0: Chris@0: // Display the language selector. Chris@0: $this->drupalLogin($this->administrator); Chris@0: $edit = ['language_configuration[language_alterable]' => TRUE]; Chris@0: $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); Chris@0: $this->drupalLogin($this->translator); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests the basic translation UI. Chris@0: */ Chris@0: public function testTranslationUI() { Chris@0: parent::testTranslationUI(); Chris@0: $this->doUninstallTest(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests changing the published status on a node without fields. Chris@0: */ Chris@0: public function testPublishedStatusNoFields() { Chris@0: // Test changing the published status of an article without fields. Chris@0: $this->drupalLogin($this->administrator); Chris@0: // Delete all fields. Chris@0: $this->drupalGet('admin/structure/types/manage/article/fields'); Chris@0: $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete', [], t('Delete')); Chris@0: $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags/delete', [], t('Delete')); Chris@0: $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/delete', [], t('Delete')); Chris@0: Chris@0: // Add a node. Chris@0: $default_langcode = $this->langcodes[0]; Chris@0: $values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]]; Chris@0: $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); Chris@0: $storage = $this->container->get('entity_type.manager') Chris@0: ->getStorage($this->entityTypeId); Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: Chris@0: // Add a content translation. Chris@0: $langcode = 'fr'; Chris@0: $language = ConfigurableLanguage::load($langcode); Chris@0: $values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]]; Chris@0: Chris@0: $entity_type_id = $entity->getEntityTypeId(); Chris@0: $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ Chris@0: $entity->getEntityTypeId() => $entity->id(), Chris@0: 'source' => $default_langcode, Chris@17: 'target' => $langcode, Chris@0: ], ['language' => $language]); Chris@0: $edit = $this->getEditValues($values, $langcode); Chris@0: $edit['status[value]'] = FALSE; Chris@0: $this->drupalPostForm($add_url, $edit, t('Save (this translation)')); Chris@0: Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: $translation = $entity->getTranslation($langcode); Chris@0: // Make sure we unpublished the node correctly. Chris@0: $this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function getTranslatorPermissions() { Chris@0: return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function getEditorPermissions() { Chris@0: return ['administer nodes', 'create article content']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function getAdministratorPermissions() { Chris@0: return array_merge(parent::getAdministratorPermissions(), ['access administration pages', 'administer content types', 'administer node fields', 'access content overview', 'bypass node access', 'administer languages', 'administer themes', 'view the administration theme']); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function getNewEntityValues($langcode) { Chris@0: return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function doTestPublishedStatus() { Chris@0: $storage = $this->container->get('entity_type.manager') Chris@0: ->getStorage($this->entityTypeId); Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: $languages = $this->container->get('language_manager')->getLanguages(); Chris@0: Chris@0: $statuses = [ Chris@0: TRUE, Chris@0: FALSE, Chris@0: ]; Chris@0: Chris@0: foreach ($statuses as $index => $value) { Chris@0: // (Un)publish the node translations and check that the translation Chris@0: // statuses are (un)published accordingly. Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $options = ['language' => $languages[$langcode]]; Chris@18: $url = $entity->toUrl('edit-form', $options); Chris@0: $this->drupalPostForm($url, ['status[value]' => $value], t('Save') . $this->getFormSubmitSuffix($entity, $langcode), $options); Chris@0: } Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: // The node is created as unpublished thus we switch to the published Chris@0: // status first. Chris@0: $status = !$index; Chris@0: $translation = $entity->getTranslation($langcode); Chris@0: $this->assertEqual($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.'); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function doTestAuthoringInfo() { Chris@0: $storage = $this->container->get('entity_type.manager') Chris@0: ->getStorage($this->entityTypeId); Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: $languages = $this->container->get('language_manager')->getLanguages(); Chris@0: $values = []; Chris@0: Chris@0: // Post different base field information for each translation. Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $user = $this->drupalCreateUser(); Chris@0: $values[$langcode] = [ Chris@0: 'uid' => $user->id(), Chris@0: 'created' => REQUEST_TIME - mt_rand(0, 1000), Chris@0: 'sticky' => (bool) mt_rand(0, 1), Chris@0: 'promote' => (bool) mt_rand(0, 1), Chris@0: ]; Chris@18: /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */ Chris@18: $date_formatter = $this->container->get('date.formatter'); Chris@0: $edit = [ Chris@18: 'uid[0][target_id]' => $user->getAccountName(), Chris@18: 'created[0][value][date]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'Y-m-d'), Chris@18: 'created[0][value][time]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'H:i:s'), Chris@0: 'sticky[value]' => $values[$langcode]['sticky'], Chris@0: 'promote[value]' => $values[$langcode]['promote'], Chris@0: ]; Chris@0: $options = ['language' => $languages[$langcode]]; Chris@18: $url = $entity->toUrl('edit-form', $options); Chris@0: $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode), $options); Chris@0: } Chris@0: Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $translation = $entity->getTranslation($langcode); Chris@0: $metadata = $this->manager->getTranslationMetadata($translation); Chris@0: $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.'); Chris@0: $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.'); Chris@0: $this->assertEqual($translation->isSticky(), $values[$langcode]['sticky'], 'Sticky of Translation correctly stored.'); Chris@0: $this->assertEqual($translation->isPromoted(), $values[$langcode]['promote'], 'Promoted of Translation correctly stored.'); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that translation page inherits admin status of edit page. Chris@0: */ Chris@0: public function testTranslationLinkTheme() { Chris@0: $this->drupalLogin($this->administrator); Chris@0: $article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]); Chris@0: Chris@0: // Set up Seven as the admin theme and use it for node editing. Chris@0: $this->container->get('theme_handler')->install(['seven']); Chris@0: $edit = []; Chris@0: $edit['admin_theme'] = 'seven'; Chris@0: $edit['use_admin_theme'] = TRUE; Chris@0: $this->drupalPostForm('admin/appearance', $edit, t('Save configuration')); Chris@0: $this->drupalGet('node/' . $article->id() . '/translations'); Chris@0: $this->assertRaw('core/themes/seven/css/base/elements.css', 'Translation uses admin theme if edit is admin.'); Chris@0: Chris@0: // Turn off admin theme for editing, assert inheritance to translations. Chris@0: $edit['use_admin_theme'] = FALSE; Chris@0: $this->drupalPostForm('admin/appearance', $edit, t('Save configuration')); Chris@0: $this->drupalGet('node/' . $article->id() . '/translations'); Chris@0: $this->assertNoRaw('core/themes/seven/css/base/elements.css', 'Translation uses frontend theme if edit is frontend.'); Chris@0: Chris@0: // Assert presence of translation page itself (vs. DisabledBundle below). Chris@0: $this->assertResponse(200); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that no metadata is stored for a disabled bundle. Chris@0: */ Chris@0: public function testDisabledBundle() { Chris@0: // Create a bundle that does not have translation enabled. Chris@0: $disabledBundle = $this->randomMachineName(); Chris@0: $this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]); Chris@0: Chris@0: // Create a node for each bundle. Chris@0: $node = $this->drupalCreateNode([ Chris@0: 'type' => $this->bundle, Chris@0: 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, Chris@0: ]); Chris@0: Chris@0: // Make sure that nothing was inserted into the {content_translation} table. Chris@0: $rows = db_query('SELECT nid, count(nid) AS count FROM {node_field_data} WHERE type <> :type GROUP BY nid HAVING count(nid) >= 2', [':type' => $this->bundle])->fetchAll(); Chris@0: $this->assertEqual(0, count($rows)); Chris@0: Chris@0: // Ensure the translation tab is not accessible. Chris@0: $this->drupalGet('node/' . $node->id() . '/translations'); Chris@0: $this->assertResponse(403); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that translations are rendered properly. Chris@0: */ Chris@0: public function testTranslationRendering() { Chris@0: $default_langcode = $this->langcodes[0]; Chris@0: $values[$default_langcode] = $this->getNewEntityValues($default_langcode); Chris@0: $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); Chris@0: $node = \Drupal::entityManager()->getStorage($this->entityTypeId)->load($this->entityId); Chris@0: $node->setPromoted(TRUE); Chris@0: Chris@0: // Create translations. Chris@0: foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) { Chris@0: $values[$langcode] = $this->getNewEntityValues($langcode); Chris@0: $translation = $node->addTranslation($langcode, $values[$langcode]); Chris@0: // Publish and promote the translation to frontpage. Chris@0: $translation->setPromoted(TRUE); Chris@17: $translation->setPublished(); Chris@0: } Chris@0: $node->save(); Chris@0: Chris@0: // Test that the frontpage view displays the correct translations. Chris@0: \Drupal::service('module_installer')->install(['views'], TRUE); Chris@0: $this->rebuildContainer(); Chris@0: $this->doTestTranslations('node', $values); Chris@0: Chris@0: // Enable the translation language renderer. Chris@0: $view = \Drupal::entityManager()->getStorage('view')->load('frontpage'); Chris@0: $display = &$view->getDisplay('default'); Chris@0: $display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***'; Chris@0: $view->save(); Chris@0: Chris@0: // Need to check from the beginning, including the base_path, in the url Chris@0: // since the pattern for the default language might be a substring of Chris@0: // the strings for other languages. Chris@0: $base_path = base_path(); Chris@0: Chris@0: // Check the frontpage for 'Read more' links to each translation. Chris@0: // See also assertTaxonomyPage() in NodeAccessBaseTableTest. Chris@0: $node_href = 'node/' . $node->id(); Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]); Chris@0: $num_match_found = 0; Chris@0: if ($langcode == 'en') { Chris@0: // Site default language does not have langcode prefix in the URL. Chris@0: $expected_href = $base_path . $node_href; Chris@0: } Chris@0: else { Chris@0: $expected_href = $base_path . $langcode . '/' . $node_href; Chris@0: } Chris@0: $pattern = '|^' . $expected_href . '$|'; Chris@0: foreach ($this->xpath("//a[text()='Read more']") as $link) { Chris@0: if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) { Chris@0: $num_match_found++; Chris@0: } Chris@0: } Chris@0: $this->assertTrue($num_match_found == 1, 'There is 1 Read more link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)'); Chris@0: } Chris@0: Chris@0: // Check the frontpage for 'Add new comment' links that include the Chris@0: // language. Chris@0: $comment_form_href = 'node/' . $node->id() . '#comment-form'; Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]); Chris@0: $num_match_found = 0; Chris@0: if ($langcode == 'en') { Chris@0: // Site default language does not have langcode prefix in the URL. Chris@0: $expected_href = $base_path . $comment_form_href; Chris@0: } Chris@0: else { Chris@0: $expected_href = $base_path . $langcode . '/' . $comment_form_href; Chris@0: } Chris@0: $pattern = '|^' . $expected_href . '$|'; Chris@0: foreach ($this->xpath("//a[text()='Add new comment']") as $link) { Chris@0: if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) { Chris@0: $num_match_found++; Chris@0: } Chris@0: } Chris@0: $this->assertTrue($num_match_found == 1, 'There is 1 Add new comment link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)'); Chris@0: } Chris@0: Chris@0: // Test that the node page displays the correct translations. Chris@0: $this->doTestTranslations('node/' . $node->id(), $values); Chris@0: Chris@0: // Test that the node page has the correct alternate hreflang links. Chris@18: $this->doTestAlternateHreflangLinks($node->toUrl()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that the given path displays the correct translation values. Chris@0: * Chris@0: * @param string $path Chris@0: * The path to be tested. Chris@0: * @param array $values Chris@0: * The translation values to be found. Chris@0: */ Chris@0: protected function doTestTranslations($path, array $values) { Chris@0: $languages = $this->container->get('language_manager')->getLanguages(); Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $this->drupalGet($path, ['language' => $languages[$langcode]]); Chris@0: $this->assertText($values[$langcode]['title'][0]['value'], format_string('The %langcode node translation is correctly displayed.', ['%langcode' => $langcode])); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that the given path provides the correct alternate hreflang links. Chris@0: * Chris@0: * @param \Drupal\Core\Url $url Chris@0: * The path to be tested. Chris@0: */ Chris@0: protected function doTestAlternateHreflangLinks(Url $url) { Chris@0: $languages = $this->container->get('language_manager')->getLanguages(); Chris@0: $url->setAbsolute(); Chris@0: $urls = []; Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $language_url = clone $url; Chris@0: $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]); Chris@0: } Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: $this->drupalGet($urls[$langcode]); Chris@0: foreach ($urls as $alternate_langcode => $language_url) { Chris@0: // Retrieve desired link elements from the HTML head. Chris@0: $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]', Chris@0: [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]); Chris@0: $this->assert(isset($links[0]), format_string('The %langcode node translation has the correct alternate hreflang link for %alternate_langcode: %link.', ['%langcode' => $langcode, '%alternate_langcode' => $alternate_langcode, '%link' => $url->toString()])); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) { Chris@0: if (!$entity->isNew() && $entity->isTranslatable()) { Chris@0: $translations = $entity->getTranslationLanguages(); Chris@0: if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) { Chris@0: return ' ' . ($field->isTranslatable() ? t('(this translation)') : t('(all translations)')); Chris@0: } Chris@0: } Chris@0: return ''; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests uninstalling content_translation. Chris@0: */ Chris@0: protected function doUninstallTest() { Chris@0: // Delete all the nodes so there is no data. Chris@0: $nodes = Node::loadMultiple(); Chris@0: foreach ($nodes as $node) { Chris@0: $node->delete(); Chris@0: } Chris@0: $language_count = count(\Drupal::configFactory()->listAll('language.content_settings.')); Chris@0: \Drupal::service('module_installer')->uninstall(['content_translation']); Chris@0: $this->rebuildContainer(); Chris@0: $this->assertEqual($language_count, count(\Drupal::configFactory()->listAll('language.content_settings.')), 'Languages have been fixed rather than deleted during content_translation uninstall.'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function doTestTranslationEdit() { Chris@0: $storage = $this->container->get('entity_type.manager') Chris@0: ->getStorage($this->entityTypeId); Chris@0: $storage->resetCache([$this->entityId]); Chris@0: $entity = $storage->load($this->entityId); Chris@0: $languages = $this->container->get('language_manager')->getLanguages(); Chris@0: $type_name = node_get_type_label($entity); Chris@0: Chris@0: foreach ($this->langcodes as $langcode) { Chris@0: // We only want to test the title for non-english translations. Chris@0: if ($langcode != 'en') { Chris@0: $options = ['language' => $languages[$langcode]]; Chris@18: $url = $entity->toUrl('edit-form', $options); Chris@0: $this->drupalGet($url); Chris@0: Chris@0: $title = t('Edit @type @title [%language translation]', [ Chris@0: '@type' => $type_name, Chris@0: '@title' => $entity->getTranslation($langcode)->label(), Chris@0: '%language' => $languages[$langcode]->getName(), Chris@0: ]); Chris@0: $this->assertRaw($title); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests that revision translations are rendered properly. Chris@0: */ Chris@0: public function testRevisionTranslationRendering() { Chris@0: $storage = \Drupal::entityTypeManager()->getStorage('node'); Chris@0: Chris@0: // Create a node. Chris@0: $nid = $this->createEntity(['title' => 'First rev en title'], 'en'); Chris@0: $node = $storage->load($nid); Chris@0: $original_revision_id = $node->getRevisionId(); Chris@0: Chris@0: // Add a French translation. Chris@0: $translation = $node->addTranslation('fr'); Chris@0: $translation->title = 'First rev fr title'; Chris@0: $translation->setNewRevision(FALSE); Chris@0: $translation->save(); Chris@0: Chris@0: // Create a new revision. Chris@0: $node->title = 'Second rev en title'; Chris@0: $node->setNewRevision(TRUE); Chris@0: $node->save(); Chris@0: Chris@0: // Get an English view of this revision. Chris@0: $original_revision = $storage->loadRevision($original_revision_id); Chris@0: $original_revision_url = $original_revision->toUrl('revision')->toString(); Chris@0: Chris@0: // Should be different from regular node URL. Chris@0: $this->assertNotIdentical($original_revision_url, $original_revision->toUrl()->toString()); Chris@0: $this->drupalGet($original_revision_url); Chris@0: $this->assertResponse(200); Chris@0: Chris@0: // Contents should be in English, of correct revision. Chris@0: $this->assertText('First rev en title'); Chris@0: $this->assertNoText('First rev fr title'); Chris@0: Chris@0: // Get a French view. Chris@0: $url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString(); Chris@0: Chris@0: // Should have different URL from English. Chris@0: $this->assertNotIdentical($url_fr, $original_revision->toUrl()->toString()); Chris@0: $this->assertNotIdentical($url_fr, $original_revision_url); Chris@0: $this->drupalGet($url_fr); Chris@0: $this->assertResponse(200); Chris@0: Chris@0: // Contents should be in French, of correct revision. Chris@0: $this->assertText('First rev fr title'); Chris@0: $this->assertNoText('First rev en title'); Chris@0: } Chris@0: Chris@17: /** Chris@17: * Test that title is not escaped (but XSS-filtered) for details form element. Chris@17: */ Chris@17: public function testDetailsTitleIsNotEscaped() { Chris@17: $this->drupalLogin($this->administrator); Chris@17: // Make the image field a multi-value field in order to display a Chris@17: // details form element. Chris@17: $edit = ['cardinality_number' => 2]; Chris@17: $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/storage', $edit, t('Save field settings')); Chris@17: Chris@17: // Make the image field non-translatable. Chris@17: $edit = ['settings[node][article][fields][field_image]' => FALSE]; Chris@17: $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); Chris@17: Chris@17: // Create a node. Chris@17: $nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en'); Chris@17: Chris@17: // Add a French translation and assert the title markup is not escaped. Chris@17: $this->drupalGet("node/$nid/translations/add/en/fr"); Chris@17: $markup = 'Image (all languages)'; Chris@17: $this->assertSession()->assertNoEscaped($markup); Chris@17: $this->assertSession()->responseContains($markup); Chris@17: } Chris@17: Chris@0: }