DateTimeFieldTest.php 41.6 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
              $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);
118 119
              $output = $this->renderTestEntity($id);
              $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
120 121 122 123 124 125
              $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,
              ]));
126 127
              break;
          }
128 129 130
        }
      }

131 132 133 134 135 136
      // 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();
137
      $expected = $date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
138
      $output = $this->renderTestEntity($id);
139 140 141 142
      $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using plain format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
143 144 145

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

      // 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))';
164
      $output = $this->renderTestEntity($id);
165 166 167 168
      $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
169 170 171 172 173

      // 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
174
      // or user timezone.
175 176 177 178 179 180 181 182
      $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';
183
      $this->displayOptions['settings'] = [
184 185 186
        'future_format' => '@interval in the future',
        'past_format' => '@interval in the past',
        'granularity' => 3,
187
      ];
188 189 190
      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
        ->setComponent($field_name, $this->displayOptions)
        ->save();
191
      $expected = new FormattableMarkup($this->displayOptions['settings']['past_format'], [
192
        '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
193
      ]);
194
      $output = $this->renderTestEntity($id);
195 196 197 198
      $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
199 200 201 202 203

      // 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
204
      // or user timezone.
205 206 207 208 209 210 211 212 213 214
      $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();
215
      $expected = new FormattableMarkup($this->displayOptions['settings']['future_format'], [
216
        '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]),
217
      ]);
218
      $output = $this->renderTestEntity($id);
219 220 221 222
      $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
        '%expected' => $expected,
        '%timezone' => $timezone,
      ]));
223
    }
224 225 226 227 228
  }

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

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

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

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

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

        $this->renderTestEntity($id);
        switch ($setting) {
          case 'format_type':
            // Verify that a date is displayed.
            $expected = format_date($date->getTimestamp(), $new_value);
283
            $expected_iso = format_date($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
284 285
            $output = $this->renderTestEntity($id);
            $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
286
            $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]));
287 288 289 290 291 292
            break;
        }
      }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $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.');
521 522 523

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

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

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

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

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

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

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

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

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

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

    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertResponse(200);
    $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
582
  }
583 584 585 586

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

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

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

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

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

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

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

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

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

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

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

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

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

744 745 746 747
      // 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');
748

749 750 751 752
      // 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');
753

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

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

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

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

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

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

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

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

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

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