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

3
namespace Drupal\Tests\datetime\Functional;
4

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

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

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

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

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

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 111 112 113 114 115 116 117
          entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
            ->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.
118 119
              $expected = format_date($date->getTimestamp(), $new_value, '', DateTimeItemInterface::STORAGE_TIMEZONE);
              $expected_iso = format_date($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DateTimeItemInterface::STORAGE_TIMEZONE);
120 121
              $output = $this->renderTestEntity($id);
              $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
122 123 124 125 126 127
              $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,
              ]));
128 129
              break;
          }
130 131 132
        }
      }

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

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

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

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

      // 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
      // or user timezome.
      $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();

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

  /**
   * Tests date and time field.
   */
231
  public function testDatetimeField() {
232
    $field_name = $this->fieldStorage->getName();
233
    $field_label = $this->field->label();
234
    // Change the field to a datetime field.
235
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
236
    $this->fieldStorage->save();
237 238

    // Display creation form.
239
    $this->drupalGet('entity_test/add');
240 241
    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
242
    $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
243 244
    $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');
245

246
    // Build up a date in the UTC timezone.
247
    $value = '2012-12-31 00:00:00';
248 249 250 251 252 253
    $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.
254 255
    $date_format = DateFormat::load('html_date')->getPattern();
    $time_format = DateFormat::load('html_time')->getPattern();
256

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

    // Verify that the date is output according to the formatter settings.
269 270 271
    $options = [
      'format_type' => ['short', 'medium', 'long'],
    ];
272 273 274
    foreach ($options as $setting => $values) {
      foreach ($values as $new_value) {
        // Update the entity display settings.
275
        $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
276
        entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
277
          ->setComponent($field_name, $this->displayOptions)
278 279 280 281 282 283 284
          ->save();

        $this->renderTestEntity($id);
        switch ($setting) {
          case 'format_type':
            // Verify that a date is displayed.
            $expected = format_date($date->getTimestamp(), $new_value);
285
            $expected_iso = format_date($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
286 287 288
            $output = $this->renderTestEntity($id);
            $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
            $this->assertContains($expected_markup, $output, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => $new_value, '%expected' => $expected, '%expected_iso' => $expected_iso]));
289 290 291 292 293 294
            break;
        }
      }
    }

    // Verify that the plain formatter works.
295
    $this->displayOptions['type'] = 'datetime_plain';
296
    $this->displayOptions['settings'] = $this->defaultSettings;
297
    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
298
      ->setComponent($field_name, $this->displayOptions)
299
      ->save();
300
    $expected = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
301 302
    $output = $this->renderTestEntity($id);
    $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', ['%expected' => $expected]));
303 304 305

    // Verify that the 'datetime_custom' formatter works.
    $this->displayOptions['type'] = 'datetime_custom';
306
    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
307 308 309 310
    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
      ->setComponent($field_name, $this->displayOptions)
      ->save();
    $expected = $date->format($this->displayOptions['settings']['date_format']);
311 312
    $output = $this->renderTestEntity($id);
    $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
313 314 315

    // Verify that the 'timezone_override' setting works.
    $this->displayOptions['type'] = 'datetime_custom';
316
    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
317 318 319
    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
      ->setComponent($field_name, $this->displayOptions)
      ->save();
320
    $expected = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
321 322
    $output = $this->renderTestEntity($id);
    $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
323 324 325 326 327 328 329

    // 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
    // or user timezome.
    $timestamp = REQUEST_TIME - 87654321;
330
    $entity = EntityTest::load($id);
331 332
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
333
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
334 335 336
    $entity->save();

    $this->displayOptions['type'] = 'datetime_time_ago';
337
    $this->displayOptions['settings'] = [
338 339 340
      'future_format' => '@interval from now',
      'past_format' => '@interval earlier',
      'granularity' => 3,
341
    ];
342 343 344 345
    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
      ->setComponent($field_name, $this->displayOptions)
      ->save();
    $expected = SafeMarkup::format($this->displayOptions['settings']['past_format'], [
346
      '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
347
    ]);
348 349
    $output = $this->renderTestEntity($id);
    $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
