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

3
namespace Drupal\Tests\datetime\Functional;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $date_value = '2012-12-01';
    $time_value = '12:99:00';
847
    $edit = [