Version 1
[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     'session',
25     'theme',
26     'route',
27     'timezone',
28     'url.path.parent',
29     'url.query_args:_wrapper_format',
30     'user'
31   ];
32
33   /**
34    * Modules to enable.
35    *
36    * @var array
37    */
38   public static $modules = ['block', 'language', 'content_translation', 'node', 'datetime', 'field_ui', 'help'];
39
40   /**
41    * The profile to install as a basis for testing.
42    *
43    * @var string
44    */
45   protected $profile = 'standard';
46
47   protected function setUp() {
48     $this->entityTypeId = 'node';
49     $this->bundle = 'article';
50     parent::setUp();
51
52     // Ensure the help message is shown even with prefixed paths.
53     $this->drupalPlaceBlock('help_block', ['region' => 'content']);
54
55     // Display the language selector.
56     $this->drupalLogin($this->administrator);
57     $edit = ['language_configuration[language_alterable]' => TRUE];
58     $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type'));
59     $this->drupalLogin($this->translator);
60   }
61
62   /**
63    * Tests the basic translation UI.
64    */
65   public function testTranslationUI() {
66     parent::testTranslationUI();
67     $this->doUninstallTest();
68   }
69
70   /**
71    * Tests changing the published status on a node without fields.
72    */
73   public function testPublishedStatusNoFields() {
74     // Test changing the published status of an article without fields.
75     $this->drupalLogin($this->administrator);
76     // Delete all fields.
77     $this->drupalGet('admin/structure/types/manage/article/fields');
78     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete', [], t('Delete'));
79     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags/delete', [], t('Delete'));
80     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/delete', [], t('Delete'));
81
82     // Add a node.
83     $default_langcode = $this->langcodes[0];
84     $values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]];
85     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
86     $storage = $this->container->get('entity_type.manager')
87       ->getStorage($this->entityTypeId);
88     $storage->resetCache([$this->entityId]);
89     $entity = $storage->load($this->entityId);
90
91     // Add a content translation.
92     $langcode = 'fr';
93     $language = ConfigurableLanguage::load($langcode);
94     $values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
95
96     $entity_type_id = $entity->getEntityTypeId();
97     $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
98       $entity->getEntityTypeId() => $entity->id(),
99       'source' => $default_langcode,
100       'target' => $langcode
101     ], ['language' => $language]);
102     $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), t('Save and unpublish (this translation)'));
103
104     $storage->resetCache([$this->entityId]);
105     $entity = $storage->load($this->entityId);
106     $translation = $entity->getTranslation($langcode);
107     // Make sure we unpublished the node correctly.
108     $this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
109   }
110
111   /**
112    * {@inheritdoc}
113    */
114   protected function getTranslatorPermissions() {
115     return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
116   }
117
118   /**
119    * {@inheritdoc}
120    */
121   protected function getEditorPermissions() {
122     return ['administer nodes', 'create article content'];
123   }
124
125   /**
126    * {@inheritdoc}
127    */
128   protected function getAdministratorPermissions() {
129     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']);
130   }
131
132   /**
133    * {@inheritdoc}
134    */
135   protected function getNewEntityValues($langcode) {
136     return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
137   }
138
139   /**
140    * {@inheritdoc}
141    */
142   protected function getFormSubmitAction(EntityInterface $entity, $langcode) {
143     if ($entity->getTranslation($langcode)->isPublished()) {
144       return t('Save and keep published') . $this->getFormSubmitSuffix($entity, $langcode);
145     }
146     else {
147       return t('Save and keep unpublished') . $this->getFormSubmitSuffix($entity, $langcode);
148     }
149   }
150
151   /**
152    * {@inheritdoc}
153    */
154   protected function doTestPublishedStatus() {
155     $storage = $this->container->get('entity_type.manager')
156       ->getStorage($this->entityTypeId);
157     $storage->resetCache([$this->entityId]);
158     $entity = $storage->load($this->entityId);
159     $languages = $this->container->get('language_manager')->getLanguages();
160
161     $actions = [
162       t('Save and keep published'),
163       t('Save and unpublish'),
164     ];
165
166     foreach ($actions as $index => $action) {
167       // (Un)publish the node translations and check that the translation
168       // statuses are (un)published accordingly.
169       foreach ($this->langcodes as $langcode) {
170         $options = ['language' => $languages[$langcode]];
171         $url = $entity->urlInfo('edit-form', $options);
172         $this->drupalPostForm($url, [], $action . $this->getFormSubmitSuffix($entity, $langcode), $options);
173       }
174       $storage->resetCache([$this->entityId]);
175       $entity = $storage->load($this->entityId);
176       foreach ($this->langcodes as $langcode) {
177         // The node is created as unpublished thus we switch to the published
178         // status first.
179         $status = !$index;
180         $translation = $entity->getTranslation($langcode);
181         $this->assertEqual($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
182       }
183     }
184   }
185
186   /**
187    * {@inheritdoc}
188    */
189   protected function doTestAuthoringInfo() {
190     $storage = $this->container->get('entity_type.manager')
191       ->getStorage($this->entityTypeId);
192     $storage->resetCache([$this->entityId]);
193     $entity = $storage->load($this->entityId);
194     $languages = $this->container->get('language_manager')->getLanguages();
195     $values = [];
196
197     // Post different base field information for each translation.
198     foreach ($this->langcodes as $langcode) {
199       $user = $this->drupalCreateUser();
200       $values[$langcode] = [
201         'uid' => $user->id(),
202         'created' => REQUEST_TIME - mt_rand(0, 1000),
203         'sticky' => (bool) mt_rand(0, 1),
204         'promote' => (bool) mt_rand(0, 1),
205       ];
206       $edit = [
207         'uid[0][target_id]' => $user->getUsername(),
208         'created[0][value][date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'),
209         'created[0][value][time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'),
210         'sticky[value]' => $values[$langcode]['sticky'],
211         'promote[value]' => $values[$langcode]['promote'],
212       ];
213       $options = ['language' => $languages[$langcode]];
214       $url = $entity->urlInfo('edit-form', $options);
215       $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode), $options);
216     }
217
218     $storage->resetCache([$this->entityId]);
219     $entity = $storage->load($this->entityId);
220     foreach ($this->langcodes as $langcode) {
221       $translation = $entity->getTranslation($langcode);
222       $metadata = $this->manager->getTranslationMetadata($translation);
223       $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
224       $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
225       $this->assertEqual($translation->isSticky(), $values[$langcode]['sticky'], 'Sticky of Translation correctly stored.');
226       $this->assertEqual($translation->isPromoted(), $values[$langcode]['promote'], 'Promoted of Translation correctly stored.');
227     }
228   }
229
230   /**
231    * Tests that translation page inherits admin status of edit page.
232    */
233   public function testTranslationLinkTheme() {
234     $this->drupalLogin($this->administrator);
235     $article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
236
237     // Set up Seven as the admin theme and use it for node editing.
238     $this->container->get('theme_handler')->install(['seven']);
239     $edit = [];
240     $edit['admin_theme'] = 'seven';
241     $edit['use_admin_theme'] = TRUE;
242     $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
243     $this->drupalGet('node/' . $article->id() . '/translations');
244     $this->assertRaw('core/themes/seven/css/base/elements.css', 'Translation uses admin theme if edit is admin.');
245
246     // Turn off admin theme for editing, assert inheritance to translations.
247     $edit['use_admin_theme'] = FALSE;
248     $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
249     $this->drupalGet('node/' . $article->id() . '/translations');
250     $this->assertNoRaw('core/themes/seven/css/base/elements.css', 'Translation uses frontend theme if edit is frontend.');
251
252     // Assert presence of translation page itself (vs. DisabledBundle below).
253     $this->assertResponse(200);
254   }
255
256   /**
257    * Tests that no metadata is stored for a disabled bundle.
258    */
259   public function testDisabledBundle() {
260     // Create a bundle that does not have translation enabled.
261     $disabledBundle = $this->randomMachineName();
262     $this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]);
263
264     // Create a node for each bundle.
265     $node = $this->drupalCreateNode([
266       'type' => $this->bundle,
267       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
268     ]);
269
270     // Make sure that nothing was inserted into the {content_translation} table.
271     $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();
272     $this->assertEqual(0, count($rows));
273
274     // Ensure the translation tab is not accessible.
275     $this->drupalGet('node/' . $node->id() . '/translations');
276     $this->assertResponse(403);
277   }
278
279   /**
280    * Tests that translations are rendered properly.
281    */
282   public function testTranslationRendering() {
283     $default_langcode = $this->langcodes[0];
284     $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
285     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
286     $node = \Drupal::entityManager()->getStorage($this->entityTypeId)->load($this->entityId);
287     $node->setPromoted(TRUE);
288
289     // Create translations.
290     foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) {
291       $values[$langcode] = $this->getNewEntityValues($langcode);
292       $translation = $node->addTranslation($langcode, $values[$langcode]);
293       // Publish and promote the translation to frontpage.
294       $translation->setPromoted(TRUE);
295       $translation->setPublished(TRUE);
296     }
297     $node->save();
298
299     // Test that the frontpage view displays the correct translations.
300     \Drupal::service('module_installer')->install(['views'], TRUE);
301     $this->rebuildContainer();
302     $this->doTestTranslations('node', $values);
303
304     // Enable the translation language renderer.
305     $view = \Drupal::entityManager()->getStorage('view')->load('frontpage');
306     $display = &$view->getDisplay('default');
307     $display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***';
308     $view->save();
309
310     // Need to check from the beginning, including the base_path, in the url
311     // since the pattern for the default language might be a substring of
312     // the strings for other languages.
313     $base_path = base_path();
314
315     // Check the frontpage for 'Read more' links to each translation.
316     // See also assertTaxonomyPage() in NodeAccessBaseTableTest.
317     $node_href = 'node/' . $node->id();
318     foreach ($this->langcodes as $langcode) {
319       $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
320       $num_match_found = 0;
321       if ($langcode == 'en') {
322         // Site default language does not have langcode prefix in the URL.
323         $expected_href = $base_path . $node_href;
324       }
325       else {
326         $expected_href = $base_path . $langcode . '/' . $node_href;
327       }
328       $pattern = '|^' . $expected_href . '$|';
329       foreach ($this->xpath("//a[text()='Read more']") as $link) {
330         if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
331           $num_match_found++;
332         }
333       }
334       $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 . '.)');
335     }
336
337     // Check the frontpage for 'Add new comment' links that include the
338     // language.
339     $comment_form_href = 'node/' . $node->id() . '#comment-form';
340     foreach ($this->langcodes as $langcode) {
341       $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
342       $num_match_found = 0;
343       if ($langcode == 'en') {
344         // Site default language does not have langcode prefix in the URL.
345         $expected_href = $base_path . $comment_form_href;
346       }
347       else {
348         $expected_href = $base_path . $langcode . '/' . $comment_form_href;
349       }
350       $pattern = '|^' . $expected_href . '$|';
351       foreach ($this->xpath("//a[text()='Add new comment']") as $link) {
352         if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
353           $num_match_found++;
354         }
355       }
356       $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 . '.)');
357     }
358
359     // Test that the node page displays the correct translations.
360     $this->doTestTranslations('node/' . $node->id(), $values);
361
362     // Test that the node page has the correct alternate hreflang links.
363     $this->doTestAlternateHreflangLinks($node->urlInfo());
364   }
365
366   /**
367    * Tests that the given path displays the correct translation values.
368    *
369    * @param string $path
370    *   The path to be tested.
371    * @param array $values
372    *   The translation values to be found.
373    */
374   protected function doTestTranslations($path, array $values) {
375     $languages = $this->container->get('language_manager')->getLanguages();
376     foreach ($this->langcodes as $langcode) {
377       $this->drupalGet($path, ['language' => $languages[$langcode]]);
378       $this->assertText($values[$langcode]['title'][0]['value'], format_string('The %langcode node translation is correctly displayed.', ['%langcode' => $langcode]));
379     }
380   }
381
382   /**
383    * Tests that the given path provides the correct alternate hreflang links.
384    *
385    * @param \Drupal\Core\Url $url
386    *   The path to be tested.
387    */
388   protected function doTestAlternateHreflangLinks(Url $url) {
389     $languages = $this->container->get('language_manager')->getLanguages();
390     $url->setAbsolute();
391     $urls = [];
392     foreach ($this->langcodes as $langcode) {
393       $language_url = clone $url;
394       $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
395     }
396     foreach ($this->langcodes as $langcode) {
397       $this->drupalGet($urls[$langcode]);
398       foreach ($urls as $alternate_langcode => $language_url) {
399         // Retrieve desired link elements from the HTML head.
400         $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]',
401           [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]);
402         $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       }
404     }
405   }
406
407   /**
408    * {@inheritdoc}
409    */
410   protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
411     if (!$entity->isNew() && $entity->isTranslatable()) {
412       $translations = $entity->getTranslationLanguages();
413       if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) {
414         return ' ' . ($field->isTranslatable() ? t('(this translation)') : t('(all translations)'));
415       }
416     }
417     return '';
418   }
419
420   /**
421    * Tests uninstalling content_translation.
422    */
423   protected function doUninstallTest() {
424     // Delete all the nodes so there is no data.
425     $nodes = Node::loadMultiple();
426     foreach ($nodes as $node) {
427       $node->delete();
428     }
429     $language_count = count(\Drupal::configFactory()->listAll('language.content_settings.'));
430     \Drupal::service('module_installer')->uninstall(['content_translation']);
431     $this->rebuildContainer();
432     $this->assertEqual($language_count, count(\Drupal::configFactory()->listAll('language.content_settings.')), 'Languages have been fixed rather than deleted during content_translation uninstall.');
433   }
434
435   /**
436    * {@inheritdoc}
437    */
438   protected function doTestTranslationEdit() {
439     $storage = $this->container->get('entity_type.manager')
440       ->getStorage($this->entityTypeId);
441     $storage->resetCache([$this->entityId]);
442     $entity = $storage->load($this->entityId);
443     $languages = $this->container->get('language_manager')->getLanguages();
444     $type_name = node_get_type_label($entity);
445
446     foreach ($this->langcodes as $langcode) {
447       // We only want to test the title for non-english translations.
448       if ($langcode != 'en') {
449         $options = ['language' => $languages[$langcode]];
450         $url = $entity->urlInfo('edit-form', $options);
451         $this->drupalGet($url);
452
453         $title = t('<em>Edit @type</em> @title [%language translation]', [
454           '@type' => $type_name,
455           '@title' => $entity->getTranslation($langcode)->label(),
456           '%language' => $languages[$langcode]->getName(),
457         ]);
458         $this->assertRaw($title);
459       }
460     }
461   }
462
463   /**
464    * Tests that revision translations are rendered properly.
465    */
466   public function testRevisionTranslationRendering() {
467     $storage = \Drupal::entityTypeManager()->getStorage('node');
468
469     // Create a node.
470     $nid = $this->createEntity(['title' => 'First rev en title'], 'en');
471     $node = $storage->load($nid);
472     $original_revision_id = $node->getRevisionId();
473
474     // Add a French translation.
475     $translation = $node->addTranslation('fr');
476     $translation->title = 'First rev fr title';
477     $translation->setNewRevision(FALSE);
478     $translation->save();
479
480     // Create a new revision.
481     $node->title = 'Second rev en title';
482     $node->setNewRevision(TRUE);
483     $node->save();
484
485     // Get an English view of this revision.
486     $original_revision = $storage->loadRevision($original_revision_id);
487     $original_revision_url = $original_revision->toUrl('revision')->toString();
488
489     // Should be different from regular node URL.
490     $this->assertNotIdentical($original_revision_url, $original_revision->toUrl()->toString());
491     $this->drupalGet($original_revision_url);
492     $this->assertResponse(200);
493
494     // Contents should be in English, of correct revision.
495     $this->assertText('First rev en title');
496     $this->assertNoText('First rev fr title');
497
498     // Get a French view.
499     $url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
500
501     // Should have different URL from English.
502     $this->assertNotIdentical($url_fr, $original_revision->toUrl()->toString());
503     $this->assertNotIdentical($url_fr, $original_revision_url);
504     $this->drupalGet($url_fr);
505     $this->assertResponse(200);
506
507     // Contents should be in French, of correct revision.
508     $this->assertText('First rev fr title');
509     $this->assertNoText('First rev en title');
510   }
511
512 }