DateTimeFieldTest.php 42.7 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\datetime\Functional;
4

5
use Drupal\Component\Render\FormattableMarkup;
6
use Drupal\Core\Datetime\DrupalDateTime;
7
use Drupal\Core\Datetime\Entity\DateFormat;
8
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
9
use Drupal\entity_test\Entity\EntityTest;
10
use Drupal\field\Entity\FieldConfig;
11
use Drupal\field\Entity\FieldStorageConfig;
12
use Drupal\node\Entity\Node;
13 14 15

/**
 * Tests Datetime field functionality.
16 17
 *
 * @group datetime
18
 */
19
class DateTimeFieldTest extends DateTestBase {
20

21 22
  /**
   * The default display settings to use for the formatters.
23 24 25
   *
   * @var array
   */
26
  protected $defaultSettings = ['timezone_override' => ''];
27

28 29 30
  /**
   * {@inheritdoc}
   */
31 32
  protected function getTestFieldType() {
    return 'datetime';
33 34 35 36 37
  }

  /**
   * Tests date field functionality.
   */
38
  public function testDateField() {
39
    $field_name = $this->fieldStorage->getName();
40

41 42
    $display_repository = \Drupal::service('entity_display.repository');

43 44 45
    // Loop through defined timezones to test that date-only fields work at the
    // extremes.
    foreach (static::$timezones as $timezone) {
46

47
      $this->setSiteTimezone($timezone);
48
      $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
49

50 51 52
      // Display creation form.
      $this->drupalGet('entity_test/add');
      $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
53
      $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]//label[contains(@class,"js-form-required")]', TRUE, 'Required markup found');
54
      $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Time element not found.');
55 56
      $this->assertFieldByXPath('//input[@aria-describedby="edit-' . $field_name . '-0-value--description"]', NULL, 'ARIA described-by found');
      $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0-value--description"]', NULL, 'ARIA description found');
57 58 59 60 61

      // Build up a date in the UTC timezone. Note that using this will also
      // mimic the user in a different timezone simply entering '2012-12-31' via
      // the UI.
      $value = '2012-12-31 00:00:00';
62
      $date = new DrupalDateTime($value, DateTimeItemInterface::STORAGE_TIMEZONE);
63 64 65 66 67

      // Submit a valid date and ensure it is accepted.
      $date_format = DateFormat::load('html_date')->getPattern();
      $time_format = DateFormat::load('html_time')->getPattern();

68
      $edit = [
69
        "{$field_name}[0][value][date]" => $date->format($date_format),
70
      ];
71
      $this->drupalPostForm(NULL, $edit, t('Save'));
72
      preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
73
      $id = $match[1];
74
      $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
      $this->assertRaw($date->format($date_format));
      $this->assertNoRaw($date->format($time_format));

      // Verify the date doesn't change if using a timezone that is UTC+12 when
      // the entity is edited through the form.
      $entity = EntityTest::load($id);
      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
      $this->drupalGet('entity_test/manage/' . $id . '/edit');
      $this->drupalPostForm(NULL, [], t('Save'));
      $this->drupalGet('entity_test/manage/' . $id . '/edit');
      $this->drupalPostForm(NULL, [], t('Save'));
      $this->drupalGet('entity_test/manage/' . $id . '/edit');
      $this->drupalPostForm(NULL, [], t('Save'));
      $entity = EntityTest::load($id);
      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);

