54d9c7303846acd4dbbb0b665f71623c7e80b52d
[yaffs-website] / web / core / modules / node / tests / src / Functional / NodeTranslationUITest.php
1 <?php
2
3 namespace Drupal\Tests\node\Functional;
4
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
7 use Drupal\Core\Language\LanguageInterface;
8 use Drupal\Core\Url;
9 use Drupal\node\Entity\Node;
10 use Drupal\language\Entity\ConfigurableLanguage;
11
12 /**
13  * Tests the Node Translation UI.
14  *
15  * @group node
16  */
17 class NodeTranslationUITest extends ContentTranslationUITestBase {
18
19   /**
20    * {inheritdoc}
21    */
22   protected $defaultCacheContexts = [
23     'languages:language_interface',
24     'theme',
25     'route',
26     'timezone',
27     'url.path.parent',
28     'url.query_args:_wrapper_format',
29     'user.roles',
30     'url.path.is_front',
31     // These two cache contexts are added by BigPipe.
32     'cookies:big_pipe_nojs',
33     'session.exists',
34   ];
35
36   /**
37    * Modules to enable.
38    *
39    * @var array
40    */
41   public static $modules = ['block', 'language', 'content_translation', 'node', 'datetime', 'field_ui', 'help'];
42
43   /**
44    * The profile to install as a basis for testing.
45    *
46    * @var string
47    */
48   protected $profile = 'standard';
49
50   protected function setUp() {
51     $this->entityTypeId = 'node';
52     $this->bundle = 'article';
53     parent::setUp();
54
55     // Ensure the help message is shown even with prefixed paths.
56     $this->drupalPlaceBlock('help_block', ['region' => 'content']);
57
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);
63   }
64
65   /**
66    * Tests the basic translation UI.
67    */
68   public function testTranslationUI() {
69     parent::testTranslationUI();
70     $this->doUninstallTest();
71   }
72
73   /**
74    * Tests changing the published status on a node without fields.
75    */
76   public function testPublishedStatusNoFields() {
77     // Test changing the published status of an article without fields.
78     $this->drupalLogin($this->administrator);
79     // Delete all fields.
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'));
84
85     // Add a node.
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);
93
94     // Add a content translation.
95     $langcode = 'fr';
96     $language = ConfigurableLanguage::load($langcode);
97     $values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
98
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)'));
108
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.');
114   }
115
116   /**
117    * {@inheritdoc}
118    */
119   protected function getTranslatorPermissions() {
120     return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
121   }
122
123   /**
124    * {@inheritdoc}
125    */
126   protected function getEditorPermissions() {
127     return ['administer nodes', 'create article content'];
128   }
129
130   /**
131    * {@inheritdoc}
132    */
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']);
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   protected function getNewEntityValues($langcode) {
141     return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
142   }
143
144   /**
145    * {@inheritdoc}
146    */
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();
153
154     $statuses = [
155       TRUE,
156       FALSE,
157     ];
158
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);
166       }
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
171         // status first.
172         $status = !$index;
173         $translation = $entity->getTranslation($langcode);
174         $this->assertEqual($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
175       }
176     }
177   }
178
179   /**
180    * {@inheritdoc}
181    */
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();
188     $values = [];
189
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),
198       ];
199       $edit = [
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'],
205       ];
206       $options = ['language' => $languages[$langcode]];
207       $url = $entity->urlInfo('edit-form', $options);
208       $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode), $options);
209     }
210
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.');
220     }
221   }
222
223   /**
224    * Tests that translation page inherits admin status of edit page.
225    */
226   public function testTranslationLinkTheme() {
227     $this->drupalLogin($this->administrator);
228     $article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
229
230     // Set up Seven as the admin theme and use it for node editing.
231     $this->container->get('theme_handler')->install(['seven']);
232     $edit = [];
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.');
238
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.');
244
245     // Assert presence of translation page itself (vs. DisabledBundle below).
246     $this->assertResponse(200);
247   }
248
249   /**
250    * Tests that no metadata is stored for a disabled bundle.
251    */
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]);
256
257     // Create a node for each bundle.
258     $node = $this->drupalCreateNode([
259       'type' => $this->bundle,
260       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
261     ]);
262
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));
266
267     // Ensure the translation tab is not accessible.
268     $this->drupalGet('node/' . $node->id() . '/translations');
269     $this->assertResponse(403);
270   }
271
272   /**
273    * Tests that translations are rendered properly.
274    */
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);
281
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();
289     }
290     $node->save();
291
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);
296
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***';
301     $view->save();
302
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();
307
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;
317       }
318       else {
319         $expected_href = $base_path . $langcode . '/' . $node_href;
320       }
321       $pattern = '|^' . $expected_href . '$|';
322       foreach ($this->xpath("//a[text()='Read more']") as $link) {
323         if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
324           $num_match_found++;
325         }
326       }
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 . '.)');
328     }
329
330     // Check the frontpage for 'Add new comment' links that include the
331     // language.
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;
339       }
340       else {
341         $expected_href = $base_path . $langcode . '/' . $comment_form_href;
342       }
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) {
346           $num_match_found++;
347         }
348       }
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 . '.)');
350     }
351
352     // Test that the node page displays the correct translations.
353     $this->doTestTranslations('node/' . $node->id(), $values);
354
355     // Test that the node page has the correct alternate hreflang links.
356     $this->doTestAlternateHreflangLinks($node->urlInfo());
357   }
358
359   /**
360    * Tests that the given path displays the correct translation values.
361    *
362    * @param string $path
363    *   The path to be tested.
364    * @param array $values
365    *   The translation values to be found.
366    */
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]));
372     }
373   }
374
375   /**
376    * Tests that the given path provides the correct alternate hreflang links.
377    *
378    * @param \Drupal\Core\Url $url
379    *   The path to be tested.
380    */
381   protected function doTestAlternateHreflangLinks(Url $url) {
382     $languages = $this->container->get('language_manager')->getLanguages();
383     $url->setAbsolute();
384     $urls = [];
385     foreach ($this->langcodes as $langcode) {
386       $language_url = clone $url;
387       $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
388     }
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()]));
396       }
397     }
398   }
399
400   /**
401    * {@inheritdoc}
402    */
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)'));
408       }
409     }
410     return '';
411   }
412
413   /**
414    * Tests uninstalling content_translation.
415    */
416   protected function doUninstallTest() {
417     // Delete all the nodes so there is no data.
418     $nodes = Node::loadMultiple();
419     foreach ($nodes as $node) {
420       $node->delete();
421     }
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.');
426   }
427
428   /**
429    * {@inheritdoc}
430    */
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);
438
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);
445
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(),
450         ]);
451         $this->assertRaw($title);
452       }
453     }
454   }
455
456   /**
457    * Tests that revision translations are rendered properly.
458    */
459   public function testRevisionTranslationRendering() {
460     $storage = \Drupal::entityTypeManager()->getStorage('node');
461
462     // Create a node.
463     $nid = $this->createEntity(['title' => 'First rev en title'], 'en');
464     $node = $storage->load($nid);
465     $original_revision_id = $node->getRevisionId();
466
467     // Add a French translation.
468     $translation = $node->addTranslation('fr');
469     $translation->title = 'First rev fr title';
470     $translation->setNewRevision(FALSE);
471     $translation->save();
472
473     // Create a new revision.
474     $node->title = 'Second rev en title';
475     $node->setNewRevision(TRUE);
476     $node->save();
477
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();
481
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);
486
487     // Contents should be in English, of correct revision.
488     $this->assertText('First rev en title');
489     $this->assertNoText('First rev fr title');
490
491     // Get a French view.
492     $url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
493
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);
499
500     // Contents should be in French, of correct revision.
501     $this->assertText('First rev fr title');
502     $this->assertNoText('First rev en title');
503   }
504
505   /**
506    * Test that title is not escaped (but XSS-filtered) for details form element.
507    */
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'));
514
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'));
518
519     // Create a node.
520     $nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en');
521
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);
527   }
528
529 }