3 namespace Drupal\Tests\node\Functional;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
7 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\node\Entity\Node;
10 use Drupal\language\Entity\ConfigurableLanguage;
13 * Tests the Node Translation UI.
17 class NodeTranslationUITest extends ContentTranslationUITestBase {
22 protected $defaultCacheContexts = [
23 'languages:language_interface',
28 'url.query_args:_wrapper_format',
31 // These two cache contexts are added by BigPipe.
32 'cookies:big_pipe_nojs',
41 public static $modules = ['block', 'language', 'content_translation', 'node', 'datetime', 'field_ui', 'help'];
44 * The profile to install as a basis for testing.
48 protected $profile = 'standard';
50 protected function setUp() {
51 $this->entityTypeId = 'node';
52 $this->bundle = 'article';
55 // Ensure the help message is shown even with prefixed paths.
56 $this->drupalPlaceBlock('help_block', ['region' => 'content']);
58 // Display the language selector.
59 $this->drupalLogin($this->administrator);
60 $edit = ['language_configuration[language_alterable]' => TRUE];
61 $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type'));
62 $this->drupalLogin($this->translator);
66 * Tests the basic translation UI.
68 public function testTranslationUI() {
69 parent::testTranslationUI();
70 $this->doUninstallTest();
74 * Tests changing the published status on a node without fields.
76 public function testPublishedStatusNoFields() {
77 // Test changing the published status of an article without fields.
78 $this->drupalLogin($this->administrator);
80 $this->drupalGet('admin/structure/types/manage/article/fields');
81 $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete', [], t('Delete'));
82 $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags/delete', [], t('Delete'));
83 $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/delete', [], t('Delete'));
86 $default_langcode = $this->langcodes[0];
87 $values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]];
88 $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
89 $storage = $this->container->get('entity_type.manager')
90 ->getStorage($this->entityTypeId);
91 $storage->resetCache([$this->entityId]);
92 $entity = $storage->load($this->entityId);
94 // Add a content translation.
96 $language = ConfigurableLanguage::load($langcode);
97 $values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
99 $entity_type_id = $entity->getEntityTypeId();
100 $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
101 $entity->getEntityTypeId() => $entity->id(),
102 'source' => $default_langcode,
103 'target' => $langcode,
104 ], ['language' => $language]);
105 $edit = $this->getEditValues($values, $langcode);
106 $edit['status[value]'] = FALSE;
107 $this->drupalPostForm($add_url, $edit, t('Save (this translation)'));
109 $storage->resetCache([$this->entityId]);
110 $entity = $storage->load($this->entityId);
111 $translation = $entity->getTranslation($langcode);
112 // Make sure we unpublished the node correctly.
113 $this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
119 protected function getTranslatorPermissions() {
120 return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
126 protected function getEditorPermissions() {
127 return ['administer nodes', 'create article content'];
133 protected function getAdministratorPermissions() {
134 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']);
140 protected function getNewEntityValues($langcode) {
141 return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
147 protected function doTestPublishedStatus() {
148 $storage = $this->container->get('entity_type.manager')
149 ->getStorage($this->entityTypeId);
150 $storage->resetCache([$this->entityId]);
151 $entity = $storage->load($this->entityId);
152 $languages = $this->container->get('language_manager')->getLanguages();
159 foreach ($statuses as $index => $value) {
160 // (Un)publish the node translations and check that the translation
161 // statuses are (un)published accordingly.
162 foreach ($this->langcodes as $langcode) {
163 $options = ['language' => $languages[$langcode]];
164 $url = $entity->urlInfo('edit-form', $options);
165 $this->drupalPostForm($url, ['status[value]' => $value], t('Save') . $this->getFormSubmitSuffix($entity, $langcode), $options);
167 $storage->resetCache([$this->entityId]);
168 $entity = $storage->load($this->entityId);
169 foreach ($this->langcodes as $langcode) {
170 // The node is created as unpublished thus we switch to the published
173 $translation = $entity->getTranslation($langcode);
174 $this->assertEqual($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
182 protected function doTestAuthoringInfo() {
183 $storage = $this->container->get('entity_type.manager')
184 ->getStorage($this->entityTypeId);
185 $storage->resetCache([$this->entityId]);
186 $entity = $storage->load($this->entityId);
187 $languages = $this->container->get('language_manager')->getLanguages();
190 // Post different base field information for each translation.
191 foreach ($this->langcodes as $langcode) {
192 $user = $this->drupalCreateUser();
193 $values[$langcode] = [
194 'uid' => $user->id(),
195 'created' => REQUEST_TIME - mt_rand(0, 1000),
196 'sticky' => (bool) mt_rand(0, 1),
197 'promote' => (bool) mt_rand(0, 1),
200 'uid[0][target_id]' => $user->getUsername(),
201 'created[0][value][date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'),
202 'created[0][value][time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'),
203 'sticky[value]' => $values[$langcode]['sticky'],
204 'promote[value]' => $values[$langcode]['promote'],
206 $options = ['language' => $languages[$langcode]];
207 $url = $entity->urlInfo('edit-form', $options);
208 $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode), $options);
211 $storage->resetCache([$this->entityId]);
212 $entity = $storage->load($this->entityId);
213 foreach ($this->langcodes as $langcode) {
214 $translation = $entity->getTranslation($langcode);
215 $metadata = $this->manager->getTranslationMetadata($translation);
216 $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
217 $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
218 $this->assertEqual($translation->isSticky(), $values[$langcode]['sticky'], 'Sticky of Translation correctly stored.');
219 $this->assertEqual($translation->isPromoted(), $values[$langcode]['promote'], 'Promoted of Translation correctly stored.');
224 * Tests that translation page inherits admin status of edit page.
226 public function testTranslationLinkTheme() {
227 $this->drupalLogin($this->administrator);
228 $article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
230 // Set up Seven as the admin theme and use it for node editing.
231 $this->container->get('theme_handler')->install(['seven']);
233 $edit['admin_theme'] = 'seven';
234 $edit['use_admin_theme'] = TRUE;
235 $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
236 $this->drupalGet('node/' . $article->id() . '/translations');
237 $this->assertRaw('core/themes/seven/css/base/elements.css', 'Translation uses admin theme if edit is admin.');
239 // Turn off admin theme for editing, assert inheritance to translations.
240 $edit['use_admin_theme'] = FALSE;
241 $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
242 $this->drupalGet('node/' . $article->id() . '/translations');
243 $this->assertNoRaw('core/themes/seven/css/base/elements.css', 'Translation uses frontend theme if edit is frontend.');
245 // Assert presence of translation page itself (vs. DisabledBundle below).
246 $this->assertResponse(200);
250 * Tests that no metadata is stored for a disabled bundle.
252 public function testDisabledBundle() {
253 // Create a bundle that does not have translation enabled.
254 $disabledBundle = $this->randomMachineName();
255 $this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]);
257 // Create a node for each bundle.
258 $node = $this->drupalCreateNode([
259 'type' => $this->bundle,
260 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
263 // Make sure that nothing was inserted into the {content_translation} table.
264 $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();
265 $this->assertEqual(0, count($rows));
267 // Ensure the translation tab is not accessible.
268 $this->drupalGet('node/' . $node->id() . '/translations');
269 $this->assertResponse(403);
273 * Tests that translations are rendered properly.
275 public function testTranslationRendering() {
276 $default_langcode = $this->langcodes[0];
277 $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
278 $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
279 $node = \Drupal::entityManager()->getStorage($this->entityTypeId)->load($this->entityId);
280 $node->setPromoted(TRUE);
282 // Create translations.
283 foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) {
284 $values[$langcode] = $this->getNewEntityValues($langcode);
285 $translation = $node->addTranslation($langcode, $values[$langcode]);
286 // Publish and promote the translation to frontpage.
287 $translation->setPromoted(TRUE);
288 $translation->setPublished();
292 // Test that the frontpage view displays the correct translations.
293 \Drupal::service('module_installer')->install(['views'], TRUE);
294 $this->rebuildContainer();
295 $this->doTestTranslations('node', $values);
297 // Enable the translation language renderer.
298 $view = \Drupal::entityManager()->getStorage('view')->load('frontpage');
299 $display = &$view->getDisplay('default');
300 $display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***';
303 // Need to check from the beginning, including the base_path, in the url
304 // since the pattern for the default language might be a substring of
305 // the strings for other languages.
306 $base_path = base_path();
308 // Check the frontpage for 'Read more' links to each translation.
309 // See also assertTaxonomyPage() in NodeAccessBaseTableTest.
310 $node_href = 'node/' . $node->id();
311 foreach ($this->langcodes as $langcode) {
312 $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
313 $num_match_found = 0;
314 if ($langcode == 'en') {
315 // Site default language does not have langcode prefix in the URL.
316 $expected_href = $base_path . $node_href;
319 $expected_href = $base_path . $langcode . '/' . $node_href;
321 $pattern = '|^' . $expected_href . '$|';
322 foreach ($this->xpath("//a[text()='Read more']") as $link) {
323 if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
327 $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 . '.)');
330 // Check the frontpage for 'Add new comment' links that include the
332 $comment_form_href = 'node/' . $node->id() . '#comment-form';
333 foreach ($this->langcodes as $langcode) {
334 $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
335 $num_match_found = 0;
336 if ($langcode == 'en') {
337 // Site default language does not have langcode prefix in the URL.
338 $expected_href = $base_path . $comment_form_href;
341 $expected_href = $base_path . $langcode . '/' . $comment_form_href;
343 $pattern = '|^' . $expected_href . '$|';
344 foreach ($this->xpath("//a[text()='Add new comment']") as $link) {
345 if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
349 $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 . '.)');
352 // Test that the node page displays the correct translations.
353 $this->doTestTranslations('node/' . $node->id(), $values);
355 // Test that the node page has the correct alternate hreflang links.
356 $this->doTestAlternateHreflangLinks($node->urlInfo());
360 * Tests that the given path displays the correct translation values.
362 * @param string $path
363 * The path to be tested.
364 * @param array $values
365 * The translation values to be found.
367 protected function doTestTranslations($path, array $values) {
368 $languages = $this->container->get('language_manager')->getLanguages();
369 foreach ($this->langcodes as $langcode) {
370 $this->drupalGet($path, ['language' => $languages[$langcode]]);
371 $this->assertText($values[$langcode]['title'][0]['value'], format_string('The %langcode node translation is correctly displayed.', ['%langcode' => $langcode]));
376 * Tests that the given path provides the correct alternate hreflang links.
378 * @param \Drupal\Core\Url $url
379 * The path to be tested.
381 protected function doTestAlternateHreflangLinks(Url $url) {
382 $languages = $this->container->get('language_manager')->getLanguages();
385 foreach ($this->langcodes as $langcode) {
386 $language_url = clone $url;
387 $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
389 foreach ($this->langcodes as $langcode) {
390 $this->drupalGet($urls[$langcode]);
391 foreach ($urls as $alternate_langcode => $language_url) {
392 // Retrieve desired link elements from the HTML head.
393 $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]',
394 [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]);
395 $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()]));
403 protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
404 if (!$entity->isNew() && $entity->isTranslatable()) {
405 $translations = $entity->getTranslationLanguages();
406 if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) {
407 return ' ' . ($field->isTranslatable() ? t('(this translation)') : t('(all translations)'));
414 * Tests uninstalling content_translation.
416 protected function doUninstallTest() {
417 // Delete all the nodes so there is no data.
418 $nodes = Node::loadMultiple();
419 foreach ($nodes as $node) {
422 $language_count = count(\Drupal::configFactory()->listAll('language.content_settings.'));
423 \Drupal::service('module_installer')->uninstall(['content_translation']);
424 $this->rebuildContainer();
425 $this->assertEqual($language_count, count(\Drupal::configFactory()->listAll('language.content_settings.')), 'Languages have been fixed rather than deleted during content_translation uninstall.');
431 protected function doTestTranslationEdit() {
432 $storage = $this->container->get('entity_type.manager')
433 ->getStorage($this->entityTypeId);
434 $storage->resetCache([$this->entityId]);
435 $entity = $storage->load($this->entityId);
436 $languages = $this->container->get('language_manager')->getLanguages();
437 $type_name = node_get_type_label($entity);
439 foreach ($this->langcodes as $langcode) {
440 // We only want to test the title for non-english translations.
441 if ($langcode != 'en') {
442 $options = ['language' => $languages[$langcode]];
443 $url = $entity->urlInfo('edit-form', $options);
444 $this->drupalGet($url);
446 $title = t('<em>Edit @type</em> @title [%language translation]', [
447 '@type' => $type_name,
448 '@title' => $entity->getTranslation($langcode)->label(),
449 '%language' => $languages[$langcode]->getName(),
451 $this->assertRaw($title);
457 * Tests that revision translations are rendered properly.
459 public function testRevisionTranslationRendering() {
460 $storage = \Drupal::entityTypeManager()->getStorage('node');
463 $nid = $this->createEntity(['title' => 'First rev en title'], 'en');
464 $node = $storage->load($nid);
465 $original_revision_id = $node->getRevisionId();
467 // Add a French translation.
468 $translation = $node->addTranslation('fr');
469 $translation->title = 'First rev fr title';
470 $translation->setNewRevision(FALSE);
471 $translation->save();
473 // Create a new revision.
474 $node->title = 'Second rev en title';
475 $node->setNewRevision(TRUE);
478 // Get an English view of this revision.
479 $original_revision = $storage->loadRevision($original_revision_id);
480 $original_revision_url = $original_revision->toUrl('revision')->toString();
482 // Should be different from regular node URL.
483 $this->assertNotIdentical($original_revision_url, $original_revision->toUrl()->toString());
484 $this->drupalGet($original_revision_url);
485 $this->assertResponse(200);
487 // Contents should be in English, of correct revision.
488 $this->assertText('First rev en title');
489 $this->assertNoText('First rev fr title');
491 // Get a French view.
492 $url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
494 // Should have different URL from English.
495 $this->assertNotIdentical($url_fr, $original_revision->toUrl()->toString());
496 $this->assertNotIdentical($url_fr, $original_revision_url);
497 $this->drupalGet($url_fr);
498 $this->assertResponse(200);
500 // Contents should be in French, of correct revision.
501 $this->assertText('First rev fr title');
502 $this->assertNoText('First rev en title');
506 * Test that title is not escaped (but XSS-filtered) for details form element.
508 public function testDetailsTitleIsNotEscaped() {
509 $this->drupalLogin($this->administrator);
510 // Make the image field a multi-value field in order to display a
511 // details form element.
512 $edit = ['cardinality_number' => 2];
513 $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/storage', $edit, t('Save field settings'));
515 // Make the image field non-translatable.
516 $edit = ['settings[node][article][fields][field_image]' => FALSE];
517 $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
520 $nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en');
522 // Add a French translation and assert the title markup is not escaped.
523 $this->drupalGet("node/$nid/translations/add/en/fr");
524 $markup = 'Image <span class="translation-entity-all-languages">(all languages)</span>';
525 $this->assertSession()->assertNoEscaped($markup);
526 $this->assertSession()->responseContains($markup);