ContentTranslationUITestBase.php 20 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\content_translation\Tests\ContentTranslationUITestBase.
6 7
 */

8
namespace Drupal\content_translation\Tests;
9 10

use Drupal\Core\Entity\EntityInterface;
11
use Drupal\Core\Language\Language;
12
use Drupal\Core\Language\LanguageInterface;
13 14
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
15 16

/**
17
 * Tests the Content Translation UI.
18
 */
19
abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
20

21 22 23 24 25 26 27
  /**
   * The id of the entity being translated.
   *
   * @var mixed
   */
  protected $entityId;

28 29 30
  /**
   * Whether the behavior of the language selector should be tested.
   *
31
   * @var bool
32 33 34 35 36 37 38
   */
  protected $testLanguageSelector = TRUE;

  /**
   * Tests the basic translation UI.
   */
  function testTranslationUI() {
39
    $this->doTestBasicTranslation();
40
    $this->doTestTranslationOverview();
41 42 43
    $this->doTestOutdatedStatus();
    $this->doTestPublishedStatus();
    $this->doTestAuthoringInfo();
44
    $this->doTestTranslationEdit();
45
    $this->doTestTranslationChanged();
46
    $this->doTestTranslationDeletion();
47 48 49 50 51
  }

  /**
   * Tests the basic translation workflow.
   */
52
  protected function doTestBasicTranslation() {
53 54 55
    // Create a new test entity with original values in the default language.
    $default_langcode = $this->langcodes[0];
    $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
56
    $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
57
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
58
    $this->assertTrue($entity, 'Entity found in the database.');
59
    $this->drupalGet($entity->urlInfo());
60
    $this->assertResponse(200, 'Entity URL is valid.');
61
    $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
62
    $this->assertNoText('Source language', 'Source language column correctly hidden.');
63 64 65 66 67 68

    $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.', array('@property' => $property));
69
      $this->assertEqual($stored_value, $value, $message);
70 71
    }

72
    // Add a content translation.
73
    $langcode = 'it';
74
    $language = ConfigurableLanguage::load($langcode);
75 76
    $values[$langcode] = $this->getNewEntityValues($langcode);

77 78 79 80 81 82
    $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
      $entity->getEntityTypeId() => $entity->id(),
      'source' => $default_langcode,
      'target' => $langcode
    ], array('language' => $language));
    $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
83
    if ($this->testLanguageSelector) {
84
      $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.');
85
    }
86
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
87
    $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
88
    $this->assertNoText('Source language', 'Source language column correctly hidden.');
89 90 91

    // Switch the source language.
    $langcode = 'fr';
92
    $language = ConfigurableLanguage::load($langcode);
93 94
    $source_langcode = 'it';
    $edit = array('source_langcode[source]' => $source_langcode);
95 96 97 98 99
    $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
      $entity->getEntityTypeId() => $entity->id(),
      'source' => $default_langcode,
      'target' => $langcode
    ], array('language' => $language));
100 101
    // This does not save anything, it merely reloads the form and fills in the
    // fields with the values from the different source language.
102
    $this->drupalPostForm($add_url, $edit, t('Change'));
103
    $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.');
104 105 106

    // Add another translation and mark the other ones as outdated.
    $values[$langcode] = $this->getNewEntityValues($langcode);
107
    $edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
108 109 110 111 112 113
    $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
      $entity->getEntityTypeId() => $entity->id(),
      'source' => $source_langcode,
      'target' => $langcode
    ], array('language' => $language));
    $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
114
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
115
    $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
116
    $this->assertText('Source language', 'Source language column correctly shown.');
117 118 119 120 121 122 123 124 125 126 127

    // 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.', array('%property' => $property, '%language' => $langcode));
        $this->assertEqual($stored_value, $value, $message);
      }
    }
128 129
  }

130 131 132 133
  /**
   * Tests that the translation overview shows the correct values.
   */
  protected function doTestTranslationOverview() {
134
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
135
    $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
136 137 138

    foreach ($this->langcodes as $langcode) {
      if ($entity->hasTranslation($langcode)) {
139
        $language = new Language(array('id' => $langcode));
140
        $view_path = $entity->url('canonical', array('language' => $language));
141 142
        $elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path));
        $this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode)));
143
        $edit_path = $entity->url('edit-form', array('language' => $language));
144 145
        $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path));
        $this->assertEqual((string) $elements[0], t('Edit'), format_string('Edit link correct for %language translation.', array('%language' => $langcode)));
146 147 148 149
      }
    }
  }

150 151 152
  /**
   * Tests up-to-date status tracking.
   */
153
  protected function doTestOutdatedStatus() {
154
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
155
    $langcode = 'fr';
156
    $languages = \Drupal::languageManager()->getLanguages();
157 158

    // Mark translations as outdated.
159
    $edit = array('content_translation[retranslate]' => TRUE);
160 161
    $edit_path = $entity->urlInfo('edit-form', array('language' => $languages[$langcode]));
    $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
162
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
163

164 165
    // Check that every translation has the correct "outdated" status, and that
    // the Translation fieldset is open if the translation is "outdated".
166
    foreach ($this->langcodes as $added_langcode) {
167 168
      $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($added_langcode)));
      $this->drupalGet($url);
169
      if ($added_langcode == $langcode) {
170
        $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
171
        $this->assertFalse($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab should be collapsed by default.');
172 173
      }
      else {
174
        $this->assertFieldByXPath('//input[@name="content_translation[outdated]"]', TRUE, 'The translate flag is checked by default.');
175
        $this->assertTrue($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab is correctly expanded when the translation is outdated.');
176
        $edit = array('content_translation[outdated]' => FALSE);
177 178
        $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $added_langcode));
        $this->drupalGet($url);
179
        $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
180
        $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
181
        $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.');
182 183 184 185 186 187 188
      }
    }
  }

  /**
   * Tests the translation publishing status.
   */