      // Reset display options since these get changed below.
92
      $this->displayOptions = [
93 94
        'type' => 'datetime_default',
        'label' => 'hidden',
95 96
        'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
      ];
97
      // Verify that the date is output according to the formatter settings.
98 99 100
      $options = [
        'format_type' => ['short', 'medium', 'long'],
      ];
101 102 103
      // Formats that display a time component for date-only fields will display
      // the default time, so that is applied before calculating the expected
      // value.
104
      $this->massageTestDate($date);
105 106 107
      foreach ($options as $setting => $values) {
        foreach ($values as $new_value) {
          // Update the entity display settings.
108
          $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
109 110
          $this->container->get('entity_display.repository')
            ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
111 112 113 114 115 116 117 118
            ->setComponent($field_name, $this->displayOptions)
            ->save();

          $this->renderTestEntity($id);
          switch ($setting) {
            case 'format_type':
              // Verify that a date is displayed. Since this is a date-only
              // field, it is expected to display the time as 00:00:00.
119 120 121 122
              /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
              $date_formatter = $this->container->get('date.formatter');
              $expected = $date_formatter->format($date->getTimestamp(), $new_value, '', DateTimeItemInterface::STORAGE_TIMEZONE);
              $expected_iso = $date_formatter->format($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DateTimeItemInterface::STORAGE_TIMEZONE);
123 124
              $output = $this->renderTestEntity($id);
              $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
125 126 127 128 129 130
              $this->assertContains($expected_markup, $output, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute in %timezone.', [
                '%value' => $new_value,
                '%expected' => $expected,
                '%expected_iso' => $expected_iso,
                '%timezone' => $timezone,
              ]));
131 132
              break;
          }
133 134 135
        }
      }

136 137 138
      // Verify that the plain formatter works.
      $this->displayOptions['type'] = 'datetime_plain';
      $this->displayOptions['settings'] = $this->defaultSettings;
139 140
      $display_repository
        ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
141 142
        ->setComponent($field_name, $this->displayOptions)
        ->save();
143
      $expected = $date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
144
      $output = $this->renderTestEntity($id);
145 146 147 148
      $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using plain format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
149 150 151

      // Verify that the 'datetime_custom' formatter works.
      $this->displayOptions['type'] = 'datetime_custom';
152
      $this->displayOptions['settings'] = ['date_format' => 'm/d/Y'] + $this->defaultSettings;
153 154
      $display_repository
        ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
155 156 157
        ->setComponent($field_name, $this->displayOptions)
        ->save();
      $expected = $date->format($this->displayOptions['settings']['date_format']);
158
      $output = $this->renderTestEntity($id);
159 160 161 162
      $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using datetime_custom format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
163 164 165 166

      // Test that allowed markup in custom format is preserved and XSS is
      // removed.
      $this->displayOptions['settings']['date_format'] = '\\<\\s\\t\\r\\o\\n\\g\\>m/d/Y\\<\\/\\s\\t\\r\\o\\n\\g\\>\\<\\s\\c\\r\\i\\p\\t\\>\\a\\l\\e\\r\\t\\(\\S\\t\\r\\i\\n\\g\\.\\f\\r\\o\\m\\C\\h\\a\\r\\C\\o\\d\\e\\(\\8\\8\\,\\8\\3\\,\\8\\3\\)\\)\\<\\/\\s\\c\\r\\i\\p\\t\\>';
167
      $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
168 169 170
        ->setComponent($field_name, $this->displayOptions)
        ->save();
      $expected = '<strong>' . $date->format('m/d/Y') . '</strong>alert(String.fromCharCode(88,83,83))';
171
      $output = $this->renderTestEntity($id);
172 173 174 175
      $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
176 177 178 179 180

      // Verify that the 'datetime_time_ago' formatter works for intervals in the
      // past.  First update the test entity so that the date difference always
      // has the same interval.  Since the database always stores UTC, and the
      // interval will use this, force the test date to use UTC and not the local
181
      // or user timezone.
182 183 184 185 186 187 188 189
      $timestamp = REQUEST_TIME - 87654321;
      $entity = EntityTest::load($id);
      $field_name = $this->fieldStorage->getName();
      $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
      $entity->{$field_name}->value = $date->format($date_format);
      $entity->save();

      $this->displayOptions['type'] = 'datetime_time_ago';
190
      $this->displayOptions['settings'] = [
191 192 193
        'future_format' => '@interval in the future',
        'past_format' => '@interval in the past',
        'granularity' => 3,
194
      ];
195
      $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
196 197
        ->setComponent($field_name, $this->displayOptions)
        ->save();
198
      $expected = new FormattableMarkup($this->displayOptions['settings']['past_format'], [
199
        '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
200
      ]);
