TimezoneTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. namespace Drupal\KernelTests\Core\Datetime\Element;
  3. use Drupal\Core\Datetime\DrupalDateTime;
  4. use Drupal\Core\Datetime\Entity\DateFormat;
  5. use Drupal\Core\Form\FormBuilderInterface;
  6. use Drupal\Core\Form\FormInterface;
  7. use Drupal\Core\Form\FormState;
  8. use Drupal\Core\Form\FormStateInterface;
  9. use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
  10. /**
  11. * Tests the timezone handling of datetime and datelist element types.
  12. *
  13. * A range of different permutations of #default_value and #date_timezone
  14. * for an element are setup in a single form by the buildForm() method, and
  15. * tested in various ways for both element types.
  16. *
  17. * @group Form
  18. */
  19. class TimezoneTest extends EntityKernelTestBase implements FormInterface {
  20. /**
  21. * {@inheritdoc}
  22. */
  23. public static $modules = ['system'];
  24. /**
  25. * The date used in tests.
  26. *
  27. * @var \Drupal\Core\Datetime\DrupalDateTime
  28. */
  29. protected $date;
  30. /**
  31. * An array of timezones with labels denoting their use in the tests.
  32. *
  33. * @var array
  34. */
  35. protected $timezones = [
  36. // UTC-12, no DST.
  37. 'zone A' => 'Pacific/Kwajalein',
  38. // UTC-7, no DST.
  39. 'zone B' => 'America/Phoenix',
  40. // UTC+5:30, no DST.
  41. 'user' => 'Asia/Kolkata',
  42. 'UTC' => 'UTC',
  43. ];
  44. /**
  45. * The test date formatted in various formats and timezones.
  46. *
  47. * @var array
  48. */
  49. protected $formattedDates = [];
  50. /**
  51. * HTML date format pattern.
  52. *
  53. * @var string
  54. */
  55. protected $dateFormat;
  56. /**
  57. * HTML time format pattern.
  58. *
  59. * @var string
  60. */
  61. protected $timeFormat;
  62. /**
  63. * The element type that is being tested ('datetime' or 'datelist').
  64. *
  65. * @var string
  66. */
  67. protected $elementType;
  68. /**
  69. * The number of test elements on the form.
  70. *
  71. * @var int
  72. */
  73. protected $testConditions;
  74. /**
  75. * {@inheritdoc}
  76. */
  77. public function buildForm(array $form, FormStateInterface $form_state) {
  78. $form['test1'] = [
  79. '#title' => 'No default date, #date_timezone present',
  80. '#type' => $this->elementType,
  81. '#default_value' => '',
  82. '#date_timezone' => $this->timezones['zone A'],
  83. '#test_expect_timezone' => 'zone A',
  84. ];
  85. $form['test2'] = [
  86. '#title' => 'No default date, no #date_timezone',
  87. '#type' => $this->elementType,
  88. '#default_value' => '',
  89. '#test_expect_timezone' => 'user',
  90. ];
  91. $form['test3'] = [
  92. '#title' => 'Default date present with default timezone, #date_timezone same',
  93. '#type' => $this->elementType,
  94. '#default_value' => $this->date,
  95. '#date_timezone' => $this->timezones['user'],
  96. '#test_expect_timezone' => 'user',
  97. ];
  98. $form['test4'] = [
  99. '#title' => 'Default date present with default timezone, #date_timezone different',
  100. '#type' => $this->elementType,
  101. '#default_value' => $this->date,
  102. '#date_timezone' => $this->timezones['zone A'],
  103. '#test_expect_timezone' => 'zone A',
  104. ];
  105. $form['test5'] = [
  106. '#title' => 'Default date present with default timezone, no #date_timezone',
  107. '#type' => $this->elementType,
  108. '#default_value' => $this->date,
  109. '#test_expect_timezone' => 'user',
  110. ];
  111. $dateWithTimeZoneA = clone $this->date;
  112. $dateWithTimeZoneA->setTimezone(new \DateTimeZone($this->timezones['zone A']));
  113. $form['test6'] = [
  114. '#title' => 'Default date present with unusual timezone, #date_timezone same',
  115. '#type' => $this->elementType,
  116. '#default_value' => $dateWithTimeZoneA,
  117. '#date_timezone' => $this->timezones['zone A'],
  118. '#test_expect_timezone' => 'zone A',
  119. ];
  120. $form['test7'] = [
  121. '#title' => 'Default date present with unusual timezone, #date_timezone different',
  122. '#type' => $this->elementType,
  123. '#default_value' => $dateWithTimeZoneA,
  124. '#date_timezone' => $this->timezones['zone B'],
  125. '#test_expect_timezone' => 'zone B',
  126. ];
  127. $form['test8'] = [
  128. '#title' => 'Default date present with unusual timezone, no #date_timezone',
  129. '#type' => $this->elementType,
  130. '#default_value' => $dateWithTimeZoneA,
  131. '#test_expect_timezone' => 'user',
  132. ];
  133. $this->testConditions = 8;
  134. return $form;
  135. }
  136. /**
  137. * {@inheritdoc}
  138. */
  139. protected function setUp() {
  140. parent::setUp();
  141. $this->installConfig(['system']);
  142. // Setup the background time zones.
  143. $this->timezones['php initial'] = date_default_timezone_get();
  144. $user = $this->createUser();
  145. $user->set('timezone', $this->timezones['user'])->save();
  146. // This also sets PHP's assumed time.
  147. \Drupal::currentUser()->setAccount($user);
  148. // Set a reference date to use in tests.
  149. $this->date = new DrupalDatetime('2000-01-01 12:00', NULL);
  150. // Create arrays listing the dates and times of $this->date formatted
  151. // according to the various timezones of $this->timezones.
  152. $this->dateFormat = DateFormat::load('html_date')->getPattern();
  153. $this->timeFormat = DateFormat::load('html_time')->getPattern();
  154. $date = clone $this->date;
  155. foreach ($this->timezones as $label => $timezone) {
  156. $date->setTimezone(new \DateTimeZone($timezone));
  157. $this->formattedDates['date'][$label] = $date->format($this->dateFormat);
  158. $this->formattedDates['time'][$label] = $date->format($this->timeFormat);
  159. $this->formattedDates['day'][$label] = $date->format('j');
  160. $this->formattedDates['month'][$label] = $date->format('n');
  161. $this->formattedDates['year'][$label] = $date->format('Y');
  162. $this->formattedDates['hour'][$label] = $date->format('G');
  163. $this->formattedDates['minute'][$label] = $date->format('i');
  164. $this->formattedDates['second'][$label] = $date->format('s');
  165. }
  166. // Validate the timezone setup.
  167. $this->assertEquals($this->timezones['user'], date_default_timezone_get(), 'Subsequent tests assume specific value for date_default_timezone_get().');
  168. $this->assertEquals(date_default_timezone_get(), $this->date->getTimezone()->getName(), 'Subsequent tests assume DrupalDateTime objects default to Drupal user time zone if none specified');
  169. }
  170. /**
  171. * Tests datetime elements interpret their times correctly when saving.
  172. *
  173. * Initial times are inevitably presented to the user using a timezone, and so
  174. * the time must be interpreted using the same timezone when it is time to
  175. * save the form, otherwise stored times may be changed without the user
  176. * changing the element's values.
  177. */
  178. public function testDatetimeElementTimesUnderstoodCorrectly() {
  179. $this->assertTimesUnderstoodCorrectly('datetime', ['date', 'time']);
  180. }
  181. /**
  182. * Tests datelist elements interpret their times correctly when saving.
  183. *
  184. * See testDatetimeElementTimesUnderstoodCorrectly() for more explanation.
  185. */
  186. public function testDatelistElementTimesUnderstoodCorrectly() {
  187. $this->assertTimesUnderstoodCorrectly('datelist', [
  188. 'day',
  189. 'month',
  190. 'year',
  191. 'hour',
  192. 'minute',
  193. 'second',
  194. ]);
  195. }
  196. /**
  197. * On datetime elements test #date_timezone after ::processDatetime.
  198. *
  199. * The element's render array has a #date_timezone value that should
  200. * accurately reflect the timezone that will be used to interpret times
  201. * entered through the element.
  202. */
  203. public function testDatetimeTimezonePropertyProcessed() {
  204. $this->assertDateTimezonePropertyProcessed('datetime');
  205. }
  206. /**
  207. * On datelist elements test #date_timezone after ::processDatetime.
  208. *
  209. * See testDatetimeTimezonePropertyProcessed() for more explanation.
  210. */
  211. public function testDatelistTimezonePropertyProcessed() {
  212. $this->assertDateTimezonePropertyProcessed('datelist');
  213. }
  214. /**
  215. * Asserts that elements interpret dates using the expected time zones.
  216. *
  217. * @param string $elementType
  218. * The element type to test.
  219. * @param array $inputs
  220. * The names of the default input elements used by this element type.
  221. *
  222. * @throws \Exception
  223. */
  224. protected function assertTimesUnderstoodCorrectly($elementType, array $inputs) {
  225. $this->elementType = $elementType;
  226. // Simulate the form being saved, with the user adding the date for any
  227. // initially empty elements, but not changing other elements.
  228. $form_state = new FormState();
  229. $form_builder = $this->container->get('form_builder');
  230. $form = $this->setupForm($form_state, $form_builder);
  231. foreach ($form as $elementName => $element) {
  232. if (
  233. isset($element['#type']) &&
  234. $element['#type'] === $this->elementType &&
  235. $element['#default_value'] === ''
  236. ) {
  237. $newValues = [];
  238. // Build an array of new values for the initially empty elements,
  239. // depending on the inputs required by the element type, and using
  240. // the timezone that will be expected for that test element.
  241. foreach ($inputs as $input) {
  242. $newValues[$input] = $this->formattedDates[$input][$element['#test_expect_timezone']];
  243. }
  244. $form_state->setValue([$elementName], $newValues);
  245. }
  246. }
  247. $form_builder->submitForm($this, $form_state);
  248. // Examine the output of each test element.
  249. $utc = new \DateTimeZone('UTC');
  250. $expectedDateUTC = clone $this->date;
  251. $expectedDateUTC->setTimezone($utc)->format('Y-m-d H:i:s');
  252. $wrongDates = [];
  253. $wrongTimezones = [];
  254. $rightDates = 0;
  255. foreach ($form_state->getCompleteForm() as $elementName => $element) {
  256. if (isset($element['#type']) && $element['#type'] === $this->elementType) {
  257. $actualDate = $form_state->getValue($elementName);
  258. $actualTimezone = array_search($actualDate->getTimezone()->getName(), $this->timezones);
  259. $actualDateUTC = $actualDate->setTimezone($utc)->format('Y-m-d H:i:s');
  260. // Check that $this->date has not anywhere been accidentally changed
  261. // from its default timezone, invalidating the test logic.
  262. $this->assertEquals(date_default_timezone_get(), $this->date->getTimezone()->getName(), "Test date still set to user timezone.");
  263. // Build a list of cases where the result is not as expected.
  264. // Check the time has been understood correctly.
  265. if ($actualDate != $this->date) {
  266. $wrongDates[$element['#title']] = $actualDateUTC;
  267. }
  268. else {
  269. // Explicitly counting test passes prevents the test from seeming to
  270. // pass just because the whole loop is being skipped.
  271. $rightDates++;
  272. }
  273. // Check the correct timezone is set on the value object.
  274. if ($element['#test_expect_timezone'] !== $actualTimezone) {
  275. $wrongTimezones[$element['#title']] = [$element['#test_expect_timezone'], $actualTimezone];
  276. }
  277. }
  278. }
  279. $message = "On all elements the time should be understood correctly as $expectedDateUTC: \n" . print_r($wrongDates, TRUE);
  280. $this->assertEquals($this->testConditions, $rightDates, $message);
  281. $message = "On all elements the correct timezone should be set on the value object: (expected, actual) \n" . print_r($wrongTimezones, TRUE);
  282. $this->assertCount(0, $wrongTimezones, $message);
  283. }
  284. /**
  285. * Asserts that elements set #date_timezone correctly.
  286. *
  287. * @param string $elementType
  288. * The element type to test.
  289. *
  290. * @throws \Exception
  291. */
  292. public function assertDateTimezonePropertyProcessed($elementType) {
  293. $this->elementType = $elementType;
  294. // Simulate form being loaded and default values displayed to user.
  295. $form_state = new FormState();
  296. $form_builder = $this->container->get('form_builder');
  297. $this->setupForm($form_state, $form_builder);
  298. // Check the #date_timezone property on each processed test element.
  299. $wrongTimezones = [];
  300. foreach ($form_state->getCompleteForm() as $elementName => $element) {
  301. if (isset($element['#type']) && $element['#type'] === $this->elementType) {
  302. // Check the correct timezone is set on the value object.
  303. $actualTimezone = array_search($element['#date_timezone'], $this->timezones, TRUE);
  304. if ($element['#test_expect_timezone'] !== $actualTimezone) {
  305. $wrongTimezones[$element['#title']] = [
  306. $element['#test_expect_timezone'],
  307. $actualTimezone,
  308. ];
  309. }
  310. }
  311. $this->assertEquals($this->timezones['user'], date_default_timezone_get(), 'Subsequent tests assume specific value for date_default_timezone_get().');
  312. $message = "The correct timezone should be set on the processed {$this->elementType} elements: (expected, actual) \n" . print_r($wrongTimezones, TRUE);
  313. $this->assertCount(0, $wrongTimezones, $message);
  314. }
  315. }
  316. /**
  317. * Simulate form being loaded and default values displayed to user.
  318. *
  319. * @param \Drupal\Core\Form\FormStateInterface $form_state
  320. * A form_state object.
  321. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
  322. * A form_builder object.
  323. *
  324. * @return \Drupal\Core\Form\FormStateInterface
  325. * The modified form state.
  326. */
  327. protected function setupForm(FormStateInterface $form_state, FormBuilderInterface $form_builder) {
  328. $form_id = $form_builder->getFormId($this, $form_state);
  329. $form = $form_builder->retrieveForm($form_id, $form_state);
  330. $form_state->setValidationEnforced();
  331. $form_state->clearErrors();
  332. $form_builder->prepareForm($form_id, $form, $form_state);
  333. $form_builder->processForm($form_id, $form, $form_state);
  334. return $form_builder->retrieveForm($form_id, $form_state);
  335. }
  336. /**
  337. * {@inheritdoc}
  338. */
  339. public function getFormId() {
  340. return 'test_datetime_elements';
  341. }
  342. /**
  343. * {@inheritdoc}
  344. */
  345. public function submitForm(array &$form, FormStateInterface $form_state) {
  346. }
  347. /**
  348. * {@inheritdoc}
  349. */
  350. public function validateForm(array &$form, FormStateInterface $form_state) {
  351. }
  352. }