350 351 352 353 354 355 356

    // 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
    // or user timezome.
    $timestamp = REQUEST_TIME + 87654321;
357
    $entity = EntityTest::load($id);
358 359
    $field_name = $this->fieldStorage->getName();
    $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
360
    $entity->{$field_name}->value = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
361 362 363 364 365 366
    $entity->save();

    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
      ->setComponent($field_name, $this->displayOptions)
      ->save();
    $expected = SafeMarkup::format($this->displayOptions['settings']['future_format'], [
367
      '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
368
    ]);
369 370
    $output = $this->renderTestEntity($id);
    $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
371 372 373 374 375
  }

  /**
   * Tests Date List Widget functionality.
   */
376
  public function testDatelistWidget() {
377
    $field_name = $this->fieldStorage->getName();
378
    $field_label = $this->field->label();
379 380 381 382 383 384 385

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

    // Change the widget to a datelist widget.
    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
386
      ->setComponent($field_name, [
387
        'type' => 'datetime_datelist',
388
        'settings' => [
389
          'date_order' => 'YMD',
390 391
        ],
      ])
392 393 394 395 396
      ->save();
    \Drupal::entityManager()->clearCachedFieldDefinitions();

    // Display creation form.
    $this->drupalGet('entity_test/add');
397
    $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
398 399
    $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');
400 401 402 403 404 405 406 407 408 409

    // 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.
410
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
411 412 413
    $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.');

414
    // Change the field to a datetime field.
415
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
416
    $this->fieldStorage->save();
417 418

    // Change the widget to a datelist widget.
419
    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
420
      ->setComponent($field_name, [
421
        'type' => 'datetime_datelist',
422
        'settings' => [
423 424 425
          'increment' => 1,
          'date_order' => 'YMD',
          'time_type' => '12',
426 427
        ],
      ])
428
      ->save();
429
    \Drupal::entityManager()->clearCachedFieldDefinitions();
430

431 432 433 434 435
    // 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.
436
    $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
437 438
    $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');

439
    // Display creation form.
440
    $this->drupalGet('entity_test/add');
441 442 443

    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-year\"]", NULL, 'Year element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-year", '', 'No year selected.');
444
    $this->assertOptionByText("edit-$field_name-0-value-year", t('Year'));
445 446
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-month\"]", NULL, 'Month element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-month", '', 'No month selected.');
447
    $this->assertOptionByText("edit-$field_name-0-value-month", t('Month'));
448 449
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-day\"]", NULL, 'Day element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-day", '', 'No day selected.');
450
    $this->assertOptionByText("edit-$field_name-0-value-day", t('Day'));
451 452
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
453
    $this->assertOptionByText("edit-$field_name-0-value-hour", t('Hour'));
454 455
    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element found.');
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '', 'No minute selected.');
456
    $this->assertOptionByText("edit-$field_name-0-value-minute", t('Minute'));
457 458 459
    $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.');
460
    $this->assertOptionByText("edit-$field_name-0-value-ampm", t('AM/PM'));
461 462

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

465
    $edit = [];
466 467 468
    // Add the ampm indicator since we are testing 12 hour time.
    $date_value['ampm'] = 'am';
    foreach ($date_value as $part => $value) {
469
      $edit["{$field_name}[0][value][$part]"] = $value;
470 471
    }

472
    $this->drupalPostForm(NULL, $edit, t('Save'));
473
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
474
    $id = $match[1];
475
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
476

477 478 479 480 481 482
    $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.');
483 484 485

    // Test the widget using increment other than 1 and 24 hour mode.
    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
486
      ->setComponent($field_name, [
487
        'type' => 'datetime_datelist',
488
        'settings' => [
489 490 491
          'increment' => 15,
          'date_order' => 'YMD',
          'time_type' => '24',
492 493
        ],
      ])
494 495 496 497 498 499 500 501 502 503 504 505
      ->save();
    \Drupal::entityManager()->clearCachedFieldDefinitions();

    // 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.
506
    $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
507

508
    $edit = [];
509 510 511 512 513
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
514
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
515
    $id = $match[1];
516
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
517 518 519 520 521 522

    $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.');