201
      $output = $this->renderTestEntity($id);
202 203 204 205
      $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
206 207 208 209 210

      // Verify that the 'datetime_time_ago' formatter works for intervals in the
      // future.  First update the test entity so that the date difference always
      // has the same interval.  Since the database always stores UTC, and the
      // interval will use this, force the test date to use UTC and not the local
211
      // or user timezone.
212 213 214 215 216 217 218
      $timestamp = REQUEST_TIME + 87654321;
      $entity = EntityTest::load($id);
      $field_name = $this->fieldStorage->getName();
      $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
      $entity->{$field_name}->value = $date->format($date_format);
      $entity->save();

219
      $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
220 221
        ->setComponent($field_name, $this->displayOptions)
        ->save();
222
      $expected = new FormattableMarkup($this->displayOptions['settings']['future_format'], [
223
        '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
224
      ]);
225
      $output = $this->renderTestEntity($id);
226 227 228 229
      $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
230
    }
231 232 233 234 235
  }

  /**
   * Tests date and time field.
   */
236
  public function testDatetimeField() {
237
    $field_name = $this->fieldStorage->getName();
238
    $field_label = $this->field->label();
239
    // Change the field to a datetime field.
240
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
241
    $this->fieldStorage->save();
242 243

    // Display creation form.
244
    $this->drupalGet('entity_test/add');
245 246
    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
247
    $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
248 249
    $this->assertFieldByXPath('//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]', NULL, 'ARIA described-by found');
    $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0--description"]', NULL, 'ARIA description found');
250

251
    // Build up a date in the UTC timezone.
252
    $value = '2012-12-31 00:00:00';
253 254 255 256 257 258
    $date = new DrupalDateTime($value, 'UTC');

    // Update the timezone to the system default.
    $date->setTimezone(timezone_open(drupal_get_user_timezone()));

    // Submit a valid date and ensure it is accepted.
259 260
    $date_format = DateFormat::load('html_date')->getPattern();
    $time_format = DateFormat::load('html_time')->getPattern();
261

262
    $edit = [
263 264
      "{$field_name}[0][value][date]" => $date->format($date_format),
      "{$field_name}[0][value][time]" => $date->format($time_format),
265
    ];
266
    $this->drupalPostForm(NULL, $edit, t('Save'));
267
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
268
    $id = $match[1];
269
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
270 271 272
    $this->assertRaw($date->format($date_format));
    $this->assertRaw($date->format($time_format));

273 274 275
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');

276
    // Verify that the date is output according to the formatter settings.
277 278 279
    $options = [
      'format_type' => ['short', 'medium', 'long'],
    ];
280 281 282
    foreach ($options as $setting => $values) {
      foreach ($values as $new_value) {
        // Update the entity display settings.
283
        $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
284
        $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
285
          ->setComponent($field_name, $this->displayOptions)
286 287 288 289 290 291
          ->save();

        $this->renderTestEntity($id);
        switch ($setting) {
          case 'format_type':
            // Verify that a date is displayed.
292 293 294
            $date_formatter = $this->container->get('date.formatter');
            $expected = $date_formatter->format($date->getTimestamp(), $new_value);
            $expected_iso = $date_formatter->format($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
295 296
            $output = $this->renderTestEntity($id);
            $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
297
            $this->assertContains($expected_markup, $output, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => $new_value, '%expected' => $expected, '%expected_iso' => $expected_iso]));
298 299 300 301 302 303
            break;
        }
      }
    }

    // Verify that the plain formatter works.
304
    $this->displayOptions['type'] = 'datetime_plain';
305
    $this->displayOptions['settings'] = $this->defaultSettings;
306 307
    $display_repository
      ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
308
      ->setComponent($field_name, $this->displayOptions)
309
      ->save();
310
    $expected = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
311
    $output = $this->renderTestEntity($id);
312
    $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', ['%expected' => $expected]));
313 314 315

    // Verify that the 'datetime_custom' formatter works.
    $this->displayOptions['type'] = 'datetime_custom';
316
    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
317
    $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
