DateTimeFieldTest.php 43.2 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 31 32
  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'classy';

33 34 35
  /**
   * {@inheritdoc}
   */
36 37
  protected function getTestFieldType() {
    return 'datetime';
38 39 40 41 42
  }

  /**
   * Tests date field functionality.
   */
43
  public function testDateField() {
44
    $field_name = $this->fieldStorage->getName();
45

46 47
    $display_repository = \Drupal::service('entity_display.repository');

48 49 50
    // Loop through defined timezones to test that date-only fields work at the
    // extremes.
    foreach (static::$timezones as $timezone) {
51

52
      $this->setSiteTimezone($timezone);
53
      $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
54

55 56
      // Display creation form.
      $this->drupalGet('entity_test/add');
57
      $this->assertSession()->fieldValueEquals("{$field_name}[0][value][date]", '');
58
      $this->assertSession()->elementExists('xpath', '//*[@id="edit-' . $field_name . '-wrapper"]//label[contains(@class,"js-form-required")]');
59
      $this->assertSession()->fieldNotExists("{$field_name}[0][value][time]");
60 61 62
      // ARIA described-by.
      $this->assertSession()->elementExists('xpath', '//input[@aria-describedby="edit-' . $field_name . '-0-value--description"]');
      $this->assertSession()->elementExists('xpath', '//div[@id="edit-' . $field_name . '-0-value--description"]');
63 64 65 66 67

      // 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';
68
      $date = new DrupalDateTime($value, DateTimeItemInterface::STORAGE_TIMEZONE);
69 70 71 72 73

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

74
      $edit = [
75
        "{$field_name}[0][value][date]" => $date->format($date_format),
76
      ];
77
      $this->drupalPostForm(NULL, $edit, t('Save'));
78
      preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
79
      $id = $match[1];
80
      $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
      $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.
98
      $this->displayOptions = [
99 100
        'type' => 'datetime_default',
        'label' => 'hidden',
101 102
        'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
      ];
103
      // Verify that the date is output according to the formatter settings.
104 105 106
      $options = [
        'format_type' => ['short', 'medium', 'long'],
      ];
107 108 109
      // Formats that display a time component for date-only fields will display
      // the default time, so that is applied before calculating the expected
      // value.
110
      $this->massageTestDate($date);
111 112 113
      foreach ($options as $setting => $values) {
        foreach ($values as $new_value) {
          // Update the entity display settings.
114
          $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
115 116
          $this->container->get('entity_display.repository')
            ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
117 118 119 120 121 122 123 124
            ->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.
125 126 127 128
              /** @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);
129 130
              $output = $this->renderTestEntity($id);
              $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
131
              $this->assertStringContainsString($expected_markup, $output, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute in %timezone.', [
132 133 134 135 136
                '%value' => $new_value,
                '%expected' => $expected,
                '%expected_iso' => $expected_iso,
                '%timezone' => $timezone,
              ]));
137 138
              break;
          }
139 140 141
        }
      }

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

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

      // 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\\>';
173
      $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
174 175 176
        ->setComponent($field_name, $this->displayOptions)
        ->save();
      $expected = '<strong>' . $date->format('m/d/Y') . '</strong>alert(String.fromCharCode(88,83,83))';
177
      $output = $this->renderTestEntity($id);
178
      $this->assertStringContainsString($expected, $output, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected in %timezone.', [
179 180 181
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
182 183 184 185 186

      // 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
187
      // or user timezone.
188 189 190 191 192 193 194 195
      $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';
196
      $this->displayOptions['settings'] = [
197 198 199
        'future_format' => '@interval in the future',
        'past_format' => '@interval in the past',
        'granularity' => 3,
200
      ];
201
      $display_repository->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
202 203
        ->setComponent($field_name, $this->displayOptions)
        ->save();
204
      $expected = new FormattableMarkup($this->displayOptions['settings']['past_format'], [
205
        '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
206
      ]);
207
      $output = $this->renderTestEntity($id);
208
      $this->assertStringContainsString((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
209 210 211
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
212 213 214 215 216

      // 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
217
      // or user timezone.
218 219 220 221 222 223 224
      $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();

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

  /**
   * Tests date and time field.
   */
242
  public function testDatetimeField() {
243
    $field_name = $this->fieldStorage->getName();
244
    $field_label = $this->field->label();
245
    // Change the field to a datetime field.
246
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
247
    $this->fieldStorage->save();
248 249

    // Display creation form.
250
    $this->drupalGet('entity_test/add');
251 252
    $this->assertSession()->fieldValueEquals("{$field_name}[0][value][date]", '');
    $this->assertSession()->fieldValueEquals("{$field_name}[0][value][time]", '');
253 254 255
    $this->assertSession()->elementTextContains('xpath', '//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label);
    $this->assertSession()->elementExists('xpath', '//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]');
    $this->assertSession()->elementExists('xpath', '//div[@id="edit-' . $field_name . '-0--description"]');
256

257
    // Build up a date in the UTC timezone.
258
    $value = '2012-12-31 00:00:00';
259 260 261
    $date = new DrupalDateTime($value, 'UTC');

    // Update the timezone to the system default.
262
    $date->setTimezone(timezone_open(date_default_timezone_get()));
263 264

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

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

279 280 281
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');

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

        $this->renderTestEntity($id);
        switch ($setting) {
          case 'format_type':
            // Verify that a date is displayed.
298 299 300
            $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');
301 302
            $output = $this->renderTestEntity($id);
            $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
303
            $this->assertStringContainsString($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]));
304 305 306 307 308 309
            break;
        }
      }
    }

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

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

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

    // 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
344
    // or user timezone.
345
    $timestamp = REQUEST_TIME - 87654321;
346
    $entity = EntityTest::load($id);
347 348
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
349
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
350 351 352
    $entity->save();

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

    // 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
371
    // or user timezone.
372
    $timestamp = REQUEST_TIME + 87654321;
373
    $entity = EntityTest::load($id);
374 375
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
376
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
377 378
    $entity->save();

379 380
    $display_repository
      ->getViewDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
381 382
      ->setComponent($field_name, $this->displayOptions)
      ->save();
383
    $expected = new FormattableMarkup($this->displayOptions['settings']['future_format'], [
384
      '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
385
    ]);
386
    $output = $this->renderTestEntity($id);
387
    $this->assertStringContainsString((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
388 389 390 391 392
  }

  /**
   * Tests Date List Widget functionality.
   */
393
  public function testDatelistWidget() {
394
    $field_name = $this->fieldStorage->getName();
395
    $field_label = $this->field->label();
396 397 398 399 400

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

401 402 403
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');

404
    // Change the widget to a datelist widget.
405
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
406
      ->setComponent($field_name, [
407
        'type' => 'datetime_datelist',
408
        'settings' => [
409
          'date_order' => 'YMD',
410 411
        ],
      ])
412
      ->save();
413
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
414 415 416

    // Display creation form.
    $this->drupalGet('entity_test/add');
417 418 419
    $this->assertSession()->elementTextContains('xpath', '//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label);
    $this->assertSession()->elementExists('xpath', '//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]');
    $this->assertSession()->elementExists('xpath', '//div[@id="edit-' . $field_name . '-0--description"]');
420 421

    // Assert that Hour and Minute Elements do not appear on Date Only
422 423
    $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-hour\"]");
    $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-minute\"]");
424 425 426 427 428 429

    // 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.
430
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
431
    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
432
    $this->assertSession()->elementNotExists('xpath', $xpathIncr);
433

434
    // Change the field to a datetime field.
435
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
436
    $this->fieldStorage->save();
437 438

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

451 452 453 454 455
    // 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.
456
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
457
    $this->assertSession()->elementExists('xpath', $xpathIncr);
458

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

462 463
    // Year element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-year\"]");
464
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-year", '')->isSelected());
465
    $this->assertSession()->optionExists("edit-$field_name-0-value-year", 'Year');
466 467
    // Month element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-month\"]");
468
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-month", '')->isSelected());
469
    $this->assertSession()->optionExists("edit-$field_name-0-value-month", 'Month');
470 471
    // Day element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-day\"]");
472
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-day", '')->isSelected());
473
    $this->assertSession()->optionExists("edit-$field_name-0-value-day", 'Day');
474 475
    // Hour element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-hour\"]");
476
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-hour", '')->isSelected());
477
    $this->assertSession()->optionExists("edit-$field_name-0-value-hour", 'Hour');
478 479
    // Minute element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-minute\"]");
480
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-minute", '')->isSelected());
481
    $this->assertSession()->optionExists("edit-$field_name-0-value-minute", 'Minute');
482 483 484 485
    // No Second element.
    $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-second\"]");
    // AMPM element.
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-ampm\"]");
486
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-ampm", '')->isSelected());
487
    $this->assertSession()->optionExists("edit-$field_name-0-value-ampm", 'AM/PM');
488 489

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

492
    $edit = [];
493 494 495
    // Add the ampm indicator since we are testing 12 hour time.
    $date_value['ampm'] = 'am';
    foreach ($date_value as $part => $value) {
496
      $edit["{$field_name}[0][value][$part]"] = $value;
497 498
    }

499
    $this->drupalPostForm(NULL, $edit, t('Save'));
500
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
501
    $id = $match[1];
502
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
503

504 505 506 507 508 509
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-year", '2012')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-month", '12')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-day", '31')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-hour", '5')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-minute", '15')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-ampm", 'am')->isSelected());
510 511

    // Test the widget using increment other than 1 and 24 hour mode.