523 524 525

    // Test the widget for partial completion of fields.
    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
526
      ->setComponent($field_name, [
527
        'type' => 'datetime_datelist',
528
        'settings' => [
529 530 531
          'increment' => 1,
          'date_order' => 'YMD',
          'time_type' => '24',
532 533
        ],
      ])
534 535 536 537
      ->save();
    \Drupal::entityManager()->clearCachedFieldDefinitions();

    // Test the widget for validation notifications.
538
    foreach ($this->datelistDataProvider($field_label) as $data) {
539 540 541 542 543 544
      list($date_value, $expected) = $data;

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

      // Submit a partial date and ensure and error message is provided.
545
      $edit = [];
546 547 548 549 550 551
      foreach ($date_value as $part => $value) {
        $edit["{$field_name}[0][value][$part]"] = $value;
      }

      $this->drupalPostForm(NULL, $edit, t('Save'));
      $this->assertResponse(200);
552 553 554
      foreach ($expected as $expected_text) {
        $this->assertText(t($expected_text));
      }
555 556 557 558 559
    }

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

560 561
    $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'];
    $edit = [];
562 563 564 565 566 567
    foreach ($date_value as $part => $value) {
      $edit["{$field_name}[0][value][$part]"] = $value;
    }

    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertResponse(200);
568
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
569
    $id = $match[1];
570
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
571 572 573 574

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

575 576
    $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => '0'];
    $edit = [];
577 578 579 580 581 582 583
    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.');
584
  }
585 586 587 588

  /**
   * The data provider for testing the validation of the datelist widget.
   *
589 590 591
   * @param string $field_label
   *   The label of the field being tested.
   *
592 593 594
   * @return array
   *   An array of datelist input permutations to test.
   */
595
  protected function datelistDataProvider($field_label) {
596
    return [
597
      // Nothing selected.
598 599 600 601
      [
        ['year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
        ["The $field_label date is required."],
      ],
602
      // Year only selected, validation error on Month, Day, Hour, Minute.
603 604 605 606 607 608 609 610 611 612
      [
        ['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.',
        ],
      ],
613
      // Year and Month selected, validation error on Day, Hour, Minute.
614 615 616 617 618 619 620 621 622
      [
        ['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.',
        ],
      ],
623
      // Year, Month and Day selected, validation error on Hour, Minute.
624 625 626 627 628 629 630 631
      [
        ['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.',
        ],
      ],
632
      // Year, Month, Day and Hour selected, validation error on Minute only.
633 634 635 636 637 638 639
      [
        ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
        [
          "The $field_label date is incomplete.",
          'A value must be selected for minute.',
        ],
      ],
640
    ];
641 642 643 644 645
  }

  /**
   * Test default value functionality.
   */
646
  public function testDefaultValue() {
647
    // Create a test content type.
648
    $this->drupalCreateContentType(['type' => 'date_content']);
649

650
    // Create a field storage with settings to validate.
651
    $field_name = Unicode::strtolower($this->randomMachineName());
652
    $field_storage = FieldStorageConfig::create([
653
      'field_name' => $field_name,
654 655
      'entity_type' => 'node',
      'type' => 'datetime',
656 657
      'settings' => ['datetime_type' => 'date'],
    ]);
658
    $field_storage->save();
659

660
    $field = FieldConfig::create([
661
      'field_storage' => $field_storage,
662
      'bundle' => 'date_content',
663
    ]);
664
    $field->save();
665

666 667 668
    // Loop through defined timezones to test that date-only defaults work at
    // the extremes.
    foreach (static::$timezones as $timezone) {
669

670
      $this->setSiteTimezone($timezone);
671
      $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
672

673
      // Set now as default_value.
674
      $field_edit = [
675
        'default_value_input[default_date_type]' => 'now',
676
      ];
677 678 679 680 681 682 683 684 685 686
      $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();
687
      $this->assertEqual($config_entity['default_value'][0], [
688 689
        'default_date_type' => 'now',
        'default_date' => 'now',
690
      ], 'Default value has been stored successfully');
691 692 693 694 695 696 697 698

      // Clear field cache in order to avoid stale cache values.
      \Drupal::entityManager()->clearCachedFieldDefinitions();

      // 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)
699
        ->offsetGet(0)->value, $expected_date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT));
700 701

      // Set an invalid relative default_value to test validation.
702
      $field_edit = [
703 704
        'default_value_input[default_date_type]' => 'relative',
        'default_value_input[default_date]' => 'invalid date',
705
      ];
706
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
707

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

710
      // Set a relative default_value.
711
      $field_edit = [
712 713
        'default_value_input[default_date_type]' => 'relative',
        'default_value_input[default_date]' => '+90 days',
714
      ];
715 716 717 718 719 720 721 722 723 724
      $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();
725
      $this->assertEqual($config_entity['default_value'][0], [
726 727
        'default_date_type' => 'relative',
        'default_date' => '+90 days',
728
      ], 'Default value has been stored successfully');
729 730 731 732 733 734 735 736 737

      // Clear field cache in order to avoid stale cache values.
      \Drupal::entityManager()->clearCachedFieldDefinitions();

      // 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)
738
        ->offsetGet(0)->value, $expected_date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT));