189
  protected function doTestPublishedStatus() {
190
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
191 192 193 194

    // Unpublish translations.
    foreach ($this->langcodes as $index => $langcode) {
      if ($index > 0) {
195
        $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode)));
196
        $edit = array('content_translation[status]' => FALSE);
197
        $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
198
        $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
199
        $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.');
200 201 202
      }
    }

203
    // Check that the last published translation cannot be unpublished.
204
    $this->drupalGet($entity->urlInfo('edit-form'));
205
    $this->assertFieldByXPath('//input[@name="content_translation[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.');
206 207 208 209 210
  }

  /**
   * Tests the translation authoring information.
   */
211
  protected function doTestAuthoringInfo() {
212
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
213 214 215 216 217 218
    $values = array();

    // Post different authoring information for each translation.
    foreach ($this->langcodes as $index => $langcode) {
      $user = $this->drupalCreateUser();
      $values[$langcode] = array(
219
        'uid' => $user->id(),
220 221 222
        'created' => REQUEST_TIME - mt_rand(0, 1000),
      );
      $edit = array(
223
        'content_translation[uid]' => $user->getUsername(),
224
        'content_translation[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
225
      );
226 227
      $url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode)));
      $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
228 229
    }

230
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
231
    foreach ($this->langcodes as $langcode) {
232 233 234
      $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.');
235 236 237 238 239 240
    }

    // Try to post non valid values and check that they are rejected.
    $langcode = 'en';
    $edit = array(
      // User names have by default length 8.
241
      'content_translation[uid]' => $this->randomMachineName(12),
242
      'content_translation[created]' => '19/11/1978',
243
    );
244
    $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode));
245
    $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.');
246 247 248
    $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.');
249 250 251 252 253
  }

  /**
   * Tests translation deletion.
   */
254
  protected function doTestTranslationDeletion() {
255
    // Confirm and delete a translation.
256
    $this->drupalLogin($this->translator);
257
    $langcode = 'fr';
258
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
259 260
    $language = ConfigurableLanguage::load($langcode);
    $url = $entity->urlInfo('edit-form', array('language' => $language));
261
    $this->drupalPostForm($url, array(), t('Delete translation'));
262
    $this->drupalPostForm(NULL, array(), t('Delete @language translation', array('@language' => $language->getName())));
263
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
264 265
    if ($this->assertTrue(is_object($entity), 'Entity found')) {
      $translations = $entity->getTranslationLanguages();
266
      $this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.');
267
    }
268 269 270 271 272

    // Check that the translator cannot delete the original translation.
    $args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
    $this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args));
    $this->assertResponse(403);
273 274 275 276 277 278
  }

  /**
   * Returns an array of entity field values to be tested.
   */
  protected function getNewEntityValues($langcode) {
279
    return array($this->fieldName => array(array('value' => $this->randomMachineName(16))));
280 281 282 283 284 285 286
  }

  /**
   * Returns an edit array containing the values to be posted.
   */
  protected function getEditValues($values, $langcode, $new = FALSE) {
    $edit = $values[$langcode];
287
    $langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode;
288 289
    foreach ($values[$langcode] as $property => $value) {
      if (is_array($value)) {
290
        $edit["{$property}[0][value]"] = $value[0]['value'];
291 292 293 294 295 296
        unset($edit[$property]);
      }
    }
    return $edit;
  }

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
  /**
   * 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);
  }

313 314
  /**
   * Returns the form action value to be used to submit the entity form.
315 316 317
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being tested.
318 319
   * @param string $langcode
   *   Language code for the form.
320 321 322
   *
   * @return string
   *   Name of the button to hit.
323
   */
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
  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 '';
341 342
  }

343 344 345
  /**
   * Returns the translation object to use to retrieve the translated values.
   *
346
   * @param \Drupal\Core\Entity\EntityInterface $entity
347 348 349 350 351 352 353 354
   *   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) {
355
    return $entity->getTranslation($langcode);
356 357 358 359 360
  }

  /**
   * Returns the value for the specified property in the given language.
   *
361
   * @param \Drupal\Core\Entity\EntityInterface $translation
362 363 364 365 366 367 368 369 370
   *   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.
   */
371
  protected function getValue(EntityInterface $translation, $property, $langcode) {
372
    $key = $property == 'user_id' ? 'target_id' : 'value';
373
    return $translation->get($property)->{$key};
374 375
  }

376 377 378 379 380 381 382 383 384 385 386 387 388
  /**
   * Returns the name of the field that implements the changed timestamp.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being tested.
   *
   * @return string
   *   The the field name.
   */
  protected function getChangedFieldName($entity) {
    return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
  }

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
  /**
   * Tests edit content translation.
   */
  protected function doTestTranslationEdit() {
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
    $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 = array('language' => $languages[$langcode]);
        $url = $entity->urlInfo('edit-form', $options);
        $this->drupalGet($url);

        $this->assertRaw($entity->getTranslation($langcode)->label());
      }
    }
  }

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
  /**
   * Tests the basic translation workflow.
   */
  protected function doTestTranslationChanged() {
    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
    $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 = array(
          $this->fieldName . '[0][value]' => $this->randomString(),
        );
        $edit_path = $entity->urlInfo('edit-form', array('language' => $language));
        $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));

        $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
        $this->assertEqual(
          $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
          format_string('Changed time for language %language is the latest change over all languages.', array('%language' => $language->getName()))
        );
      }

      $timestamps = array();
      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.'
        );
      }
    }
  }

475
}