318 319 320
      ->setComponent($field_name, $this->displayOptions)
      ->save();
    $expected = $date->format($this->displayOptions['settings']['date_format']);
321
    $output = $this->renderTestEntity($id);
322
    $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
323 324 325

    // Verify that the 'timezone_override' setting works.
    $this->displayOptions['type'] = 'datetime_custom';
326
    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
327
    $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
328 329
      ->setComponent($field_name, $this->displayOptions)
      ->save();
330
    $expected = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
331
    $output = $this->renderTestEntity($id);
332
    $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
333 334 335 336 337

    // Verify that the 'datetime_time_ago' formatter works for intervals in the
    // past.  First update the test entity so that the date difference always
    // has the same interval.  Since the database always stores UTC, and the
    // interval will use this, force the test date to use UTC and not the local
338
    // or user timezone.
339
    $timestamp = REQUEST_TIME - 87654321;
340
    $entity = EntityTest::load($id);
341 342
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
343
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
344 345 346
    $entity->save();

    $this->displayOptions['type'] = 'datetime_time_ago';
347
    $this->displayOptions['settings'] = [
348 349 350
      'future_format' => '@interval from now',
      'past_format' => '@interval earlier',
      'granularity' => 3,
351
    ];
352
    $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
353 354
      ->setComponent($field_name, $this->displayOptions)
      ->save();
355
    $expected = new FormattableMarkup($this->displayOptions['settings']['past_format'], [
356
      '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
357
    ]);
358
    $output = $this->renderTestEntity($id);
359
    $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
360 361 362 363 364

    // Verify that the 'datetime_time_ago' formatter works for intervals in the
    // future.  First update the test entity so that the date difference always
    // has the same interval.  Since the database always stores UTC, and the
    // interval will use this, force the test date to use UTC and not the local
365
    // or user timezone.
366
    $timestamp = REQUEST_TIME + 87654321;
367
    $entity = EntityTest::load($id);
368 369
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
370
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
371 372
    $entity->save();

373 374
    $display_repository
      ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
375 376
      ->setComponent($field_name, $this->displayOptions)
      ->save();
377
    $expected = new FormattableMarkup($this->displayOptions['settings']['future_format'], [
378
      '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
379
    ]);
380
    $output = $this->renderTestEntity($id);
381
    $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
382 383 384 385 386
  }

  /**
   * Tests Date List Widget functionality.
   */
387
  public function testDatelistWidget() {
388
    $field_name = $this->fieldStorage->getName();
389
    $field_label = $this->field->label();
390 391 392 393 394

    // Ensure field is set to a date only field.
    $this->fieldStorage->setSetting('datetime_type', 'date');
    $this->fieldStorage->save();

395 396 397
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');

398
    // Change the widget to a datelist widget.
399
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
400
      ->setComponent($field_name, [
401
        'type' => 'datetime_datelist',
402
        'settings' => [
403
          'date_order' => 'YMD',
404 405
        ],
      ])
406
      ->save();
407
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
408 409 410

    // Display creation form.
    $this->drupalGet('entity_test/add');
411
    $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
412 413
    $this->assertFieldByXPath('//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]', NULL, 'ARIA described-by found');
    $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0--description"]', NULL, 'ARIA description found');
414 415 416 417 418 419 420 421 422 423

    // Assert that Hour and Minute Elements do not appear on Date Only
    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');

    // Go to the form display page to assert that increment option does not appear on Date Only
    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
    $this->drupalGet($fieldEditUrl);

    // Click on the widget settings button to open the widget settings form.
424
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
425 426 427
    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
    $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');

428
    // Change the field to a datetime field.
429
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
430
    $this->fieldStorage->save();
431 432

    // Change the widget to a datelist widget.
433
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
434
      ->setComponent($field_name, [
435
        'type' => 'datetime_datelist',
436
        'settings' => [
437 438 439
          'increment' => 1,
          'date_order' => 'YMD',
          'time_type' => '12',
440 441
        ],
      ])
442
      ->save();
443
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
444

445 446 447 448 449
    // Go to the form display page to assert that increment option does appear on Date Time
    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
    $this->drupalGet($fieldEditUrl);

    // Click on the widget settings button to open the widget settings form.
450
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
451 452
    $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');

453
    // Display creation form.
454
    $this->drupalGet('entity_test/add');
455 456 457

    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-year\"]", NULL, 'Year element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-year", '', 'No year selected.');
458
    $this->assertOptionByText("edit-$field_name-0-value-year", t('Year'));
459 460
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-month\"]", NULL, 'Month element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-month", '', 'No month selected.');
461
    $this->assertOptionByText("edit-$field_name-0-value-month", t('Month'));
462 463
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-day\"]", NULL, 'Day element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-day", '', 'No day selected.');
464
    $this->assertOptionByText("edit-$field_name-0-value-day", t('Day'));
465 466
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
467
    $this->assertOptionByText("edit-$field_name-0-value-hour", t('Hour'));
468 469
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '', 'No minute selected.');
470
    $this->assertOptionByText("edit-$field_name-0-value-minute", t('Minute'));