512
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
513
      ->setComponent($field_name, [
514
        'type' => 'datetime_datelist',
515
        'settings' => [
516 517 518
          'increment' => 15,
          'date_order' => 'YMD',
          'time_type' => '24',
519 520
        ],
      ])
521
      ->save();
522
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
523 524 525 526 527

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

    // Other elements are unaffected by the changed settings.
528
    $this->assertSession()->elementExists('xpath', "//*[@id=\"edit-$field_name-0-value-hour\"]");
529
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-hour", '')->isSelected());
530
    $this->assertSession()->elementNotExists('xpath', "//*[@id=\"edit-$field_name-0-value-ampm\"]");
531
    // Submit a valid date and ensure it is accepted.
532
    $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
533

534
    $edit = [];
535 536 537 538 539
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
540
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
541
    $id = $match[1];
542
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
543

544 545 546 547 548
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-year", '2012')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-month", '12')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-day", '31')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-hour", '17')->isSelected());
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-minute", '15')->isSelected());
549 550

    // Test the widget for partial completion of fields.
551
    $display_repository->getFormDisplay($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle())
552
      ->setComponent($field_name, [
553
        'type' => 'datetime_datelist',
554
        'settings' => [
555 556 557
          'increment' => 1,
          'date_order' => 'YMD',
          'time_type' => '24',
558 559
        ],
      ])
