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