471 472 473
    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-second\"]", NULL, 'Second element not found.');
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-ampm", '', 'No ampm selected.');
474
    $this->assertOptionByText("edit-$field_name-0-value-ampm", t('AM/PM'));
475 476

    // Submit a valid date and ensure it is accepted.
477
    $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
478

479
    $edit = [];
480 481 482
    // Add the ampm indicator since we are testing 12 hour time.
    $date_value['ampm'] = 'am';
    foreach ($date_value as $part => $value) {
483
      $edit["{$field_name}[0][value][$part]"] = $value;
484 485
    }

486
    $this->drupalPostForm(NULL, $edit, t('Save'));
487
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
488
    $id = $match[1];
489
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
490

491 492 493 494 495 496
    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
497 498

    // Test the widget using increment other than 1 and 24 hour mode.
499
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
500
      ->setComponent($field_name, [
501
        'type' => 'datetime_datelist',
502
        'settings' => [
503 504 505
          'increment' => 15,
          'date_order' => 'YMD',
          'time_type' => '24',
506 507
        ],
      ])
508
      ->save();
509
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
510 511 512 513 514 515 516 517 518 519

    // Display creation form.
    $this->drupalGet('entity_test/add');

    // Other elements are unaffected by the changed settings.
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');

    // Submit a valid date and ensure it is accepted.
520
    $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
521

522
    $edit = [];
523 524 525 526 527
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
528
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
529
    $id = $match[1];
530
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
531 532 533 534 535 536

    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
537 538

    // Test the widget for partial completion of fields.
539
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
540
      ->setComponent($field_name, [
541
        'type' => 'datetime_datelist',
542
        'settings' => [
543 544 545
          'increment' => 1,
          'date_order' => 'YMD',
          'time_type' => '24',
546 547
        ],
      ])
548
      ->save();
549
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
550 551

    // Test the widget for validation notifications.
552
    foreach ($this->datelistDataProvider($field_label) as $data) {
553 554 555 556 557 558
      list($date_value, $expected) = $data;

      // Display creation form.
      $this->drupalGet('entity_test/add');

      // Submit a partial date and ensure and error message is provided.
559
      $edit = [];
560 561 562 563 564 565
      foreach ($date_value as $part => $value) {
        $edit["{$field_name}[0][value][$part]"] = $value;
      }

      $this->drupalPostForm(NULL, $edit, t('Save'));
      $this->assertResponse(200);
566 567 568
      foreach ($expected as $expected_text) {
        $this->assertText(t($expected_text));
      }
569 570 571 572 573
    }

    // Test the widget for complete input with zeros as part of selections.
    $this->drupalGet('entity_test/add');

574 575
    $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'];
    $edit = [];
576 577 578 579 580 581
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertResponse(200);
582
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
583
    $id = $match[1];
584
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
585 586 587 588

    // Test the widget to ensure zeros are not deselected on validation.
    $this->drupalGet('entity_test/add');

589 590
    $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => '0'];
    $edit = [];
591 592 593 594 595 596 597
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertResponse(200);
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
598
  }
