Backup of db before drupal security update
[yaffs-website] / web / core / modules / content_translation / src / Tests / ContentTranslationUITestBase.php
1 <?php
2
3 namespace Drupal\content_translation\Tests;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Entity\EntityChangedInterface;
7 use Drupal\Core\Entity\EntityInterface;
8 use Drupal\Core\Language\Language;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Url;
11 use Drupal\language\Entity\ConfigurableLanguage;
12 use Drupal\Component\Utility\SafeMarkup;
13 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
14
15 /**
16  * Tests the Content Translation UI.
17  *
18  * @deprecated Scheduled for removal in Drupal 9.0.0.
19  *   Use \Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase instead.
20  */
21 abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
22
23   use AssertPageCacheContextsAndTagsTrait;
24
25   /**
26    * The id of the entity being translated.
27    *
28    * @var mixed
29    */
30   protected $entityId;
31
32   /**
33    * Whether the behavior of the language selector should be tested.
34    *
35    * @var bool
36    */
37   protected $testLanguageSelector = TRUE;
38
39   /**
40    * Flag that tells whether the HTML escaping of all languages works or not
41    * after SafeMarkup change.
42    *
43    * @var bool
44    */
45   protected $testHTMLEscapeForAllLanguages = FALSE;
46
47   /**
48    * Default cache contexts expected on a non-translated entity.
49    *
50    * Cache contexts will not be checked if this list is empty.
51    *
52    * @var string[]
53    */
54   protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions'];
55
56   /**
57    * Tests the basic translation UI.
58    */
59   public function testTranslationUI() {
60     $this->doTestBasicTranslation();
61     $this->doTestTranslationOverview();
62     $this->doTestOutdatedStatus();
63     $this->doTestPublishedStatus();
64     $this->doTestAuthoringInfo();
65     $this->doTestTranslationEdit();
66     $this->doTestTranslationChanged();
67     $this->doTestChangedTimeAfterSaveWithoutChanges();
68     $this->doTestTranslationDeletion();
69   }
70
71   /**
72    * Tests the basic translation workflow.
73    */
74   protected function doTestBasicTranslation() {
75     // Create a new test entity with original values in the default language.
76     $default_langcode = $this->langcodes[0];
77     $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
78     // Create the entity with the editor as owner, so that afterwards a new
79     // translation is created by the translator and the translation author is
80     // tested.
81     $this->drupalLogin($this->editor);
82     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
83     $this->drupalLogin($this->translator);
84     $storage = $this->container->get('entity_type.manager')
85       ->getStorage($this->entityTypeId);
86     $storage->resetCache([$this->entityId]);
87     $entity = $storage->load($this->entityId);
88     $this->assertTrue($entity, 'Entity found in the database.');
89     $this->drupalGet($entity->urlInfo());
90     $this->assertResponse(200, 'Entity URL is valid.');
91
92     // Ensure that the content language cache context is not yet added to the
93     // page.
94     $this->assertCacheContexts($this->defaultCacheContexts);
95
96     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
97     $this->assertNoText('Source language', 'Source language column correctly hidden.');
98
99     $translation = $this->getTranslation($entity, $default_langcode);
100     foreach ($values[$default_langcode] as $property => $value) {
101       $stored_value = $this->getValue($translation, $property, $default_langcode);
102       $value = is_array($value) ? $value[0]['value'] : $value;
103       $message = format_string('@property correctly stored in the default language.', ['@property' => $property]);
104       $this->assertEqual($stored_value, $value, $message);
105     }
106
107     // Add a content translation.
108     $langcode = 'it';
109     $language = ConfigurableLanguage::load($langcode);
110     $values[$langcode] = $this->getNewEntityValues($langcode);
111
112     $entity_type_id = $entity->getEntityTypeId();
113     $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
114       $entity->getEntityTypeId() => $entity->id(),
115       'source' => $default_langcode,
116       'target' => $langcode
117     ], ['language' => $language]);
118     $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
119
120     // Assert that HTML is escaped in "all languages" in UI after SafeMarkup
121     // change.
122     if ($this->testHTMLEscapeForAllLanguages) {
123       $this->assertNoRaw('&lt;span class=&quot;translation-entity-all-languages&quot;&gt;(all languages)&lt;/span&gt;');
124       $this->assertRaw('<span class="translation-entity-all-languages">(all languages)</span>');
125     }
126
127     // Ensure that the content language cache context is not yet added to the
128     // page.
129     $storage = $this->container->get('entity_type.manager')
130       ->getStorage($this->entityTypeId);
131     $storage->resetCache([$this->entityId]);
132     $entity = $storage->load($this->entityId);
133     $this->drupalGet($entity->urlInfo());
134     $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts));
135
136     // Reset the cache of the entity, so that the new translation gets the
137     // updated values.
138     $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
139     $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
140
141     $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
142     if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
143       $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(),
144         SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode]));
145
146       $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
147         SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
148           ['@target' => $langcode, '@source' => $default_langcode]));
149     }
150     else {
151       $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), 'Author of the entity remained untouched after translation for non translatable owner field.');
152     }
153
154     $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
155     if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
156       $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(),
157         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.',
158           ['@target' => $langcode, '@source' => $default_langcode]));
159     }
160     else {
161       $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.');
162     }
163
164     if ($this->testLanguageSelector) {
165       $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.');
166     }
167     $storage->resetCache([$this->entityId]);
168     $entity = $storage->load($this->entityId);
169     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
170     $this->assertNoText('Source language', 'Source language column correctly hidden.');
171
172     // Switch the source language.
173     $langcode = 'fr';
174     $language = ConfigurableLanguage::load($langcode);
175     $source_langcode = 'it';
176     $edit = ['source_langcode[source]' => $source_langcode];
177     $entity_type_id = $entity->getEntityTypeId();
178     $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
179       $entity->getEntityTypeId() => $entity->id(),
180       'source' => $default_langcode,
181       'target' => $langcode
182     ], ['language' => $language]);
183     // This does not save anything, it merely reloads the form and fills in the
184     // fields with the values from the different source language.
185     $this->drupalPostForm($add_url, $edit, t('Change'));
186     $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.');
187
188     // Add another translation and mark the other ones as outdated.
189     $values[$langcode] = $this->getNewEntityValues($langcode);
190     $edit = $this->getEditValues($values, $langcode) + ['content_translation[retranslate]' => TRUE];
191     $entity_type_id = $entity->getEntityTypeId();
192     $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
193       $entity->getEntityTypeId() => $entity->id(),
194       'source' => $source_langcode,
195       'target' => $langcode
196     ], ['language' => $language]);
197     $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
198     $storage->resetCache([$this->entityId]);
199     $entity = $storage->load($this->entityId);
200     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
201     $this->assertText('Source language', 'Source language column correctly shown.');
202
203     // Check that the entered values have been correctly stored.
204     foreach ($values as $langcode => $property_values) {
205       $translation = $this->getTranslation($entity, $langcode);
206       foreach ($property_values as $property => $value) {
207         $stored_value = $this->getValue($translation, $property, $langcode);
208         $value = is_array($value) ? $value[0]['value'] : $value;
209         $message = format_string('%property correctly stored with language %language.', ['%property' => $property, '%language' => $langcode]);
210         $this->assertEqual($stored_value, $value, $message);
211       }
212     }
213   }
214
215   /**
216    * Tests that the translation overview shows the correct values.
217    */
218   protected function doTestTranslationOverview() {
219     $storage = $this->container->get('entity_type.manager')
220       ->getStorage($this->entityTypeId);
221     $storage->resetCache([$this->entityId]);
222     $entity = $storage->load($this->entityId);
223     $translate_url = $entity->urlInfo('drupal:content-translation-overview');
224     $this->drupalGet($translate_url);
225     $translate_url->setAbsolute(FALSE);
226
227     foreach ($this->langcodes as $langcode) {
228       if ($entity->hasTranslation($langcode)) {
229         $language = new Language(['id' => $langcode]);
230         $view_url = $entity->url('canonical', ['language' => $language]);
231         $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]);
232         $this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', ['%language' => $langcode]));
233         $edit_path = $entity->url('edit-form', ['language' => $language]);
234         $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', [':href' => $edit_path]);
235         $this->assertEqual((string) $elements[0], t('Edit'), format_string('Edit link correct for %language translation.', ['%language' => $langcode]));
236       }
237     }
238   }
239
240   /**
241    * Tests up-to-date status tracking.
242    */
243   protected function doTestOutdatedStatus() {
244     $storage = $this->container->get('entity_type.manager')
245       ->getStorage($this->entityTypeId);
246     $storage->resetCache([$this->entityId]);
247     $entity = $storage->load($this->entityId);
248     $langcode = 'fr';
249     $languages = \Drupal::languageManager()->getLanguages();
250
251     // Mark translations as outdated.
252     $edit = ['content_translation[retranslate]' => TRUE];
253     $edit_path = $entity->urlInfo('edit-form', ['language' => $languages[$langcode]]);
254     $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
255     $storage->resetCache([$this->entityId]);
256     $entity = $storage->load($this->entityId);
257
258     // Check that every translation has the correct "outdated" status, and that
259     // the Translation fieldset is open if the translation is "outdated".
260     foreach ($this->langcodes as $added_langcode) {
261       $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($added_langcode)]);
262       $this->drupalGet($url);
263       if ($added_langcode == $langcode) {
264         $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
265         $this->assertFalse($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab should be collapsed by default.');
266       }
267       else {
268         $this->assertFieldByXPath('//input[@name="content_translation[outdated]"]', TRUE, 'The translate flag is checked by default.');
269         $this->assertTrue($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab is correctly expanded when the translation is outdated.');
270         $edit = ['content_translation[outdated]' => FALSE];
271         $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $added_langcode));
272         $this->drupalGet($url);
273         $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
274         $storage = $this->container->get('entity_type.manager')
275           ->getStorage($this->entityTypeId);
276         $storage->resetCache([$this->entityId]);
277         $entity = $storage->load($this->entityId);
278         $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.');
279       }
280     }
281   }
282
283   /**
284    * Tests the translation publishing status.
285    */
286   protected function doTestPublishedStatus() {
287     $storage = $this->container->get('entity_type.manager')
288       ->getStorage($this->entityTypeId);
289     $storage->resetCache([$this->entityId]);
290     $entity = $storage->load($this->entityId);
291
292     // Unpublish translations.
293     foreach ($this->langcodes as $index => $langcode) {
294       if ($index > 0) {
295         $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
296         $edit = ['content_translation[status]' => FALSE];
297         $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
298         $storage = $this->container->get('entity_type.manager')
299           ->getStorage($this->entityTypeId);
300         $storage->resetCache([$this->entityId]);
301         $entity = $storage->load($this->entityId);
302         $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.');
303       }
304     }
305
306     // Check that the last published translation cannot be unpublished.
307     $this->drupalGet($entity->urlInfo('edit-form'));
308     $this->assertFieldByXPath('//input[@name="content_translation[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.');
309   }
310
311   /**
312    * Tests the translation authoring information.
313    */
314   protected function doTestAuthoringInfo() {
315     $storage = $this->container->get('entity_type.manager')
316       ->getStorage($this->entityTypeId);
317     $storage->resetCache([$this->entityId]);
318     $entity = $storage->load($this->entityId);
319     $values = [];
320
321     // Post different authoring information for each translation.
322     foreach ($this->langcodes as $index => $langcode) {
323       $user = $this->drupalCreateUser();
324       $values[$langcode] = [
325         'uid' => $user->id(),
326         'created' => REQUEST_TIME - mt_rand(0, 1000),
327       ];
328       $edit = [
329         'content_translation[uid]' => $user->getUsername(),
330         'content_translation[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
331       ];
332       $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
333       $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
334     }
335
336     $storage = $this->container->get('entity_type.manager')
337       ->getStorage($this->entityTypeId);
338     $storage->resetCache([$this->entityId]);
339     $entity = $storage->load($this->entityId);
340     foreach ($this->langcodes as $langcode) {
341       $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
342       $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
343       $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
344     }
345
346     // Try to post non valid values and check that they are rejected.
347     $langcode = 'en';
348     $edit = [
349       // User names have by default length 8.
350       'content_translation[uid]' => $this->randomMachineName(12),
351       'content_translation[created]' => '19/11/1978',
352     ];
353     $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode));
354     $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.');
355     $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
356     $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.');
357     $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.');
358   }
359
360   /**
361    * Tests translation deletion.
362    */
363   protected function doTestTranslationDeletion() {
364     // Confirm and delete a translation.
365     $this->drupalLogin($this->translator);
366     $langcode = 'fr';
367     $storage = $this->container->get('entity_type.manager')
368       ->getStorage($this->entityTypeId);
369     $storage->resetCache([$this->entityId]);
370     $entity = $storage->load($this->entityId);
371     $language = ConfigurableLanguage::load($langcode);
372     $url = $entity->urlInfo('edit-form', ['language' => $language]);
373     $this->drupalPostForm($url, [], t('Delete translation'));
374     $this->drupalPostForm(NULL, [], t('Delete @language translation', ['@language' => $language->getName()]));
375     $storage->resetCache([$this->entityId]);
376     $entity = $storage->load($this->entityId, TRUE);
377     if ($this->assertTrue(is_object($entity), 'Entity found')) {
378       $translations = $entity->getTranslationLanguages();
379       $this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.');
380     }
381
382     // Check that the translator cannot delete the original translation.
383     $args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
384     $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
385     $this->assertResponse(403);
386   }
387
388   /**
389    * Returns an array of entity field values to be tested.
390    */
391   protected function getNewEntityValues($langcode) {
392     return [$this->fieldName => [['value' => $this->randomMachineName(16)]]];
393   }
394
395   /**
396    * Returns an edit array containing the values to be posted.
397    */
398   protected function getEditValues($values, $langcode, $new = FALSE) {
399     $edit = $values[$langcode];
400     $langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode;
401     foreach ($values[$langcode] as $property => $value) {
402       if (is_array($value)) {
403         $edit["{$property}[0][value]"] = $value[0]['value'];
404         unset($edit[$property]);
405       }
406     }
407     return $edit;
408   }
409
410   /**
411    * Returns the form action value when submitting a new translation.
412    *
413    * @param \Drupal\Core\Entity\EntityInterface $entity
414    *   The entity being tested.
415    * @param string $langcode
416    *   Language code for the form.
417    *
418    * @return string
419    *   Name of the button to hit.
420    */
421   protected function getFormSubmitActionForNewTranslation(EntityInterface $entity, $langcode) {
422     $entity->addTranslation($langcode, $entity->toArray());
423     return $this->getFormSubmitAction($entity, $langcode);
424   }
425
426   /**
427    * Returns the form action value to be used to submit the entity form.
428    *
429    * @param \Drupal\Core\Entity\EntityInterface $entity
430    *   The entity being tested.
431    * @param string $langcode
432    *   Language code for the form.
433    *
434    * @return string
435    *   Name of the button to hit.
436    */
437   protected function getFormSubmitAction(EntityInterface $entity, $langcode) {
438     return t('Save') . $this->getFormSubmitSuffix($entity, $langcode);
439   }
440
441   /**
442    * Returns appropriate submit button suffix based on translatability.
443    *
444    * @param \Drupal\Core\Entity\EntityInterface $entity
445    *   The entity being tested.
446    * @param string $langcode
447    *   Language code for the form.
448    *
449    * @return string
450    *   Submit button suffix based on translatability.
451    */
452   protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
453     return '';
454   }
455
456   /**
457    * Returns the translation object to use to retrieve the translated values.
458    *
459    * @param \Drupal\Core\Entity\EntityInterface $entity
460    *   The entity being tested.
461    * @param string $langcode
462    *   The language code identifying the translation to be retrieved.
463    *
464    * @return \Drupal\Core\TypedData\TranslatableInterface
465    *   The translation object to act on.
466    */
467   protected function getTranslation(EntityInterface $entity, $langcode) {
468     return $entity->getTranslation($langcode);
469   }
470
471   /**
472    * Returns the value for the specified property in the given language.
473    *
474    * @param \Drupal\Core\Entity\EntityInterface $translation
475    *   The translation object the property value should be retrieved from.
476    * @param string $property
477    *   The property name.
478    * @param string $langcode
479    *   The property value.
480    *
481    * @return
482    *   The property value.
483    */
484   protected function getValue(EntityInterface $translation, $property, $langcode) {
485     $key = $property == 'user_id' ? 'target_id' : 'value';
486     return $translation->get($property)->{$key};
487   }
488
489   /**
490    * Returns the name of the field that implements the changed timestamp.
491    *
492    * @param \Drupal\Core\Entity\EntityInterface $entity
493    *   The entity being tested.
494    *
495    * @return string
496    *   The field name.
497    */
498   protected function getChangedFieldName($entity) {
499     return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
500   }
501
502   /**
503    * Tests edit content translation.
504    */
505   protected function doTestTranslationEdit() {
506     $storage = $this->container->get('entity_type.manager')
507       ->getStorage($this->entityTypeId);
508     $storage->resetCache([$this->entityId]);
509     $entity = $storage->load($this->entityId);
510     $languages = $this->container->get('language_manager')->getLanguages();
511
512     foreach ($this->langcodes as $langcode) {
513       // We only want to test the title for non-english translations.
514       if ($langcode != 'en') {
515         $options = ['language' => $languages[$langcode]];
516         $url = $entity->urlInfo('edit-form', $options);
517         $this->drupalGet($url);
518
519         $this->assertRaw($entity->getTranslation($langcode)->label());
520       }
521     }
522   }
523
524   /**
525    * Tests the basic translation workflow.
526    */
527   protected function doTestTranslationChanged() {
528     $storage = $this->container->get('entity_type.manager')
529       ->getStorage($this->entityTypeId);
530     $storage->resetCache([$this->entityId]);
531     $entity = $storage->load($this->entityId);
532     $changed_field_name = $this->getChangedFieldName($entity);
533     $definition = $entity->getFieldDefinition($changed_field_name);
534     $config = $definition->getConfig($entity->bundle());
535
536     foreach ([FALSE, TRUE] as $translatable_changed_field) {
537       if ($definition->isTranslatable()) {
538         // For entities defining a translatable changed field we want to test
539         // the correct behavior of that field even if the translatability is
540         // revoked. In that case the changed timestamp should be synchronized
541         // across all translations.
542         $config->setTranslatable($translatable_changed_field);
543         $config->save();
544       }
545       elseif ($translatable_changed_field) {
546         // For entities defining a non-translatable changed field we cannot
547         // declare the field as translatable on the fly by modifying its config
548         // because the schema doesn't support this.
549         break;
550       }
551
552       foreach ($entity->getTranslationLanguages() as $language) {
553         // Ensure different timestamps.
554         sleep(1);
555
556         $langcode = $language->getId();
557
558         $edit = [
559           $this->fieldName . '[0][value]' => $this->randomString(),
560         ];
561         $edit_path = $entity->urlInfo('edit-form', ['language' => $language]);
562         $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
563
564         $storage = $this->container->get('entity_type.manager')
565           ->getStorage($this->entityTypeId);
566         $storage->resetCache([$this->entityId]);
567         $entity = $storage->load($this->entityId);
568         $this->assertEqual(
569           $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
570           format_string('Changed time for language %language is the latest change over all languages.', ['%language' => $language->getName()])
571         );
572       }
573
574       $timestamps = [];
575       foreach ($entity->getTranslationLanguages() as $language) {
576         $next_timestamp = $entity->getTranslation($language->getId())->getChangedTime();
577         if (!in_array($next_timestamp, $timestamps)) {
578           $timestamps[] = $next_timestamp;
579         }
580       }
581
582       if ($translatable_changed_field) {
583         $this->assertEqual(
584           count($timestamps), count($entity->getTranslationLanguages()),
585           'All timestamps from all languages are different.'
586         );
587       }
588       else {
589         $this->assertEqual(
590           count($timestamps), 1,
591           'All timestamps from all languages are identical.'
592         );
593       }
594     }
595   }
596
597   /**
598    * Test the changed time after API and FORM save without changes.
599    */
600   public function doTestChangedTimeAfterSaveWithoutChanges() {
601     $storage = $this->container->get('entity_type.manager')
602       ->getStorage($this->entityTypeId);
603     $storage->resetCache([$this->entityId]);
604     $entity = $storage->load($this->entityId);
605     // Test only entities, which implement the EntityChangedInterface.
606     if ($entity instanceof EntityChangedInterface) {
607       $changed_timestamp = $entity->getChangedTime();
608
609       $entity->save();
610       $storage = $this->container->get('entity_type.manager')
611         ->getStorage($this->entityTypeId);
612       $storage->resetCache([$this->entityId]);
613       $entity = $storage->load($this->entityId);
614       $this->assertEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.');
615
616       // Ensure different save timestamps.
617       sleep(1);
618
619       // Save the entity on the regular edit form.
620       $language = $entity->language();
621       $edit_path = $entity->urlInfo('edit-form', ['language' => $language]);
622       $this->drupalPostForm($edit_path, [], $this->getFormSubmitAction($entity, $language->getId()));
623
624       $storage->resetCache([$this->entityId]);
625       $entity = $storage->load($this->entityId);
626       $this->assertNotEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.');
627     }
628   }
629
630 }