Mercurial > hg > isophonics-drupal-site
comparison core/tests/Drupal/Tests/UiHelperTrait.php @ 17:129ea1e6d783
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:21:36 +0000 |
parents | |
children | af1871eacc83 |
comparison
equal
deleted
inserted
replaced
16:c2387f117808 | 17:129ea1e6d783 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests; | |
4 | |
5 use Behat\Mink\Driver\GoutteDriver; | |
6 use Drupal\Component\Render\FormattableMarkup; | |
7 use Drupal\Component\Utility\Html; | |
8 use Drupal\Component\Utility\UrlHelper; | |
9 use Drupal\Core\Session\AccountInterface; | |
10 use Drupal\Core\Session\AnonymousUserSession; | |
11 use Drupal\Core\Test\RefreshVariablesTrait; | |
12 use Drupal\Core\Url; | |
13 | |
14 /** | |
15 * Provides UI helper methods. | |
16 */ | |
17 trait UiHelperTrait { | |
18 | |
19 use BrowserHtmlDebugTrait; | |
20 use AssertHelperTrait; | |
21 use RefreshVariablesTrait; | |
22 | |
23 /** | |
24 * The current user logged in using the Mink controlled browser. | |
25 * | |
26 * @var \Drupal\user\UserInterface | |
27 */ | |
28 protected $loggedInUser = FALSE; | |
29 | |
30 /** | |
31 * The number of meta refresh redirects to follow, or NULL if unlimited. | |
32 * | |
33 * @var null|int | |
34 */ | |
35 protected $maximumMetaRefreshCount = NULL; | |
36 | |
37 /** | |
38 * The number of meta refresh redirects followed during ::drupalGet(). | |
39 * | |
40 * @var int | |
41 */ | |
42 protected $metaRefreshCount = 0; | |
43 | |
44 /** | |
45 * Fills and submits a form. | |
46 * | |
47 * @param array $edit | |
48 * Field data in an associative array. Changes the current input fields | |
49 * (where possible) to the values indicated. | |
50 * | |
51 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
52 * be unchecked. | |
53 * @param string $submit | |
54 * Value of the submit button whose click is to be emulated. For example, | |
55 * 'Save'. The processing of the request depends on this value. For example, | |
56 * a form may have one button with the value 'Save' and another button with | |
57 * the value 'Delete', and execute different code depending on which one is | |
58 * clicked. | |
59 * @param string $form_html_id | |
60 * (optional) HTML ID of the form to be submitted. On some pages | |
61 * there are many identical forms, so just using the value of the submit | |
62 * button is not enough. For example: 'trigger-node-presave-assign-form'. | |
63 * Note that this is not the Drupal $form_id, but rather the HTML ID of the | |
64 * form, which is typically the same thing but with hyphens replacing the | |
65 * underscores. | |
66 */ | |
67 protected function submitForm(array $edit, $submit, $form_html_id = NULL) { | |
68 $assert_session = $this->assertSession(); | |
69 | |
70 // Get the form. | |
71 if (isset($form_html_id)) { | |
72 $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']"); | |
73 $submit_button = $assert_session->buttonExists($submit, $form); | |
74 $action = $form->getAttribute('action'); | |
75 } | |
76 else { | |
77 $submit_button = $assert_session->buttonExists($submit); | |
78 $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); | |
79 $action = $form->getAttribute('action'); | |
80 } | |
81 | |
82 // Edit the form values. | |
83 foreach ($edit as $name => $value) { | |
84 $field = $assert_session->fieldExists($name, $form); | |
85 | |
86 // Provide support for the values '1' and '0' for checkboxes instead of | |
87 // TRUE and FALSE. | |
88 // @todo Get rid of supporting 1/0 by converting all tests cases using | |
89 // this to boolean values. | |
90 $field_type = $field->getAttribute('type'); | |
91 if ($field_type === 'checkbox') { | |
92 $value = (bool) $value; | |
93 } | |
94 | |
95 $field->setValue($value); | |
96 } | |
97 | |
98 // Submit form. | |
99 $this->prepareRequest(); | |
100 $submit_button->press(); | |
101 | |
102 // Ensure that any changes to variables in the other thread are picked up. | |
103 $this->refreshVariables(); | |
104 | |
105 // Check if there are any meta refresh redirects (like Batch API pages). | |
106 if ($this->checkForMetaRefresh()) { | |
107 // We are finished with all meta refresh redirects, so reset the counter. | |
108 $this->metaRefreshCount = 0; | |
109 } | |
110 | |
111 // Log only for JavascriptTestBase tests because for Goutte we log with | |
112 // ::getResponseLogHandler. | |
113 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
114 $out = $this->getSession()->getPage()->getContent(); | |
115 $html_output = 'POST request to: ' . $action . | |
116 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
117 $html_output .= '<hr />' . $out; | |
118 $html_output .= $this->getHtmlOutputHeaders(); | |
119 $this->htmlOutput($html_output); | |
120 } | |
121 | |
122 } | |
123 | |
124 /** | |
125 * Executes a form submission. | |
126 * | |
127 * It will be done as usual submit form with Mink. | |
128 * | |
129 * @param \Drupal\Core\Url|string $path | |
130 * Location of the post form. Either a Drupal path or an absolute path or | |
131 * NULL to post to the current page. For multi-stage forms you can set the | |
132 * path to NULL and have it post to the last received page. Example: | |
133 * | |
134 * @code | |
135 * // First step in form. | |
136 * $edit = array(...); | |
137 * $this->drupalPostForm('some_url', $edit, 'Save'); | |
138 * | |
139 * // Second step in form. | |
140 * $edit = array(...); | |
141 * $this->drupalPostForm(NULL, $edit, 'Save'); | |
142 * @endcode | |
143 * @param array $edit | |
144 * Field data in an associative array. Changes the current input fields | |
145 * (where possible) to the values indicated. | |
146 * | |
147 * When working with form tests, the keys for an $edit element should match | |
148 * the 'name' parameter of the HTML of the form. For example, the 'body' | |
149 * field for a node has the following HTML: | |
150 * @code | |
151 * <textarea id="edit-body-und-0-value" class="text-full form-textarea | |
152 * resize-vertical" placeholder="" cols="60" rows="9" | |
153 * name="body[0][value]"></textarea> | |
154 * @endcode | |
155 * When testing this field using an $edit parameter, the code becomes: | |
156 * @code | |
157 * $edit["body[0][value]"] = 'My test value'; | |
158 * @endcode | |
159 * | |
160 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
161 * be unchecked. Multiple select fields can be tested using 'name[]' and | |
162 * setting each of the desired values in an array: | |
163 * @code | |
164 * $edit = array(); | |
165 * $edit['name[]'] = array('value1', 'value2'); | |
166 * @endcode | |
167 * @todo change $edit to disallow NULL as a value for Drupal 9. | |
168 * https://www.drupal.org/node/2802401 | |
169 * @param string $submit | |
170 * The id, name, label or value of the submit button which is to be clicked. | |
171 * For example, 'Save'. The first element matched by | |
172 * \Drupal\Tests\WebAssert::buttonExists() will be used. The processing of | |
173 * the request depends on this value. For example, a form may have one | |
174 * button with the value 'Save' and another button with the value 'Delete', | |
175 * and execute different code depending on which one is clicked. | |
176 * @param array $options | |
177 * Options to be forwarded to the url generator. | |
178 * @param string|null $form_html_id | |
179 * (optional) HTML ID of the form to be submitted. On some pages | |
180 * there are many identical forms, so just using the value of the submit | |
181 * button is not enough. For example: 'trigger-node-presave-assign-form'. | |
182 * Note that this is not the Drupal $form_id, but rather the HTML ID of the | |
183 * form, which is typically the same thing but with hyphens replacing the | |
184 * underscores. | |
185 * | |
186 * @return string | |
187 * (deprecated) The response content after submit form. It is necessary for | |
188 * backwards compatibility and will be removed before Drupal 9.0. You should | |
189 * just use the webAssert object for your assertions. | |
190 * | |
191 * @see \Drupal\Tests\WebAssert::buttonExists() | |
192 */ | |
193 protected function drupalPostForm($path, $edit, $submit, array $options = [], $form_html_id = NULL) { | |
194 if (is_object($submit)) { | |
195 // Cast MarkupInterface objects to string. | |
196 $submit = (string) $submit; | |
197 } | |
198 if ($edit === NULL) { | |
199 $edit = []; | |
200 } | |
201 if (is_array($edit)) { | |
202 $edit = $this->castSafeStrings($edit); | |
203 } | |
204 | |
205 if (isset($path)) { | |
206 $this->drupalGet($path, $options); | |
207 } | |
208 | |
209 $this->submitForm($edit, $submit, $form_html_id); | |
210 | |
211 return $this->getSession()->getPage()->getContent(); | |
212 } | |
213 | |
214 /** | |
215 * Logs in a user using the Mink controlled browser. | |
216 * | |
217 * If a user is already logged in, then the current user is logged out before | |
218 * logging in the specified user. | |
219 * | |
220 * Please note that neither the current user nor the passed-in user object is | |
221 * populated with data of the logged in user. If you need full access to the | |
222 * user object after logging in, it must be updated manually. If you also need | |
223 * access to the plain-text password of the user (set by drupalCreateUser()), | |
224 * e.g. to log in the same user again, then it must be re-assigned manually. | |
225 * For example: | |
226 * @code | |
227 * // Create a user. | |
228 * $account = $this->drupalCreateUser(array()); | |
229 * $this->drupalLogin($account); | |
230 * // Load real user object. | |
231 * $pass_raw = $account->passRaw; | |
232 * $account = User::load($account->id()); | |
233 * $account->passRaw = $pass_raw; | |
234 * @endcode | |
235 * | |
236 * @param \Drupal\Core\Session\AccountInterface $account | |
237 * User object representing the user to log in. | |
238 * | |
239 * @see drupalCreateUser() | |
240 */ | |
241 protected function drupalLogin(AccountInterface $account) { | |
242 if ($this->loggedInUser) { | |
243 $this->drupalLogout(); | |
244 } | |
245 | |
246 $this->drupalGet('user/login'); | |
247 $this->submitForm([ | |
248 'name' => $account->getUsername(), | |
249 'pass' => $account->passRaw, | |
250 ], t('Log in')); | |
251 | |
252 // @see ::drupalUserIsLoggedIn() | |
253 $account->sessionId = $this->getSession()->getCookie(\Drupal::service('session_configuration')->getOptions(\Drupal::request())['name']); | |
254 $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()])); | |
255 | |
256 $this->loggedInUser = $account; | |
257 $this->container->get('current_user')->setAccount($account); | |
258 } | |
259 | |
260 /** | |
261 * Logs a user out of the Mink controlled browser and confirms. | |
262 * | |
263 * Confirms logout by checking the login page. | |
264 */ | |
265 protected function drupalLogout() { | |
266 // Make a request to the logout page, and redirect to the user page, the | |
267 // idea being if you were properly logged out you should be seeing a login | |
268 // screen. | |
269 $assert_session = $this->assertSession(); | |
270 $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); | |
271 $assert_session->fieldExists('name'); | |
272 $assert_session->fieldExists('pass'); | |
273 | |
274 // @see BrowserTestBase::drupalUserIsLoggedIn() | |
275 unset($this->loggedInUser->sessionId); | |
276 $this->loggedInUser = FALSE; | |
277 \Drupal::currentUser()->setAccount(new AnonymousUserSession()); | |
278 } | |
279 | |
280 /** | |
281 * Returns WebAssert object. | |
282 * | |
283 * @param string $name | |
284 * (optional) Name of the session. Defaults to the active session. | |
285 * | |
286 * @return \Drupal\Tests\WebAssert | |
287 * A new web-assert option for asserting the presence of elements with. | |
288 */ | |
289 public function assertSession($name = NULL) { | |
290 $this->addToAssertionCount(1); | |
291 return new WebAssert($this->getSession($name), $this->baseUrl); | |
292 } | |
293 | |
294 /** | |
295 * Retrieves a Drupal path or an absolute path. | |
296 * | |
297 * @param string|\Drupal\Core\Url $path | |
298 * Drupal path or URL to load into Mink controlled browser. | |
299 * @param array $options | |
300 * (optional) Options to be forwarded to the url generator. | |
301 * @param string[] $headers | |
302 * An array containing additional HTTP request headers, the array keys are | |
303 * the header names and the array values the header values. This is useful | |
304 * to set for example the "Accept-Language" header for requesting the page | |
305 * in a different language. Note that not all headers are supported, for | |
306 * example the "Accept" header is always overridden by the browser. For | |
307 * testing REST APIs it is recommended to obtain a separate HTTP client | |
308 * using getHttpClient() and performing requests that way. | |
309 * | |
310 * @return string | |
311 * The retrieved HTML string, also available as $this->getRawContent() | |
312 * | |
313 * @see \Drupal\Tests\BrowserTestBase::getHttpClient() | |
314 */ | |
315 protected function drupalGet($path, array $options = [], array $headers = []) { | |
316 $options['absolute'] = TRUE; | |
317 $url = $this->buildUrl($path, $options); | |
318 | |
319 $session = $this->getSession(); | |
320 | |
321 $this->prepareRequest(); | |
322 foreach ($headers as $header_name => $header_value) { | |
323 $session->setRequestHeader($header_name, $header_value); | |
324 } | |
325 | |
326 $session->visit($url); | |
327 $out = $session->getPage()->getContent(); | |
328 | |
329 // Ensure that any changes to variables in the other thread are picked up. | |
330 $this->refreshVariables(); | |
331 | |
332 // Replace original page output with new output from redirected page(s). | |
333 if ($new = $this->checkForMetaRefresh()) { | |
334 $out = $new; | |
335 // We are finished with all meta refresh redirects, so reset the counter. | |
336 $this->metaRefreshCount = 0; | |
337 } | |
338 | |
339 // Log only for JavascriptTestBase tests because for Goutte we log with | |
340 // ::getResponseLogHandler. | |
341 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
342 $html_output = 'GET request to: ' . $url . | |
343 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
344 $html_output .= '<hr />' . $out; | |
345 $html_output .= $this->getHtmlOutputHeaders(); | |
346 $this->htmlOutput($html_output); | |
347 } | |
348 | |
349 return $out; | |
350 } | |
351 | |
352 /** | |
353 * Builds an a absolute URL from a system path or a URL object. | |
354 * | |
355 * @param string|\Drupal\Core\Url $path | |
356 * A system path or a URL. | |
357 * @param array $options | |
358 * Options to be passed to Url::fromUri(). | |
359 * | |
360 * @return string | |
361 * An absolute URL string. | |
362 */ | |
363 protected function buildUrl($path, array $options = []) { | |
364 if ($path instanceof Url) { | |
365 $url_options = $path->getOptions(); | |
366 $options = $url_options + $options; | |
367 $path->setOptions($options); | |
368 return $path->setAbsolute()->toString(); | |
369 } | |
370 // The URL generator service is not necessarily available yet; e.g., in | |
371 // interactive installer tests. | |
372 elseif (\Drupal::hasService('url_generator')) { | |
373 $force_internal = isset($options['external']) && $options['external'] == FALSE; | |
374 if (!$force_internal && UrlHelper::isExternal($path)) { | |
375 return Url::fromUri($path, $options)->toString(); | |
376 } | |
377 else { | |
378 $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path; | |
379 // Path processing is needed for language prefixing. Skip it when a | |
380 // path that may look like an external URL is being used as internal. | |
381 $options['path_processing'] = !$force_internal; | |
382 return Url::fromUri($uri, $options) | |
383 ->setAbsolute() | |
384 ->toString(); | |
385 } | |
386 } | |
387 else { | |
388 return $this->getAbsoluteUrl($path); | |
389 } | |
390 } | |
391 | |
392 /** | |
393 * Takes a path and returns an absolute path. | |
394 * | |
395 * @param string $path | |
396 * A path from the Mink controlled browser content. | |
397 * | |
398 * @return string | |
399 * The $path with $base_url prepended, if necessary. | |
400 */ | |
401 protected function getAbsoluteUrl($path) { | |
402 global $base_url, $base_path; | |
403 | |
404 $parts = parse_url($path); | |
405 if (empty($parts['host'])) { | |
406 // Ensure that we have a string (and no xpath object). | |
407 $path = (string) $path; | |
408 // Strip $base_path, if existent. | |
409 $length = strlen($base_path); | |
410 if (substr($path, 0, $length) === $base_path) { | |
411 $path = substr($path, $length); | |
412 } | |
413 // Ensure that we have an absolute path. | |
414 if (empty($path) || $path[0] !== '/') { | |
415 $path = '/' . $path; | |
416 } | |
417 // Finally, prepend the $base_url. | |
418 $path = $base_url . $path; | |
419 } | |
420 return $path; | |
421 } | |
422 | |
423 /** | |
424 * Prepare for a request to testing site. | |
425 * | |
426 * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is | |
427 * checked by drupal_valid_test_ua(). | |
428 * | |
429 * @see drupal_valid_test_ua() | |
430 */ | |
431 protected function prepareRequest() { | |
432 $session = $this->getSession(); | |
433 $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); | |
434 } | |
435 | |
436 /** | |
437 * Returns whether a given user account is logged in. | |
438 * | |
439 * @param \Drupal\Core\Session\AccountInterface $account | |
440 * The user account object to check. | |
441 * | |
442 * @return bool | |
443 * Return TRUE if the user is logged in, FALSE otherwise. | |
444 */ | |
445 protected function drupalUserIsLoggedIn(AccountInterface $account) { | |
446 $logged_in = FALSE; | |
447 | |
448 if (isset($account->sessionId)) { | |
449 $session_handler = \Drupal::service('session_handler.storage'); | |
450 $logged_in = (bool) $session_handler->read($account->sessionId); | |
451 } | |
452 | |
453 return $logged_in; | |
454 } | |
455 | |
456 /** | |
457 * Clicks the element with the given CSS selector. | |
458 * | |
459 * @param string $css_selector | |
460 * The CSS selector identifying the element to click. | |
461 */ | |
462 protected function click($css_selector) { | |
463 $starting_url = $this->getSession()->getCurrentUrl(); | |
464 $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector)); | |
465 // Log only for JavascriptTestBase tests because for Goutte we log with | |
466 // ::getResponseLogHandler. | |
467 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
468 $out = $this->getSession()->getPage()->getContent(); | |
469 $html_output = | |
470 'Clicked element with CSS selector: ' . $css_selector . | |
471 '<hr />Starting URL: ' . $starting_url . | |
472 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
473 $html_output .= '<hr />' . $out; | |
474 $html_output .= $this->getHtmlOutputHeaders(); | |
475 $this->htmlOutput($html_output); | |
476 } | |
477 } | |
478 | |
479 /** | |
480 * Follows a link by complete name. | |
481 * | |
482 * Will click the first link found with this link text. | |
483 * | |
484 * If the link is discovered and clicked, the test passes. Fail otherwise. | |
485 * | |
486 * @param string|\Drupal\Component\Render\MarkupInterface $label | |
487 * Text between the anchor tags. | |
488 * @param int $index | |
489 * (optional) The index number for cases where multiple links have the same | |
490 * text. Defaults to 0. | |
491 */ | |
492 protected function clickLink($label, $index = 0) { | |
493 $label = (string) $label; | |
494 $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); | |
495 $this->assertArrayHasKey($index, $links, 'The link ' . $label . ' was not found on the page.'); | |
496 $links[$index]->click(); | |
497 } | |
498 | |
499 /** | |
500 * Retrieves the plain-text content from the current page. | |
501 */ | |
502 protected function getTextContent() { | |
503 return $this->getSession()->getPage()->getText(); | |
504 } | |
505 | |
506 /** | |
507 * Get the current URL from the browser. | |
508 * | |
509 * @return string | |
510 * The current URL. | |
511 */ | |
512 protected function getUrl() { | |
513 return $this->getSession()->getCurrentUrl(); | |
514 } | |
515 | |
516 /** | |
517 * Checks for meta refresh tag and if found call drupalGet() recursively. | |
518 * | |
519 * This function looks for the http-equiv attribute to be set to "Refresh" and | |
520 * is case-insensitive. | |
521 * | |
522 * @return string|false | |
523 * Either the new page content or FALSE. | |
524 */ | |
525 protected function checkForMetaRefresh() { | |
526 $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); | |
527 if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { | |
528 // Parse the content attribute of the meta tag for the format: | |
529 // "[delay]: URL=[page_to_redirect_to]". | |
530 if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) { | |
531 $this->metaRefreshCount++; | |
532 return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); | |
533 } | |
534 } | |
535 return FALSE; | |
536 } | |
537 | |
538 /** | |
539 * Searches elements using a CSS selector in the raw content. | |
540 * | |
541 * The search is relative to the root element (HTML tag normally) of the page. | |
542 * | |
543 * @param string $selector | |
544 * CSS selector to use in the search. | |
545 * | |
546 * @return \Behat\Mink\Element\NodeElement[] | |
547 * The list of elements on the page that match the selector. | |
548 */ | |
549 protected function cssSelect($selector) { | |
550 return $this->getSession()->getPage()->findAll('css', $selector); | |
551 } | |
552 | |
553 } |