599 600 601 602

  /**
   * The data provider for testing the validation of the datelist widget.
   *
603 604 605
   * @param string $field_label
   *   The label of the field being tested.
   *
606 607 608
   * @return array
   *   An array of datelist input permutations to test.
   */
609
  protected function datelistDataProvider($field_label) {
610
    return [
611
      // Nothing selected.
612 613 614 615
      [
        ['year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
        ["The $field_label date is required."],
      ],
616
      // Year only selected, validation error on Month, Day, Hour, Minute.
617 618 619 620 621 622 623 624 625 626
      [
        ['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for month.',
          'A value must be selected for day.',
          'A value must be selected for hour.',
          'A value must be selected for minute.',
        ],
      ],
627
      // Year and Month selected, validation error on Day, Hour, Minute.
628 629 630 631 632 633 634 635 636
      [
        ['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for day.',
          'A value must be selected for hour.',
          'A value must be selected for minute.',
        ],
      ],
637
      // Year, Month and Day selected, validation error on Hour, Minute.
638 639 640 641 642 643 644 645
      [
        ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for hour.',
          'A value must be selected for minute.',
        ],
      ],
646
      // Year, Month, Day and Hour selected, validation error on Minute only.
647 648 649 650 651 652 653
      [
        ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for minute.',
        ],
      ],
654
    ];
655 656 657 658 659
  }

  /**
   * Test default value functionality.
   */
660
  public function testDefaultValue() {
661
    // Create a test content type.
662
    $this->drupalCreateContentType(['type' => 'date_content']);
663

664
    // Create a field storage with settings to validate.
665
    $field_name = mb_strtolower($this->randomMachineName());
666
    $field_storage = FieldStorageConfig::create([
667
      'field_name' => $field_name,
668 669
      'entity_type' => 'node',
      'type' => 'datetime',
670 671
      'settings' => ['datetime_type' => 'date'],
    ]);
672
    $field_storage->save();
673

674
    $field = FieldConfig::create([
675
      'field_storage' => $field_storage,
676
      'bundle' => 'date_content',
677
    ]);
678
    $field->save();
679

680 681 682
    // Loop through defined timezones to test that date-only defaults work at
    // the extremes.
    foreach (static::$timezones as $timezone) {
683

684
      $this->setSiteTimezone($timezone);
685
      $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
686

687
      // Set now as default_value.
688
      $field_edit = [
689
        'default_value_input[default_date_type]' => 'now',
690
      ];
691 692 693 694 695 696 697 698 699 700
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));

      // Check that default value is selected in default value form.
      $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
      $this->assertOptionSelected('edit-default-value-input-default-date-type', 'now', 'The default value is selected in instance settings page');
      $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default value is empty in instance settings page');

      // Check if default_date has been stored successfully.
      $config_entity = $this->config('field.field.node.date_content.' . $field_name)
        ->get();
701
      $this->assertEqual($config_entity['default_value'][0], [
702 703
        'default_date_type' => 'now',
        'default_date' => 'now',
704
      ], 'Default value has been stored successfully');
705 706

      // Clear field cache in order to avoid stale cache values.
707
      \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
708 709 710 711 712

      // Create a new node to check that datetime field default value is today.
      $new_node = Node::create(['type' => 'date_content']);
      $expected_date = new DrupalDateTime('now', drupal_get_user_timezone());
      $this->assertEqual($new_node->get($field_name)
713
        ->offsetGet(0)->value, $expected_date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT));
714 715

      // Set an invalid relative default_value to test validation.
716
      $field_edit = [
717 718
        'default_value_input[default_date_type]' => 'relative',
        'default_value_input[default_date]' => 'invalid date',
719
      ];
720
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
721

722
      $this->assertText('The relative date value entered is invalid.');
723

724
      // Set a relative default_value.
725
      $field_edit = [
726 727
        'default_value_input[default_date_type]' => 'relative',
        'default_value_input[default_date]' => '+90 days',
728
      ];