560
      ->save();
561
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
562 563

    // Test the widget for validation notifications.
564
    foreach ($this->datelistDataProvider($field_label) as $data) {
565 566 567 568 569 570
      list($date_value, $expected) = $data;

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

      // Submit a partial date and ensure and error message is provided.
571
      $edit = [];
572 573 574 575 576
      foreach ($date_value as $part => $value) {
        $edit["{$field_name}[0][value][$part]"] = $value;
      }

      $this->drupalPostForm(NULL, $edit, t('Save'));
577
      $this->assertSession()->statusCodeEquals(200);
578 579 580
      foreach ($expected as $expected_text) {
        $this->assertText(t($expected_text));
      }
581 582 583 584 585
    }

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

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

    $this->drupalPostForm(NULL, $edit, t('Save'));
593
    $this->assertSession()->statusCodeEquals(200);
594
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
595
    $id = $match[1];
596
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
597 598 599 600

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

601 602
    $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => '0'];
    $edit = [];
603 604 605 606 607
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
608
    $this->assertSession()->statusCodeEquals(200);
609
    $this->assertTrue($this->assertSession()->optionExists("edit-$field_name-0-value-minute", '0')->isSelected());
610
  }
611 612 613 614

  /**
   * The data provider for testing the validation of the datelist widget.
   *
615 616 617
   * @param string $field_label
   *   The label of the field being tested.
   *
618 619 620
   * @return array
   *   An array of datelist input permutations to test.
   */
621
  protected function datelistDataProvider($field_label) {
622
    return [
623
      // Nothing selected.
624 625 626 627
      [
        ['year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
        ["The $field_label date is required."],
      ],
628
      // Year only selected, validation error on Month, Day, Hour, Minute.
629 630 631 632 633 634 635 636 637 638
      [
        ['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.',
        ],
      ],
639
      // Year and Month selected, validation error on Day, Hour, Minute.
640 641 642 643 644 645 646 647 648
      [
        ['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.',
        ],
      ],
649
      // Year, Month and Day selected, validation error on Hour, Minute.
650 651 652 653 654 655 656 657
      [
        ['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.',
        ],
      ],
658
      // Year, Month, Day and Hour selected, validation error on Minute only.
659 660 661 662 663 664 665
      [
        ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for minute.',
        ],
      ],
666
    ];
667 668 669 670 671
  }

  /**
   * Test default value functionality.
   */
672
  public function testDefaultValue() {
673
    // Create a test content type.
674
    $this->drupalCreateContentType(['type' => 'date_content']);
675

676
    // Create a field storage with settings to validate.
677
    $field_name = mb_strtolower($this->randomMachineName());
678
    $field_storage = FieldStorageConfig::create([
679
      'field_name' => $field_name,
680 681
      'entity_type' => 'node',
      'type' => 'datetime',
682 683
      'settings' => ['datetime_type' => 'date'],
    ]);
684
    $field_storage->save();
685

686
    $field = FieldConfig::create([
687
      'field_storage' => $field_storage,
688
      'bundle' => 'date_content',
689
    ]);
690
    $field->save();
691

692 693 694
    // Loop through defined timezones to test that date-only defaults work at
    // the extremes.
    foreach (static::$timezones as $timezone) {
695

696
      $this->setSiteTimezone($timezone);
697
      $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
698

699
      // Set now as default_value.
700
      $field_edit = [
701
        'default_value_input[default_date_type]' => 'now',
702
      ];
703 704 705 706
      $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);
707
      $this->assertTrue($this->assertSession()->optionExists('edit-default-value-input-default-date-type', 'now')->isSelected());
708 709
      // Check that the relative default value is empty.
      $this->assertSession()->fieldValueEquals('default_value_input[default_date]', '');
710 711 712 713

      // Check if default_date has been stored successfully.
      $config_entity = $this->config('field.field.node.date_content.' . $field_name)
        ->get();
714
      $this->assertEqual($config_entity['default_value'][0], [
715 716
        'default_date_type' => 'now',
        'default_date' => 'now',
717
      ], 'Default value has been stored successfully');
718 719

      // Clear field cache in order to avoid stale cache values.
720
      \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
721 722 723

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

      // Set an invalid relative default_value to test validation.
729
      $field_edit = [
730 731
        'default_value_input[default_date_type]' => 'relative',
        'default_value_input[default_date]' => 'invalid date',
732
      ];
733
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
734

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

737
      // Set a relative default_value.
738
      $field_edit = [
739 740