PageRenderTime 41ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

/web/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php

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