729 730 731 732 733 734 735 736 737 738
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));

      // Check that default value is selected in default value form.
      $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
      $this->assertOptionSelected('edit-default-value-input-default-date-type', 'relative', 'The default value is selected in instance settings page');
      $this->assertFieldByName('default_value_input[default_date]', '+90 days', 'The relative default value is displayed in instance settings page');

      // Check if default_date has been stored successfully.
      $config_entity = $this->config('field.field.node.date_content.' . $field_name)
        ->get();
739
      $this->assertEqual($config_entity['default_value'][0], [
740 741
        'default_date_type' => 'relative',
        'default_date' => '+90 days',
742
      ], 'Default value has been stored successfully');
743 744

      // Clear field cache in order to avoid stale cache values.
745
      \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
746 747 748 749 750 751

      // Create a new node to check that datetime field default value is +90
      // days.
      $new_node = Node::create(['type' => 'date_content']);
      $expected_date = new DrupalDateTime('+90 days', drupal_get_user_timezone());
      $this->assertEqual($new_node->get($field_name)
752
        ->offsetGet(0)->value, $expected_date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT));
753 754

      // Remove default value.
755
      $field_edit = [
756
        'default_value_input[default_date_type]' => '',
757
      ];
758
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
759

760 761 762 763
      // Check that default value is selected in default value form.
      $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
      $this->assertOptionSelected('edit-default-value-input-default-date-type', '', 'The default value is selected in instance settings page');
      $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default value is empty in instance settings page');
764

765 766 767 768
      // Check if default_date has been stored successfully.
      $config_entity = $this->config('field.field.node.date_content.' . $field_name)
        ->get();
      $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
769

770
      // Clear field cache in order to avoid stale cache values.
771
      \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
772

773 774 775 776 777
      // Create a new node to check that datetime field default value is not
      // set.
      $new_node = Node::create(['type' => 'date_content']);
      $this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
    }
778 779 780 781 782
  }

  /**
   * Test that invalid values are caught and marked as invalid.
   */
783
  public function testInvalidField() {
784
    // Change the field to a datetime field.
785
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
786
    $this->fieldStorage->save();
787
    $field_name = $this->fieldStorage->getName();
788 789

    // Display creation form.
790
    $this->drupalGet('entity_test/add');
791 792
    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
793 794 795

    // Submit invalid dates and ensure they is not accepted.
    $date_value = '';
796
    $edit = [
797 798
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '12:00:00',
799
    ];
800
    $this->drupalPostForm(NULL, $edit, t('Save'));
801 802 803
    $this->assertText('date is invalid', 'Empty date value has been caught.');

    $date_value = 'aaaa-12-01';
804
    $edit = [
805 806
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '00:00:00',
807
    ];
808
    $this->drupalPostForm(NULL, $edit, t('Save'));
809
    $this->assertText('date is invalid', format_string('Invalid year value %date has been caught.', ['%date' => $date_value]));
810 811

    $date_value = '2012-75-01';
812
    $edit = [
813 814
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '00:00:00',
815
    ];
816
    $this->drupalPostForm(NULL, $edit, t('Save'));
817
    $this->assertText('date is invalid', format_string('Invalid month value %date has been caught.', ['%date' => $date_value]));
818 819

    $date_value = '2012-12-99';
820
    $edit = [
821 822
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '00:00:00',
823
    ];
824
    $this->drupalPostForm(NULL, $edit, t('Save'));
825
    $this->assertText('date is invalid', format_string('Invalid day value %date has been caught.', ['%date' => $date_value]));
826 827 828

    $date_value = '2012-12-01';
    $time_value = '';
829
    $edit = [
830 831
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
832
    ];
833
    $this->drupalPostForm(NULL, $edit, t('Save'));
834 835 836 837
    $this->assertText('date is invalid', 'Empty time value has been caught.');

    $date_value = '2012-12-01';
    $time_value = '49:00:00';
838
    $edit = [
839 840
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
841
    ];
842
    $this->drupalPostForm(NULL, $edit, t('Save'));
843
    $this->assertText('date is invalid', format_string('Invalid hour value %time has been caught.', ['%time' => $time_value]));
Dries's avatar