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