Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / datetime / tests / src / Functional / DateTimeFieldTest.php
1 <?php
2
3 namespace Drupal\Tests\datetime\Functional;
4
5 use Drupal\Component\Render\FormattableMarkup;
6 use Drupal\Component\Utility\SafeMarkup;
7 use Drupal\Component\Utility\Unicode;
8 use Drupal\Core\Datetime\DrupalDateTime;
9 use Drupal\Core\Datetime\Entity\DateFormat;
10 use Drupal\entity_test\Entity\EntityTest;
11 use Drupal\field\Entity\FieldConfig;
12 use Drupal\field\Entity\FieldStorageConfig;
13 use Drupal\node\Entity\Node;
14
15 /**
16  * Tests Datetime field functionality.
17  *
18  * @group datetime
19  */
20 class DateTimeFieldTest extends DateTestBase {
21
22   /**
23    * The default display settings to use for the formatters.
24    *
25    * @var array
26    */
27   protected $defaultSettings = ['timezone_override' => ''];
28
29   /**
30    * {@inheritdoc}
31    */
32   protected function getTestFieldType() {
33     return 'datetime';
34   }
35
36   /**
37    * Tests date field functionality.
38    */
39   public function testDateField() {
40     $field_name = $this->fieldStorage->getName();
41
42     // Loop through defined timezones to test that date-only fields work at the
43     // extremes.
44     foreach (static::$timezones as $timezone) {
45
46       $this->setSiteTimezone($timezone);
47       $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
48
49       // Display creation form.
50       $this->drupalGet('entity_test/add');
51       $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
52       $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]//label[contains(@class,"js-form-required")]', TRUE, 'Required markup found');
53       $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Time element not found.');
54       $this->assertFieldByXPath('//input[@aria-describedby="edit-' . $field_name . '-0-value--description"]', NULL, 'ARIA described-by found');
55       $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0-value--description"]', NULL, 'ARIA description found');
56
57       // Build up a date in the UTC timezone. Note that using this will also
58       // mimic the user in a different timezone simply entering '2012-12-31' via
59       // the UI.
60       $value = '2012-12-31 00:00:00';
61       $date = new DrupalDateTime($value, DATETIME_STORAGE_TIMEZONE);
62
63       // Submit a valid date and ensure it is accepted.
64       $date_format = DateFormat::load('html_date')->getPattern();
65       $time_format = DateFormat::load('html_time')->getPattern();
66
67       $edit = [
68         "{$field_name}[0][value][date]" => $date->format($date_format),
69       ];
70       $this->drupalPostForm(NULL, $edit, t('Save'));
71       preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
72       $id = $match[1];
73       $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
74       $this->assertRaw($date->format($date_format));
75       $this->assertNoRaw($date->format($time_format));
76
77       // Verify the date doesn't change if using a timezone that is UTC+12 when
78       // the entity is edited through the form.
79       $entity = EntityTest::load($id);
80       $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
81       $this->drupalGet('entity_test/manage/' . $id . '/edit');
82       $this->drupalPostForm(NULL, [], t('Save'));
83       $this->drupalGet('entity_test/manage/' . $id . '/edit');
84       $this->drupalPostForm(NULL, [], t('Save'));
85       $this->drupalGet('entity_test/manage/' . $id . '/edit');
86       $this->drupalPostForm(NULL, [], t('Save'));
87       $entity = EntityTest::load($id);
88       $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
89
90       // Reset display options since these get changed below.
91       $this->displayOptions = [
92         'type' => 'datetime_default',
93         'label' => 'hidden',
94         'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
95       ];
96       // Verify that the date is output according to the formatter settings.
97       $options = [
98         'format_type' => ['short', 'medium', 'long'],
99       ];
100       // Formats that display a time component for date-only fields will display
101       // the default time, so that is applied before calculating the expected
102       // value.
103       datetime_date_default_time($date);
104       foreach ($options as $setting => $values) {
105         foreach ($values as $new_value) {
106           // Update the entity display settings.
107           $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
108           entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
109             ->setComponent($field_name, $this->displayOptions)
110             ->save();
111
112           $this->renderTestEntity($id);
113           switch ($setting) {
114             case 'format_type':
115               // Verify that a date is displayed. Since this is a date-only
116               // field, it is expected to display the time as 00:00:00.
117               $expected = format_date($date->getTimestamp(), $new_value, '', DATETIME_STORAGE_TIMEZONE);
118               $expected_iso = format_date($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
119               $output = $this->renderTestEntity($id);
120               $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
121               $this->assertContains($expected_markup, $output, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute in %timezone.', [
122                 '%value' => $new_value,
123                 '%expected' => $expected,
124                 '%expected_iso' => $expected_iso,
125                 '%timezone' => $timezone,
126               ]));
127               break;
128           }
129         }
130       }
131
132       // Verify that the plain formatter works.
133       $this->displayOptions['type'] = 'datetime_plain';
134       $this->displayOptions['settings'] = $this->defaultSettings;
135       entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
136         ->setComponent($field_name, $this->displayOptions)
137         ->save();
138       $expected = $date->format(DATETIME_DATE_STORAGE_FORMAT);
139       $output = $this->renderTestEntity($id);
140       $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using plain format displayed as %expected in %timezone.', [
141         '%expected' => $expected,
142         '%timezone' => $timezone,
143       ]));
144
145       // Verify that the 'datetime_custom' formatter works.
146       $this->displayOptions['type'] = 'datetime_custom';
147       $this->displayOptions['settings'] = ['date_format' => 'm/d/Y'] + $this->defaultSettings;
148       entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
149         ->setComponent($field_name, $this->displayOptions)
150         ->save();
151       $expected = $date->format($this->displayOptions['settings']['date_format']);
152       $output = $this->renderTestEntity($id);
153       $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using datetime_custom format displayed as %expected in %timezone.', [
154         '%expected' => $expected,
155         '%timezone' => $timezone,
156       ]));
157
158       // Test that allowed markup in custom format is preserved and XSS is
159       // removed.
160       $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\\>';
161       entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
162         ->setComponent($field_name, $this->displayOptions)
163         ->save();
164       $expected = '<strong>' . $date->format('m/d/Y') . '</strong>alert(String.fromCharCode(88,83,83))';
165       $output = $this->renderTestEntity($id);
166       $this->assertContains($expected, $output, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected in %timezone.', [
167         '%expected' => $expected,
168         '%timezone' => $timezone,
169       ]));
170
171       // Verify that the 'datetime_time_ago' formatter works for intervals in the
172       // past.  First update the test entity so that the date difference always
173       // has the same interval.  Since the database always stores UTC, and the
174       // interval will use this, force the test date to use UTC and not the local
175       // or user timezome.
176       $timestamp = REQUEST_TIME - 87654321;
177       $entity = EntityTest::load($id);
178       $field_name = $this->fieldStorage->getName();
179       $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
180       $entity->{$field_name}->value = $date->format($date_format);
181       $entity->save();
182
183       $this->displayOptions['type'] = 'datetime_time_ago';
184       $this->displayOptions['settings'] = [
185         'future_format' => '@interval in the future',
186         'past_format' => '@interval in the past',
187         'granularity' => 3,
188       ];
189       entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
190         ->setComponent($field_name, $this->displayOptions)
191         ->save();
192       $expected = SafeMarkup::format($this->displayOptions['settings']['past_format'], [
193         '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
194       ]);
195       $output = $this->renderTestEntity($id);
196       $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
197         '%expected' => $expected,
198         '%timezone' => $timezone,
199       ]));
200
201       // Verify that the 'datetime_time_ago' formatter works for intervals in the
202       // future.  First update the test entity so that the date difference always
203       // has the same interval.  Since the database always stores UTC, and the
204       // interval will use this, force the test date to use UTC and not the local
205       // or user timezome.
206       $timestamp = REQUEST_TIME + 87654321;
207       $entity = EntityTest::load($id);
208       $field_name = $this->fieldStorage->getName();
209       $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
210       $entity->{$field_name}->value = $date->format($date_format);
211       $entity->save();
212
213       entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
214         ->setComponent($field_name, $this->displayOptions)
215         ->save();
216       $expected = SafeMarkup::format($this->displayOptions['settings']['future_format'], [
217         '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
218       ]);
219       $output = $this->renderTestEntity($id);
220       $this->assertContains((string) $expected, $output, new FormattableMarkup('Formatted date field using datetime_time_ago format displayed as %expected in %timezone.', [
221         '%expected' => $expected,
222         '%timezone' => $timezone,
223       ]));
224     }
225   }
226
227   /**
228    * Tests date and time field.
229    */
230   public function testDatetimeField() {
231     $field_name = $this->fieldStorage->getName();
232     $field_label = $this->field->label();
233     // Change the field to a datetime field.
234     $this->fieldStorage->setSetting('datetime_type', 'datetime');
235     $this->fieldStorage->save();
236
237     // Display creation form.
238     $this->drupalGet('entity_test/add');
239     $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
240     $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
241     $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
242     $this->assertFieldByXPath('//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]', NULL, 'ARIA described-by found');
243     $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0--description"]', NULL, 'ARIA description found');
244
245     // Build up a date in the UTC timezone.
246     $value = '2012-12-31 00:00:00';
247     $date = new DrupalDateTime($value, 'UTC');
248
249     // Update the timezone to the system default.
250     $date->setTimezone(timezone_open(drupal_get_user_timezone()));
251
252     // Submit a valid date and ensure it is accepted.
253     $date_format = DateFormat::load('html_date')->getPattern();
254     $time_format = DateFormat::load('html_time')->getPattern();
255
256     $edit = [
257       "{$field_name}[0][value][date]" => $date->format($date_format),
258       "{$field_name}[0][value][time]" => $date->format($time_format),
259     ];
260     $this->drupalPostForm(NULL, $edit, t('Save'));
261     preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
262     $id = $match[1];
263     $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
264     $this->assertRaw($date->format($date_format));
265     $this->assertRaw($date->format($time_format));
266
267     // Verify that the date is output according to the formatter settings.
268     $options = [
269       'format_type' => ['short', 'medium', 'long'],
270     ];
271     foreach ($options as $setting => $values) {
272       foreach ($values as $new_value) {
273         // Update the entity display settings.
274         $this->displayOptions['settings'] = [$setting => $new_value] + $this->defaultSettings;
275         entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
276           ->setComponent($field_name, $this->displayOptions)
277           ->save();
278
279         $this->renderTestEntity($id);
280         switch ($setting) {
281           case 'format_type':
282             // Verify that a date is displayed.
283             $expected = format_date($date->getTimestamp(), $new_value);
284             $expected_iso = format_date($date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
285             $output = $this->renderTestEntity($id);
286             $expected_markup = '<time datetime="' . $expected_iso . '" class="datetime">' . $expected . '</time>';
287             $this->assertContains($expected_markup, $output, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => $new_value, '%expected' => $expected, '%expected_iso' => $expected_iso]));
288             break;
289         }
290       }
291     }
292
293     // Verify that the plain formatter works.
294     $this->displayOptions['type'] = 'datetime_plain';
295     $this->displayOptions['settings'] = $this->defaultSettings;
296     entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
297       ->setComponent($field_name, $this->displayOptions)
298       ->save();
299     $expected = $date->format(DATETIME_DATETIME_STORAGE_FORMAT);
300     $output = $this->renderTestEntity($id);
301     $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', ['%expected' => $expected]));
302
303     // Verify that the 'datetime_custom' formatter works.
304     $this->displayOptions['type'] = 'datetime_custom';
305     $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
306     entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
307       ->setComponent($field_name, $this->displayOptions)
308       ->save();
309     $expected = $date->format($this->displayOptions['settings']['date_format']);
310     $output = $this->renderTestEntity($id);
311     $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
312
313     // Verify that the 'timezone_override' setting works.
314     $this->displayOptions['type'] = 'datetime_custom';
315     $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
316     entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
317       ->setComponent($field_name, $this->displayOptions)
318       ->save();
319     $expected = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
320     $output = $this->renderTestEntity($id);
321     $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
322
323     // Verify that the 'datetime_time_ago' formatter works for intervals in the
324     // past.  First update the test entity so that the date difference always
325     // has the same interval.  Since the database always stores UTC, and the
326     // interval will use this, force the test date to use UTC and not the local
327     // or user timezome.
328     $timestamp = REQUEST_TIME - 87654321;
329     $entity = EntityTest::load($id);
330     $field_name = $this->fieldStorage->getName();
331     $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
332     $entity->{$field_name}->value = $date->format(DATETIME_DATETIME_STORAGE_FORMAT);
333     $entity->save();
334
335     $this->displayOptions['type'] = 'datetime_time_ago';
336     $this->displayOptions['settings'] = [
337       'future_format' => '@interval from now',
338       'past_format' => '@interval earlier',
339       'granularity' => 3,
340     ];
341     entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
342       ->setComponent($field_name, $this->displayOptions)
343       ->save();
344     $expected = SafeMarkup::format($this->displayOptions['settings']['past_format'], [
345       '@interval' => $this->dateFormatter->formatTimeDiffSince($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
346     ]);
347     $output = $this->renderTestEntity($id);
348     $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
349
350     // Verify that the 'datetime_time_ago' formatter works for intervals in the
351     // future.  First update the test entity so that the date difference always
352     // has the same interval.  Since the database always stores UTC, and the
353     // interval will use this, force the test date to use UTC and not the local
354     // or user timezome.
355     $timestamp = REQUEST_TIME + 87654321;
356     $entity = EntityTest::load($id);
357     $field_name = $this->fieldStorage->getName();
358     $date = DrupalDateTime::createFromTimestamp($timestamp, 'UTC');
359     $entity->{$field_name}->value = $date->format(DATETIME_DATETIME_STORAGE_FORMAT);
360     $entity->save();
361
362     entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
363       ->setComponent($field_name, $this->displayOptions)
364       ->save();
365     $expected = SafeMarkup::format($this->displayOptions['settings']['future_format'], [
366       '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']])
367     ]);
368     $output = $this->renderTestEntity($id);
369     $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
370   }
371
372   /**
373    * Tests Date List Widget functionality.
374    */
375   public function testDatelistWidget() {
376     $field_name = $this->fieldStorage->getName();
377     $field_label = $this->field->label();
378
379     // Ensure field is set to a date only field.
380     $this->fieldStorage->setSetting('datetime_type', 'date');
381     $this->fieldStorage->save();
382
383     // Change the widget to a datelist widget.
384     entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
385       ->setComponent($field_name, [
386         'type' => 'datetime_datelist',
387         'settings' => [
388           'date_order' => 'YMD',
389         ],
390       ])
391       ->save();
392     \Drupal::entityManager()->clearCachedFieldDefinitions();
393
394     // Display creation form.
395     $this->drupalGet('entity_test/add');
396     $this->assertFieldByXPath('//fieldset[@id="edit-' . $field_name . '-0"]/legend', $field_label, 'Fieldset and label found');
397     $this->assertFieldByXPath('//fieldset[@aria-describedby="edit-' . $field_name . '-0--description"]', NULL, 'ARIA described-by found');
398     $this->assertFieldByXPath('//div[@id="edit-' . $field_name . '-0--description"]', NULL, 'ARIA description found');
399
400     // Assert that Hour and Minute Elements do not appear on Date Only
401     $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
402     $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
403
404     // Go to the form display page to assert that increment option does not appear on Date Only
405     $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
406     $this->drupalGet($fieldEditUrl);
407
408     // Click on the widget settings button to open the widget settings form.
409     $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
410     $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
411     $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
412
413     // Change the field to a datetime field.
414     $this->fieldStorage->setSetting('datetime_type', 'datetime');
415     $this->fieldStorage->save();
416
417     // Change the widget to a datelist widget.
418     entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
419       ->setComponent($field_name, [
420         'type' => 'datetime_datelist',
421         'settings' => [
422           'increment' => 1,
423           'date_order' => 'YMD',
424           'time_type' => '12',
425         ],
426       ])
427       ->save();
428     \Drupal::entityManager()->clearCachedFieldDefinitions();
429
430     // Go to the form display page to assert that increment option does appear on Date Time
431     $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
432     $this->drupalGet($fieldEditUrl);
433
434     // Click on the widget settings button to open the widget settings form.
435     $this->drupalPostForm(NULL, [], $field_name . "_settings_edit");
436     $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');
437
438     // Display creation form.
439     $this->drupalGet('entity_test/add');
440
441     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-year\"]", NULL, 'Year element found.');
442     $this->assertOptionSelected("edit-$field_name-0-value-year", '', 'No year selected.');
443     $this->assertOptionByText("edit-$field_name-0-value-year", t('Year'));
444     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-month\"]", NULL, 'Month element found.');
445     $this->assertOptionSelected("edit-$field_name-0-value-month", '', 'No month selected.');
446     $this->assertOptionByText("edit-$field_name-0-value-month", t('Month'));
447     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-day\"]", NULL, 'Day element found.');
448     $this->assertOptionSelected("edit-$field_name-0-value-day", '', 'No day selected.');
449     $this->assertOptionByText("edit-$field_name-0-value-day", t('Day'));
450     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
451     $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
452     $this->assertOptionByText("edit-$field_name-0-value-hour", t('Hour'));
453     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element found.');
454     $this->assertOptionSelected("edit-$field_name-0-value-minute", '', 'No minute selected.');
455     $this->assertOptionByText("edit-$field_name-0-value-minute", t('Minute'));
456     $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-second\"]", NULL, 'Second element not found.');
457     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element found.');
458     $this->assertOptionSelected("edit-$field_name-0-value-ampm", '', 'No ampm selected.');
459     $this->assertOptionByText("edit-$field_name-0-value-ampm", t('AM/PM'));
460
461     // Submit a valid date and ensure it is accepted.
462     $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
463
464     $edit = [];
465     // Add the ampm indicator since we are testing 12 hour time.
466     $date_value['ampm'] = 'am';
467     foreach ($date_value as $part => $value) {
468       $edit["{$field_name}[0][value][$part]"] = $value;
469     }
470
471     $this->drupalPostForm(NULL, $edit, t('Save'));
472     preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
473     $id = $match[1];
474     $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
475
476     $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
477     $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
478     $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
479     $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
480     $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
481     $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
482
483     // Test the widget using increment other than 1 and 24 hour mode.
484     entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
485       ->setComponent($field_name, [
486         'type' => 'datetime_datelist',
487         'settings' => [
488           'increment' => 15,
489           'date_order' => 'YMD',
490           'time_type' => '24',
491         ],
492       ])
493       ->save();
494     \Drupal::entityManager()->clearCachedFieldDefinitions();
495
496     // Display creation form.
497     $this->drupalGet('entity_test/add');
498
499     // Other elements are unaffected by the changed settings.
500     $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
501     $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
502     $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');
503
504     // Submit a valid date and ensure it is accepted.
505     $date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
506
507     $edit = [];
508     foreach ($date_value as $part => $value) {
509       $edit["{$field_name}[0][value][$part]"] = $value;
510     }
511
512     $this->drupalPostForm(NULL, $edit, t('Save'));
513     preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
514     $id = $match[1];
515     $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
516
517     $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
518     $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
519     $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
520     $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
521     $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
522
523     // Test the widget for partial completion of fields.
524     entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
525       ->setComponent($field_name, [
526         'type' => 'datetime_datelist',
527         'settings' => [
528           'increment' => 1,
529           'date_order' => 'YMD',
530           'time_type' => '24',
531         ],
532       ])
533       ->save();
534     \Drupal::entityManager()->clearCachedFieldDefinitions();
535
536     // Test the widget for validation notifications.
537     foreach ($this->datelistDataProvider($field_label) as $data) {
538       list($date_value, $expected) = $data;
539
540       // Display creation form.
541       $this->drupalGet('entity_test/add');
542
543       // Submit a partial date and ensure and error message is provided.
544       $edit = [];
545       foreach ($date_value as $part => $value) {
546         $edit["{$field_name}[0][value][$part]"] = $value;
547       }
548
549       $this->drupalPostForm(NULL, $edit, t('Save'));
550       $this->assertResponse(200);
551       foreach ($expected as $expected_text) {
552         $this->assertText(t($expected_text));
553       }
554     }
555
556     // Test the widget for complete input with zeros as part of selections.
557     $this->drupalGet('entity_test/add');
558
559     $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'];
560     $edit = [];
561     foreach ($date_value as $part => $value) {
562       $edit["{$field_name}[0][value][$part]"] = $value;
563     }
564
565     $this->drupalPostForm(NULL, $edit, t('Save'));
566     $this->assertResponse(200);
567     preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
568     $id = $match[1];
569     $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
570
571     // Test the widget to ensure zeros are not deselected on validation.
572     $this->drupalGet('entity_test/add');
573
574     $date_value = ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => '0'];
575     $edit = [];
576     foreach ($date_value as $part => $value) {
577       $edit["{$field_name}[0][value][$part]"] = $value;
578     }
579
580     $this->drupalPostForm(NULL, $edit, t('Save'));
581     $this->assertResponse(200);
582     $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
583   }
584
585   /**
586    * The data provider for testing the validation of the datelist widget.
587    *
588    * @param string $field_label
589    *   The label of the field being tested.
590    *
591    * @return array
592    *   An array of datelist input permutations to test.
593    */
594   protected function datelistDataProvider($field_label) {
595     return [
596       // Nothing selected.
597       [
598         ['year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
599         ["The $field_label date is required."],
600       ],
601       // Year only selected, validation error on Month, Day, Hour, Minute.
602       [
603         ['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
604         [
605           "The $field_label date is incomplete.",
606           'A value must be selected for month.',
607           'A value must be selected for day.',
608           'A value must be selected for hour.',
609           'A value must be selected for minute.',
610         ],
611       ],
612       // Year and Month selected, validation error on Day, Hour, Minute.
613       [
614         ['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
615         [
616           "The $field_label date is incomplete.",
617           'A value must be selected for day.',
618           'A value must be selected for hour.',
619           'A value must be selected for minute.',
620         ],
621       ],
622       // Year, Month and Day selected, validation error on Hour, Minute.
623       [
624         ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
625         [
626           "The $field_label date is incomplete.",
627           'A value must be selected for hour.',
628           'A value must be selected for minute.',
629         ],
630       ],
631       // Year, Month, Day and Hour selected, validation error on Minute only.
632       [
633         ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
634         [
635           "The $field_label date is incomplete.",
636           'A value must be selected for minute.',
637         ],
638       ],
639     ];
640   }
641
642   /**
643    * Test default value functionality.
644    */
645   public function testDefaultValue() {
646     // Create a test content type.
647     $this->drupalCreateContentType(['type' => 'date_content']);
648
649     // Create a field storage with settings to validate.
650     $field_name = Unicode::strtolower($this->randomMachineName());
651     $field_storage = FieldStorageConfig::create([
652       'field_name' => $field_name,
653       'entity_type' => 'node',
654       'type' => 'datetime',
655       'settings' => ['datetime_type' => 'date'],
656     ]);
657     $field_storage->save();
658
659     $field = FieldConfig::create([
660       'field_storage' => $field_storage,
661       'bundle' => 'date_content',
662     ]);
663     $field->save();
664
665     // Loop through defined timezones to test that date-only defaults work at
666     // the extremes.
667     foreach (static::$timezones as $timezone) {
668
669       $this->setSiteTimezone($timezone);
670       $this->assertEquals($timezone, $this->config('system.date')->get('timezone.default'), 'Time zone set to ' . $timezone);
671
672       // Set now as default_value.
673       $field_edit = [
674         'default_value_input[default_date_type]' => 'now',
675       ];
676       $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
677
678       // Check that default value is selected in default value form.
679       $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
680       $this->assertOptionSelected('edit-default-value-input-default-date-type', 'now', 'The default value is selected in instance settings page');
681       $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default value is empty in instance settings page');
682
683       // Check if default_date has been stored successfully.
684       $config_entity = $this->config('field.field.node.date_content.' . $field_name)
685         ->get();
686       $this->assertEqual($config_entity['default_value'][0], [
687         'default_date_type' => 'now',
688         'default_date' => 'now',
689       ], 'Default value has been stored successfully');
690
691       // Clear field cache in order to avoid stale cache values.
692       \Drupal::entityManager()->clearCachedFieldDefinitions();
693
694       // Create a new node to check that datetime field default value is today.
695       $new_node = Node::create(['type' => 'date_content']);
696       $expected_date = new DrupalDateTime('now', drupal_get_user_timezone());
697       $this->assertEqual($new_node->get($field_name)
698         ->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
699
700       // Set an invalid relative default_value to test validation.
701       $field_edit = [
702         'default_value_input[default_date_type]' => 'relative',
703         'default_value_input[default_date]' => 'invalid date',
704       ];
705       $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
706
707       $this->assertText('The relative date value entered is invalid.');
708
709       // Set a relative default_value.
710       $field_edit = [
711         'default_value_input[default_date_type]' => 'relative',
712         'default_value_input[default_date]' => '+90 days',
713       ];
714       $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
715
716       // Check that default value is selected in default value form.
717       $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
718       $this->assertOptionSelected('edit-default-value-input-default-date-type', 'relative', 'The default value is selected in instance settings page');
719       $this->assertFieldByName('default_value_input[default_date]', '+90 days', 'The relative default value is displayed in instance settings page');
720
721       // Check if default_date has been stored successfully.
722       $config_entity = $this->config('field.field.node.date_content.' . $field_name)
723         ->get();
724       $this->assertEqual($config_entity['default_value'][0], [
725         'default_date_type' => 'relative',
726         'default_date' => '+90 days',
727       ], 'Default value has been stored successfully');
728
729       // Clear field cache in order to avoid stale cache values.
730       \Drupal::entityManager()->clearCachedFieldDefinitions();
731
732       // Create a new node to check that datetime field default value is +90
733       // days.
734       $new_node = Node::create(['type' => 'date_content']);
735       $expected_date = new DrupalDateTime('+90 days', drupal_get_user_timezone());
736       $this->assertEqual($new_node->get($field_name)
737         ->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
738
739       // Remove default value.
740       $field_edit = [
741         'default_value_input[default_date_type]' => '',
742       ];
743       $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
744
745       // Check that default value is selected in default value form.
746       $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
747       $this->assertOptionSelected('edit-default-value-input-default-date-type', '', 'The default value is selected in instance settings page');
748       $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default value is empty in instance settings page');
749
750       // Check if default_date has been stored successfully.
751       $config_entity = $this->config('field.field.node.date_content.' . $field_name)
752         ->get();
753       $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
754
755       // Clear field cache in order to avoid stale cache values.
756       \Drupal::entityManager()->clearCachedFieldDefinitions();
757
758       // Create a new node to check that datetime field default value is not
759       // set.
760       $new_node = Node::create(['type' => 'date_content']);
761       $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    */
768   public function testInvalidField() {
769     // Change the field to a datetime field.
770     $this->fieldStorage->setSetting('datetime_type', 'datetime');
771     $this->fieldStorage->save();
772     $field_name = $this->fieldStorage->getName();
773
774     // Display creation form.
775     $this->drupalGet('entity_test/add');
776     $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
777     $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
778
779     // Submit invalid dates and ensure they is not accepted.
780     $date_value = '';
781     $edit = [
782       "{$field_name}[0][value][date]" => $date_value,
783       "{$field_name}[0][value][time]" => '12:00:00',
784     ];
785     $this->drupalPostForm(NULL, $edit, t('Save'));
786     $this->assertText('date is invalid', 'Empty date value has been caught.');
787
788     $date_value = 'aaaa-12-01';
789     $edit = [
790       "{$field_name}[0][value][date]" => $date_value,
791       "{$field_name}[0][value][time]" => '00:00:00',
792     ];
793     $this->drupalPostForm(NULL, $edit, t('Save'));
794     $this->assertText('date is invalid', format_string('Invalid year value %date has been caught.', ['%date' => $date_value]));
795
796     $date_value = '2012-75-01';
797     $edit = [
798       "{$field_name}[0][value][date]" => $date_value,
799       "{$field_name}[0][value][time]" => '00:00:00',
800     ];
801     $this->drupalPostForm(NULL, $edit, t('Save'));
802     $this->assertText('date is invalid', format_string('Invalid month value %date has been caught.', ['%date' => $date_value]));
803
804     $date_value = '2012-12-99';
805     $edit = [
806       "{$field_name}[0][value][date]" => $date_value,
807       "{$field_name}[0][value][time]" => '00:00:00',
808     ];
809     $this->drupalPostForm(NULL, $edit, t('Save'));
810     $this->assertText('date is invalid', format_string('Invalid day value %date has been caught.', ['%date' => $date_value]));
811
812     $date_value = '2012-12-01';
813     $time_value = '';
814     $edit = [
815       "{$field_name}[0][value][date]" => $date_value,
816       "{$field_name}[0][value][time]" => $time_value,
817     ];
818     $this->drupalPostForm(NULL, $edit, t('Save'));
819     $this->assertText('date is invalid', 'Empty time value has been caught.');
820
821     $date_value = '2012-12-01';
822     $time_value = '49:00:00';
823     $edit = [
824       "{$field_name}[0][value][date]" => $date_value,
825       "{$field_name}[0][value][time]" => $time_value,
826     ];
827     $this->drupalPostForm(NULL, $edit, t('Save'));
828     $this->assertText('date is invalid', format_string('Invalid hour value %time has been caught.', ['%time' => $time_value]));
829
830     $date_value = '2012-12-01';
831     $time_value = '12:99:00';
832     $edit = [
833       "{$field_name}[0][value][date]" => $date_value,
834       "{$field_name}[0][value][time]" => $time_value,
835     ];
836     $this->drupalPostForm(NULL, $edit, t('Save'));
837     $this->assertText('date is invalid', format_string('Invalid minute value %time has been caught.', ['%time' => $time_value]));
838
839     $date_value = '2012-12-01';
840     $time_value = '12:15:99';
841     $edit = [
842       "{$field_name}[0][value][date]" => $date_value,
843       "{$field_name}[0][value][time]" => $time_value,
844     ];
845     $this->drupalPostForm(NULL, $edit, t('Save'));
846     $this->assertText('date is invalid', format_string('Invalid second value %time has been caught.', ['%time' => $time_value]));
847   }
848
849   /**
850    * Tests that 'Date' field storage setting form is disabled if field has data.
851    */
852   public function testDateStorageSettings() {
853     // Create a test content type.
854     $this->drupalCreateContentType(['type' => 'date_content']);
855
856     // Create a field storage with settings to validate.
857     $field_name = Unicode::strtolower($this->randomMachineName());
858     $field_storage = FieldStorageConfig::create([
859       'field_name' => $field_name,
860       'entity_type' => 'node',
861       'type' => 'datetime',
862       'settings' => [
863         'datetime_type' => 'date',
864       ],
865     ]);
866     $field_storage->save();
867     $field = FieldConfig::create([
868       'field_storage' => $field_storage,
869       'field_name' => $field_name,
870       'bundle' => 'date_content',
871     ]);
872     $field->save();
873
874     entity_get_form_display('node', 'date_content', 'default')
875       ->setComponent($field_name, [
876         'type' => 'datetime_default',
877       ])
878       ->save();
879     $edit = [
880       'title[0][value]' => $this->randomString(),
881       'body[0][value]' => $this->randomString(),
882       $field_name . '[0][value][date]' => '2016-04-01',
883     ];
884     $this->drupalPostForm('node/add/date_content', $edit, t('Save'));
885     $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage');
886     $result = $this->xpath("//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]");
887     $this->assertEqual(count($result), 1, "Changing datetime setting is disabled.");
888     $this->assertText('There is data for this field in the database. The field settings can no longer be changed.');
889   }
890
891 }