DateTimeFieldTest.php 41.9 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 43
    // Loop through defined timezones to test that date-only fields work at the
    // extremes.
    foreach (static::$timezones as $timezone) {
44

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

48 49 50
      // Display creation form.
      $this->drupalGet('entity_test/add');
      $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
51
      $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]//label[contains(@class,"js-form-required")]', TRUE, 'Required markup found');
52
      $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Time element not found.');
53 54
      $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');
55 56 57 58 59

      // 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';
60
      $date = new DrupalDateTime($value, DateTimeItemInterface::STORAGE_TIMEZONE);
61 62 63 64 65

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

66
      $edit = [
67
        "{$field_name}[0][value][date]" => $date->format($date_format),
68
      ];
69
      $this->drupalPostForm(NULL, $edit, t('Save'));
70
      preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
71
      $id = $match[1];
72
      $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
      $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.
90
      $this->displayOptions = [
91 92
        'type' => 'datetime_default',
        'label' => 'hidden',
93 94
        'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
      ];
95
      // Verify that the date is output according to the formatter settings.
96 97 98
      $options = [
        'format_type' => ['short', 'medium', 'long'],
      ];
99 100 101
      // Formats that display a time component for date-only fields will display
      // the default time, so that is applied before calculating the expected
      // value.
102
      $this->massageTestDate($date);
103 104 105
      foreach ($options as $setting => $values) {
        foreach ($values as $new_value) {
          // Update the entity display settings.
106
          $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
107 108 109 110 111 112 113 114 115
          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.
116 117 118 119
              /** @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);
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

      // 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
176
      // or user timezone.
177 178 179 180 181 182 183 184
      $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
      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
        ->setComponent($field_name, $this->displayOptions)
        ->save();
193
      $expected = new FormattableMarkup($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

      // 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
206
      // or user timezone.
207 208 209 210 211 212 213 214 215 216
      $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();
217
      $expected = new FormattableMarkup($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
          ->save();

        $this->renderTestEntity($id);
        switch ($setting) {
          case 'format_type':
            // Verify that a date is displayed.
284 285 286
            $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');
287 288
            $output = $this->renderTestEntity($id);
            $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
289
            $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]));
290 291 292 293 294 295
            break;
        }
      }
    }

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

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

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

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

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

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

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

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

    // 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')
387
      ->setComponent($field_name, [
388
        'type' => 'datetime_datelist',
389
        'settings' => [
390
          'date_order' => 'YMD',
391 392
        ],
      ])
393 394 395 396 397
      ->save();
    \Drupal::entityManager()->clearCachedFieldDefinitions();

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

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

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

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

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

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

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

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

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

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

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

    // Test the widget using increment other than 1 and 24 hour mode.
    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
487
      ->setComponent($field_name, [
488
        'type' => 'datetime_datelist',
489
        'settings' => [
490 491 492
          'increment' => 15,
          'date_order' => 'YMD',
          'time_type' => '24',
493 494
        ],
      ])
495 496 497 498 499 500 501 502 503 504 505 506
      ->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.
507
    $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
508

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $date_value = '2012-12-01';
    $time_value = '49:00:00';
825
    $edit = [
826 827
      "{$field_name}[0][value][date]" => $date_value,
      "{$field_name}[0][value][time]" => $time_value,
828
    ];
829
    $this->drupalPostForm(NULL, $edit, t('Save'));
830
    $this->assertText('date is invalid'