739 740

      // Remove default value.
741
      $field_edit = [
742
        'default_value_input[default_date_type]' => '',
743
      ];
744
      $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
745

746 747 748 749
      // 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');
750

751 752 753 754
      // 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');
755

756 757
      // Clear field cache in order to avoid stale cache values.
      \Drupal::entityManager()->clearCachedFieldDefinitions();
758

759 760 761 762 763
      // 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');
    }
764 765 766 767 768
  }

  /**
   * Test that invalid values are caught and marked as invalid.
   */
769
  public function testInvalidField() {
770
    // Change the field to a datetime field.
771
    $this->fieldStorage->setSetting('datetime_type', 'datetime');
772
    $this->fieldStorage->save();
773
    $field_name = $this->fieldStorage->getName();
774 775

    // Display creation form.
776
    $this->drupalGet('entity_test/add');
777 778
    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
779 780 781

    // Submit invalid dates and ensure they is not accepted.
    $date_value = '';
782
    $edit = [
783 784
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '12:00:00',
785
    ];
786
    $this->drupalPostForm(NULL, $edit, t('Save'));
787 788 789
    $this->assertText('date is invalid', 'Empty date value has been caught.');

    $date_value = 'aaaa-12-01';
790
    $edit = [
791 792
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '00:00:00',
793
    ];
794
    $this->drupalPostForm(NULL, $edit, t('Save'));
795
    $this->assertText('date is invalid', format_string('Invalid year value %date has been caught.', ['%date' => $date_value]));
796 797

    $date_value = '2012-75-01';
798
    $edit = [
799 800
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => '00:00:00',
801
    ];
802
    $this->drupalPostForm(NULL, $edit, t('Save'));
803
    $this->assertText('date is invalid', format_string('Invalid month value %date has been caught.', ['%date' => $date_value]));
804 805

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

    $date_value = '2012-12-01';
    $time_value = '';
815
    $edit = [
816 817
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
818
    ];
819
    $this->drupalPostForm(NULL, $edit, t('Save'));
820 821 822 823
    $this->assertText('date is invalid', 'Empty time value has been caught.');

    $date_value = '2012-12-01';
    $time_value = '49:00:00';
824
    $edit = [
825 826
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
827
    ];
828
    $this->drupalPostForm(NULL, $edit, t('Save'));
829
    $this->assertText('date is invalid', format_string('Invalid hour value %time has been caught.', ['%time' => $time_value]));
830 831 832

    $date_value = '2012-12-01';
    $time_value = '12:99:00';
833
    $edit = [
834 835
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
836
    ];
837
    $this->drupalPostForm(NULL, $edit, t('Save'));
838
    $this->assertText('date is invalid', format_string('Invalid minute value %time has been caught.', ['%time' => $time_value]));
839 840 841

    $date_value = '2012-12-01';
    $time_value = '12:15:99';
842
    $edit = [
843 844
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
845
    ];
846
    $this->drupalPostForm(NULL, $edit, t('Save'));