X-Git-Url: http://www.aleph1.co.uk/gitweb/?a=blobdiff_plain;f=web%2Fcore%2Fmodules%2Fcontent_translation%2Fsrc%2FTests%2FContentTranslationUITestBase.php;fp=web%2Fcore%2Fmodules%2Fcontent_translation%2Fsrc%2FTests%2FContentTranslationUITestBase.php;h=e7a7dfc9ddff08e88c4ae368433898457fa889d4;hb=a2bd1bf0c2c1f1a17d188f4dc0726a45494cefae;hp=0000000000000000000000000000000000000000;hpb=57c063afa3f66b07c4bbddc2d6129a96d90f0aad;p=yaffs-website diff --git a/web/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/web/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php new file mode 100644 index 000000000..e7a7dfc9d --- /dev/null +++ b/web/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php @@ -0,0 +1,630 @@ +doTestBasicTranslation(); + $this->doTestTranslationOverview(); + $this->doTestOutdatedStatus(); + $this->doTestPublishedStatus(); + $this->doTestAuthoringInfo(); + $this->doTestTranslationEdit(); + $this->doTestTranslationChanged(); + $this->doTestChangedTimeAfterSaveWithoutChanges(); + $this->doTestTranslationDeletion(); + } + + /** + * Tests the basic translation workflow. + */ + protected function doTestBasicTranslation() { + // Create a new test entity with original values in the default language. + $default_langcode = $this->langcodes[0]; + $values[$default_langcode] = $this->getNewEntityValues($default_langcode); + // Create the entity with the editor as owner, so that afterwards a new + // translation is created by the translator and the translation author is + // tested. + $this->drupalLogin($this->editor); + $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); + $this->drupalLogin($this->translator); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertTrue($entity, 'Entity found in the database.'); + $this->drupalGet($entity->urlInfo()); + $this->assertResponse(200, 'Entity URL is valid.'); + + // Ensure that the content language cache context is not yet added to the + // page. + $this->assertCacheContexts($this->defaultCacheContexts); + + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertNoText('Source language', 'Source language column correctly hidden.'); + + $translation = $this->getTranslation($entity, $default_langcode); + foreach ($values[$default_langcode] as $property => $value) { + $stored_value = $this->getValue($translation, $property, $default_langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('@property correctly stored in the default language.', ['@property' => $property]); + $this->assertEqual($stored_value, $value, $message); + } + + // Add a content translation. + $langcode = 'it'; + $language = ConfigurableLanguage::load($langcode); + $values[$langcode] = $this->getNewEntityValues($langcode); + + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $default_langcode, + 'target' => $langcode + ], ['language' => $language]); + $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode)); + + // Assert that HTML is escaped in "all languages" in UI after SafeMarkup + // change. + if ($this->testHTMLEscapeForAllLanguages) { + $this->assertNoRaw('<span class="translation-entity-all-languages">(all languages)</span>'); + $this->assertRaw('(all languages)'); + } + + // Ensure that the content language cache context is not yet added to the + // page. + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo()); + $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts)); + + // Reset the cache of the entity, so that the new translation gets the + // updated values. + $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode)); + $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + + $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid'; + if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) { + $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(), + SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode])); + + $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(), + SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.', + ['@target' => $langcode, '@source' => $default_langcode])); + } + else { + $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), 'Author of the entity remained untouched after translation for non translatable owner field.'); + } + + $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created'; + if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) { + $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(), + 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.', + ['@target' => $langcode, '@source' => $default_langcode])); + } + else { + $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.'); + } + + if ($this->testLanguageSelector) { + $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.'); + } + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertNoText('Source language', 'Source language column correctly hidden.'); + + // Switch the source language. + $langcode = 'fr'; + $language = ConfigurableLanguage::load($langcode); + $source_langcode = 'it'; + $edit = ['source_langcode[source]' => $source_langcode]; + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $default_langcode, + 'target' => $langcode + ], ['language' => $language]); + // This does not save anything, it merely reloads the form and fills in the + // fields with the values from the different source language. + $this->drupalPostForm($add_url, $edit, t('Change')); + $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.'); + + // Add another translation and mark the other ones as outdated. + $values[$langcode] = $this->getNewEntityValues($langcode); + $edit = $this->getEditValues($values, $langcode) + ['content_translation[retranslate]' => TRUE]; + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ + $entity->getEntityTypeId() => $entity->id(), + 'source' => $source_langcode, + 'target' => $langcode + ], ['language' => $language]); + $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode)); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $this->assertText('Source language', 'Source language column correctly shown.'); + + // Check that the entered values have been correctly stored. + foreach ($values as $langcode => $property_values) { + $translation = $this->getTranslation($entity, $langcode); + foreach ($property_values as $property => $value) { + $stored_value = $this->getValue($translation, $property, $langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('%property correctly stored with language %language.', ['%property' => $property, '%language' => $langcode]); + $this->assertEqual($stored_value, $value, $message); + } + } + } + + /** + * Tests that the translation overview shows the correct values. + */ + protected function doTestTranslationOverview() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $translate_url = $entity->urlInfo('drupal:content-translation-overview'); + $this->drupalGet($translate_url); + $translate_url->setAbsolute(FALSE); + + foreach ($this->langcodes as $langcode) { + if ($entity->hasTranslation($langcode)) { + $language = new Language(['id' => $langcode]); + $view_url = $entity->url('canonical', ['language' => $language]); + $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]); + $this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', ['%language' => $langcode])); + $edit_path = $entity->url('edit-form', ['language' => $language]); + $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', [':href' => $edit_path]); + $this->assertEqual((string) $elements[0], t('Edit'), format_string('Edit link correct for %language translation.', ['%language' => $langcode])); + } + } + } + + /** + * Tests up-to-date status tracking. + */ + protected function doTestOutdatedStatus() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $langcode = 'fr'; + $languages = \Drupal::languageManager()->getLanguages(); + + // Mark translations as outdated. + $edit = ['content_translation[retranslate]' => TRUE]; + $edit_path = $entity->urlInfo('edit-form', ['language' => $languages[$langcode]]); + $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode)); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + + // Check that every translation has the correct "outdated" status, and that + // the Translation fieldset is open if the translation is "outdated". + foreach ($this->langcodes as $added_langcode) { + $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($added_langcode)]); + $this->drupalGet($url); + if ($added_langcode == $langcode) { + $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.'); + $this->assertFalse($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab should be collapsed by default.'); + } + else { + $this->assertFieldByXPath('//input[@name="content_translation[outdated]"]', TRUE, 'The translate flag is checked by default.'); + $this->assertTrue($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab is correctly expanded when the translation is outdated.'); + $edit = ['content_translation[outdated]' => FALSE]; + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $added_langcode)); + $this->drupalGet($url); + $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.'); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.'); + } + } + } + + /** + * Tests the translation publishing status. + */ + protected function doTestPublishedStatus() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + + // Unpublish translations. + foreach ($this->langcodes as $index => $langcode) { + if ($index > 0) { + $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($langcode)]); + $edit = ['content_translation[status]' => FALSE]; + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode)); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.'); + } + } + + // Check that the last published translation cannot be unpublished. + $this->drupalGet($entity->urlInfo('edit-form')); + $this->assertFieldByXPath('//input[@name="content_translation[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.'); + } + + /** + * Tests the translation authoring information. + */ + protected function doTestAuthoringInfo() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $values = []; + + // Post different authoring information for each translation. + foreach ($this->langcodes as $index => $langcode) { + $user = $this->drupalCreateUser(); + $values[$langcode] = [ + 'uid' => $user->id(), + 'created' => REQUEST_TIME - mt_rand(0, 1000), + ]; + $edit = [ + 'content_translation[uid]' => $user->getUsername(), + 'content_translation[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'), + ]; + $url = $entity->urlInfo('edit-form', ['language' => ConfigurableLanguage::load($langcode)]); + $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode)); + } + + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + foreach ($this->langcodes as $langcode) { + $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.'); + $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.'); + } + + // Try to post non valid values and check that they are rejected. + $langcode = 'en'; + $edit = [ + // User names have by default length 8. + 'content_translation[uid]' => $this->randomMachineName(12), + 'content_translation[created]' => '19/11/1978', + ]; + $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode)); + $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.'); + $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); + $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.'); + $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.'); + } + + /** + * Tests translation deletion. + */ + protected function doTestTranslationDeletion() { + // Confirm and delete a translation. + $this->drupalLogin($this->translator); + $langcode = 'fr'; + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $language = ConfigurableLanguage::load($langcode); + $url = $entity->urlInfo('edit-form', ['language' => $language]); + $this->drupalPostForm($url, [], t('Delete translation')); + $this->drupalPostForm(NULL, [], t('Delete @language translation', ['@language' => $language->getName()])); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId, TRUE); + if ($this->assertTrue(is_object($entity), 'Entity found')) { + $translations = $entity->getTranslationLanguages(); + $this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.'); + } + + // Check that the translator cannot delete the original translation. + $args = [$this->entityTypeId => $entity->id(), 'language' => 'en']; + $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args)); + $this->assertResponse(403); + } + + /** + * Returns an array of entity field values to be tested. + */ + protected function getNewEntityValues($langcode) { + return [$this->fieldName => [['value' => $this->randomMachineName(16)]]]; + } + + /** + * Returns an edit array containing the values to be posted. + */ + protected function getEditValues($values, $langcode, $new = FALSE) { + $edit = $values[$langcode]; + $langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode; + foreach ($values[$langcode] as $property => $value) { + if (is_array($value)) { + $edit["{$property}[0][value]"] = $value[0]['value']; + unset($edit[$property]); + } + } + return $edit; + } + + /** + * Returns the form action value when submitting a new translation. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Name of the button to hit. + */ + protected function getFormSubmitActionForNewTranslation(EntityInterface $entity, $langcode) { + $entity->addTranslation($langcode, $entity->toArray()); + return $this->getFormSubmitAction($entity, $langcode); + } + + /** + * Returns the form action value to be used to submit the entity form. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Name of the button to hit. + */ + protected function getFormSubmitAction(EntityInterface $entity, $langcode) { + return t('Save') . $this->getFormSubmitSuffix($entity, $langcode); + } + + /** + * Returns appropriate submit button suffix based on translatability. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * Language code for the form. + * + * @return string + * Submit button suffix based on translatability. + */ + protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) { + return ''; + } + + /** + * Returns the translation object to use to retrieve the translated values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * The language code identifying the translation to be retrieved. + * + * @return \Drupal\Core\TypedData\TranslatableInterface + * The translation object to act on. + */ + protected function getTranslation(EntityInterface $entity, $langcode) { + return $entity->getTranslation($langcode); + } + + /** + * Returns the value for the specified property in the given language. + * + * @param \Drupal\Core\Entity\EntityInterface $translation + * The translation object the property value should be retrieved from. + * @param string $property + * The property name. + * @param string $langcode + * The property value. + * + * @return + * The property value. + */ + protected function getValue(EntityInterface $translation, $property, $langcode) { + $key = $property == 'user_id' ? 'target_id' : 'value'; + return $translation->get($property)->{$key}; + } + + /** + * Returns the name of the field that implements the changed timestamp. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested. + * + * @return string + * The field name. + */ + protected function getChangedFieldName($entity) { + return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed'; + } + + /** + * Tests edit content translation. + */ + protected function doTestTranslationEdit() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $languages = $this->container->get('language_manager')->getLanguages(); + + foreach ($this->langcodes as $langcode) { + // We only want to test the title for non-english translations. + if ($langcode != 'en') { + $options = ['language' => $languages[$langcode]]; + $url = $entity->urlInfo('edit-form', $options); + $this->drupalGet($url); + + $this->assertRaw($entity->getTranslation($langcode)->label()); + } + } + } + + /** + * Tests the basic translation workflow. + */ + protected function doTestTranslationChanged() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $changed_field_name = $this->getChangedFieldName($entity); + $definition = $entity->getFieldDefinition($changed_field_name); + $config = $definition->getConfig($entity->bundle()); + + foreach ([FALSE, TRUE] as $translatable_changed_field) { + if ($definition->isTranslatable()) { + // For entities defining a translatable changed field we want to test + // the correct behavior of that field even if the translatability is + // revoked. In that case the changed timestamp should be synchronized + // across all translations. + $config->setTranslatable($translatable_changed_field); + $config->save(); + } + elseif ($translatable_changed_field) { + // For entities defining a non-translatable changed field we cannot + // declare the field as translatable on the fly by modifying its config + // because the schema doesn't support this. + break; + } + + foreach ($entity->getTranslationLanguages() as $language) { + // Ensure different timestamps. + sleep(1); + + $langcode = $language->getId(); + + $edit = [ + $this->fieldName . '[0][value]' => $this->randomString(), + ]; + $edit_path = $entity->urlInfo('edit-form', ['language' => $language]); + $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode)); + + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertEqual( + $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(), + format_string('Changed time for language %language is the latest change over all languages.', ['%language' => $language->getName()]) + ); + } + + $timestamps = []; + foreach ($entity->getTranslationLanguages() as $language) { + $next_timestamp = $entity->getTranslation($language->getId())->getChangedTime(); + if (!in_array($next_timestamp, $timestamps)) { + $timestamps[] = $next_timestamp; + } + } + + if ($translatable_changed_field) { + $this->assertEqual( + count($timestamps), count($entity->getTranslationLanguages()), + 'All timestamps from all languages are different.' + ); + } + else { + $this->assertEqual( + count($timestamps), 1, + 'All timestamps from all languages are identical.' + ); + } + } + } + + /** + * Test the changed time after API and FORM save without changes. + */ + public function doTestChangedTimeAfterSaveWithoutChanges() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + // Test only entities, which implement the EntityChangedInterface. + if ($entity instanceof EntityChangedInterface) { + $changed_timestamp = $entity->getChangedTime(); + + $entity->save(); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.'); + + // Ensure different save timestamps. + sleep(1); + + // Save the entity on the regular edit form. + $language = $entity->language(); + $edit_path = $entity->urlInfo('edit-form', ['language' => $language]); + $this->drupalPostForm($edit_path, [], $this->getFormSubmitAction($entity, $language->getId())); + + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $this->assertNotEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.'); + } + } + +}