Mercurial > hg > cmmr2012-drupal-site
comparison core/tests/Drupal/Tests/BrowserTestBase.php @ 4:a9cd425dd02b
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:11:55 +0000 |
parents | c75dbcec494b |
children | 12f9dff5fda9 |
comparison
equal
deleted
inserted
replaced
3:307d7a7fd348 | 4:a9cd425dd02b |
---|---|
5 use Behat\Mink\Driver\GoutteDriver; | 5 use Behat\Mink\Driver\GoutteDriver; |
6 use Behat\Mink\Element\Element; | 6 use Behat\Mink\Element\Element; |
7 use Behat\Mink\Mink; | 7 use Behat\Mink\Mink; |
8 use Behat\Mink\Selector\SelectorsHandler; | 8 use Behat\Mink\Selector\SelectorsHandler; |
9 use Behat\Mink\Session; | 9 use Behat\Mink\Session; |
10 use Drupal\Component\Render\FormattableMarkup; | |
11 use Drupal\Component\Serialization\Json; | 10 use Drupal\Component\Serialization\Json; |
12 use Drupal\Component\Utility\Html; | |
13 use Drupal\Component\Utility\UrlHelper; | |
14 use Drupal\Core\Database\Database; | 11 use Drupal\Core\Database\Database; |
15 use Drupal\Core\Session\AccountInterface; | |
16 use Drupal\Core\Session\AnonymousUserSession; | |
17 use Drupal\Core\Test\FunctionalTestSetupTrait; | 12 use Drupal\Core\Test\FunctionalTestSetupTrait; |
18 use Drupal\Core\Test\TestSetupTrait; | 13 use Drupal\Core\Test\TestSetupTrait; |
19 use Drupal\Core\Url; | |
20 use Drupal\Core\Utility\Error; | 14 use Drupal\Core\Utility\Error; |
21 use Drupal\FunctionalTests\AssertLegacyTrait; | 15 use Drupal\FunctionalTests\AssertLegacyTrait; |
22 use Drupal\Tests\block\Traits\BlockCreationTrait; | 16 use Drupal\Tests\block\Traits\BlockCreationTrait; |
23 use Drupal\Tests\node\Traits\ContentTypeCreationTrait; | 17 use Drupal\Tests\node\Traits\ContentTypeCreationTrait; |
24 use Drupal\Tests\node\Traits\NodeCreationTrait; | 18 use Drupal\Tests\node\Traits\NodeCreationTrait; |
25 use Drupal\Tests\user\Traits\UserCreationTrait; | 19 use Drupal\Tests\user\Traits\UserCreationTrait; |
20 use GuzzleHttp\Cookie\CookieJar; | |
26 use PHPUnit\Framework\TestCase; | 21 use PHPUnit\Framework\TestCase; |
27 use Psr\Http\Message\RequestInterface; | 22 use Psr\Http\Message\RequestInterface; |
28 use Psr\Http\Message\ResponseInterface; | 23 use Psr\Http\Message\ResponseInterface; |
29 use Symfony\Component\CssSelector\CssSelectorConverter; | 24 use Symfony\Component\CssSelector\CssSelectorConverter; |
30 | 25 |
42 * @ingroup testing | 37 * @ingroup testing |
43 */ | 38 */ |
44 abstract class BrowserTestBase extends TestCase { | 39 abstract class BrowserTestBase extends TestCase { |
45 | 40 |
46 use FunctionalTestSetupTrait; | 41 use FunctionalTestSetupTrait; |
42 use UiHelperTrait { | |
43 FunctionalTestSetupTrait::refreshVariables insteadof UiHelperTrait; | |
44 } | |
47 use TestSetupTrait; | 45 use TestSetupTrait; |
48 use AssertHelperTrait; | |
49 use BlockCreationTrait { | 46 use BlockCreationTrait { |
50 placeBlock as drupalPlaceBlock; | 47 placeBlock as drupalPlaceBlock; |
51 } | 48 } |
52 use AssertLegacyTrait; | 49 use AssertLegacyTrait; |
53 use RandomGeneratorTrait; | 50 use RandomGeneratorTrait; |
54 use SessionTestTrait; | |
55 use NodeCreationTrait { | 51 use NodeCreationTrait { |
56 getNodeByTitle as drupalGetNodeByTitle; | 52 getNodeByTitle as drupalGetNodeByTitle; |
57 createNode as drupalCreateNode; | 53 createNode as drupalCreateNode; |
58 } | 54 } |
59 use ContentTypeCreationTrait { | 55 use ContentTypeCreationTrait { |
117 * @var string | 113 * @var string |
118 */ | 114 */ |
119 protected $profile = 'testing'; | 115 protected $profile = 'testing'; |
120 | 116 |
121 /** | 117 /** |
122 * The current user logged in using the Mink controlled browser. | |
123 * | |
124 * @var \Drupal\user\UserInterface | |
125 */ | |
126 protected $loggedInUser = FALSE; | |
127 | |
128 /** | |
129 * An array of custom translations suitable for drupal_rewrite_settings(). | 118 * An array of custom translations suitable for drupal_rewrite_settings(). |
130 * | 119 * |
131 * @var array | 120 * @var array |
132 */ | 121 */ |
133 protected $customTranslations; | 122 protected $customTranslations; |
134 | 123 |
135 /* | 124 /* |
136 * Mink class for the default driver to use. | 125 * Mink class for the default driver to use. |
137 * | 126 * |
138 * Shoud be a fully qualified class name that implements | 127 * Should be a fully-qualified class name that implements |
139 * Behat\Mink\Driver\DriverInterface. | 128 * Behat\Mink\Driver\DriverInterface. |
140 * | 129 * |
141 * Value can be overridden using the environment variable MINK_DRIVER_CLASS. | 130 * Value can be overridden using the environment variable MINK_DRIVER_CLASS. |
142 * | 131 * |
143 * @var string. | 132 * @var string |
144 */ | 133 */ |
145 protected $minkDefaultDriverClass = GoutteDriver::class; | 134 protected $minkDefaultDriverClass = GoutteDriver::class; |
146 | 135 |
147 /* | 136 /* |
148 * Mink default driver params. | 137 * Mink default driver params. |
180 * {@inheritdoc} | 169 * {@inheritdoc} |
181 */ | 170 */ |
182 protected $preserveGlobalState = FALSE; | 171 protected $preserveGlobalState = FALSE; |
183 | 172 |
184 /** | 173 /** |
185 * Class name for HTML output logging. | 174 * The base URL. |
186 * | 175 * |
187 * @var string | 176 * @var string |
188 */ | 177 */ |
189 protected $htmlOutputClassName; | |
190 | |
191 /** | |
192 * Directory name for HTML output logging. | |
193 * | |
194 * @var string | |
195 */ | |
196 protected $htmlOutputDirectory; | |
197 | |
198 /** | |
199 * Counter storage for HTML output logging. | |
200 * | |
201 * @var string | |
202 */ | |
203 protected $htmlOutputCounterStorage; | |
204 | |
205 /** | |
206 * Counter for HTML output logging. | |
207 * | |
208 * @var int | |
209 */ | |
210 protected $htmlOutputCounter = 1; | |
211 | |
212 /** | |
213 * HTML output output enabled. | |
214 * | |
215 * @var bool | |
216 */ | |
217 protected $htmlOutputEnabled = FALSE; | |
218 | |
219 /** | |
220 * The file name to write the list of URLs to. | |
221 * | |
222 * This file is read by the PHPUnit result printer. | |
223 * | |
224 * @var string | |
225 * | |
226 * @see \Drupal\Tests\Listeners\HtmlOutputPrinter | |
227 */ | |
228 protected $htmlOutputFile; | |
229 | |
230 /** | |
231 * HTML output test ID. | |
232 * | |
233 * @var int | |
234 */ | |
235 protected $htmlOutputTestId; | |
236 | |
237 /** | |
238 * The base URL. | |
239 * | |
240 * @var string | |
241 */ | |
242 protected $baseUrl; | 178 protected $baseUrl; |
243 | 179 |
244 /** | 180 /** |
245 * The original array of shutdown function callbacks. | 181 * The original array of shutdown function callbacks. |
246 * | 182 * |
247 * @var array | 183 * @var array |
248 */ | 184 */ |
249 protected $originalShutdownCallbacks = []; | 185 protected $originalShutdownCallbacks = []; |
250 | |
251 /** | |
252 * The number of meta refresh redirects to follow, or NULL if unlimited. | |
253 * | |
254 * @var null|int | |
255 */ | |
256 protected $maximumMetaRefreshCount = NULL; | |
257 | |
258 /** | |
259 * The number of meta refresh redirects followed during ::drupalGet(). | |
260 * | |
261 * @var int | |
262 */ | |
263 protected $metaRefreshCount = 0; | |
264 | 186 |
265 /** | 187 /** |
266 * The app root. | 188 * The app root. |
267 * | 189 * |
268 * @var string | 190 * @var string |
311 | 233 |
312 $driver->getClient()->setClient($client); | 234 $driver->getClient()->setClient($client); |
313 } | 235 } |
314 | 236 |
315 $selectors_handler = new SelectorsHandler([ | 237 $selectors_handler = new SelectorsHandler([ |
316 'hidden_field_selector' => new HiddenFieldSelector() | 238 'hidden_field_selector' => new HiddenFieldSelector(), |
317 ]); | 239 ]); |
318 $session = new Session($driver, $selectors_handler); | 240 $session = new Session($driver, $selectors_handler); |
319 $this->mink = new Mink(); | 241 $this->mink = new Mink(); |
320 $this->mink->registerSession('default', $session); | 242 $this->mink->registerSession('default', $session); |
321 $this->mink->setDefaultSessionName('default'); | 243 $this->mink->setDefaultSessionName('default'); |
322 $this->registerSessions(); | 244 $this->registerSessions(); |
323 | 245 |
324 $this->initFrontPage(); | 246 $this->initFrontPage(); |
247 | |
248 // Copies cookies from the current environment, for example, XDEBUG_SESSION | |
249 // in order to support Xdebug. | |
250 // @see BrowserTestBase::initFrontPage() | |
251 $cookies = $this->extractCookiesFromRequest(\Drupal::request()); | |
252 foreach ($cookies as $cookie_name => $values) { | |
253 foreach ($values as $value) { | |
254 $session->setCookie($cookie_name, $value); | |
255 } | |
256 } | |
325 | 257 |
326 return $session; | 258 return $session; |
327 } | 259 } |
328 | 260 |
329 /** | 261 /** |
376 } | 308 } |
377 else { | 309 else { |
378 $driver = new $this->minkDefaultDriverClass(); | 310 $driver = new $this->minkDefaultDriverClass(); |
379 } | 311 } |
380 return $driver; | 312 return $driver; |
381 } | |
382 | |
383 /** | |
384 * Creates the directory to store browser output. | |
385 * | |
386 * Creates the directory to store browser output in if a file to write | |
387 * URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter. | |
388 */ | |
389 protected function initBrowserOutputFile() { | |
390 $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE'); | |
391 $this->htmlOutputEnabled = is_file($browser_output_file); | |
392 if ($this->htmlOutputEnabled) { | |
393 $this->htmlOutputFile = $browser_output_file; | |
394 $this->htmlOutputClassName = str_replace("\\", "_", get_called_class()); | |
395 $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output'; | |
396 if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) { | |
397 file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n"); | |
398 } | |
399 $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter'; | |
400 $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory); | |
401 if (is_file($this->htmlOutputCounterStorage)) { | |
402 $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1; | |
403 } | |
404 } | |
405 } | 313 } |
406 | 314 |
407 /** | 315 /** |
408 * Get the Mink driver args from an environment variable, if it is set. Can | 316 * Get the Mink driver args from an environment variable, if it is set. Can |
409 * be overridden in a derived class so it is possible to use a different | 317 * be overridden in a derived class so it is possible to use a different |
488 // Install Drupal test site. | 396 // Install Drupal test site. |
489 $this->prepareEnvironment(); | 397 $this->prepareEnvironment(); |
490 $this->installDrupal(); | 398 $this->installDrupal(); |
491 | 399 |
492 // Setup Mink. | 400 // Setup Mink. |
493 $session = $this->initMink(); | 401 $this->initMink(); |
494 | |
495 $cookies = $this->extractCookiesFromRequest(\Drupal::request()); | |
496 foreach ($cookies as $cookie_name => $values) { | |
497 foreach ($values as $value) { | |
498 $session->setCookie($cookie_name, $value); | |
499 } | |
500 } | |
501 | 402 |
502 // Set up the browser test output file. | 403 // Set up the browser test output file. |
503 $this->initBrowserOutputFile(); | 404 $this->initBrowserOutputFile(); |
504 // If garbage collection was disabled prior to rebuilding container, | 405 // If garbage collection was disabled prior to rebuilding container, |
505 // re-enable it. | 406 // re-enable it. |
591 public function getSession($name = NULL) { | 492 public function getSession($name = NULL) { |
592 return $this->mink->getSession($name); | 493 return $this->mink->getSession($name); |
593 } | 494 } |
594 | 495 |
595 /** | 496 /** |
497 * Get session cookies from current session. | |
498 * | |
499 * @return \GuzzleHttp\Cookie\CookieJar | |
500 * A cookie jar with the current session. | |
501 */ | |
502 protected function getSessionCookies() { | |
503 $domain = parse_url($this->getUrl(), PHP_URL_HOST); | |
504 $session_id = $this->getSession()->getCookie($this->getSessionName()); | |
505 $cookies = CookieJar::fromArray([$this->getSessionName() => $session_id], $domain); | |
506 | |
507 return $cookies; | |
508 } | |
509 | |
510 /** | |
596 * Obtain the HTTP client for the system under test. | 511 * Obtain the HTTP client for the system under test. |
597 * | 512 * |
598 * Use this method for arbitrary HTTP requests to the site under test. For | 513 * Use this method for arbitrary HTTP requests to the site under test. For |
599 * most tests, you should not get the HTTP client and instead use navigation | 514 * most tests, you should not get the HTTP client and instead use navigation |
600 * methods such as drupalGet() and clickLink() in order to benefit from | 515 * methods such as drupalGet() and clickLink() in order to benefit from |
615 $mink_driver = $this->getSession()->getDriver(); | 530 $mink_driver = $this->getSession()->getDriver(); |
616 if ($mink_driver instanceof GoutteDriver) { | 531 if ($mink_driver instanceof GoutteDriver) { |
617 return $mink_driver->getClient()->getClient(); | 532 return $mink_driver->getClient()->getClient(); |
618 } | 533 } |
619 throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().'); | 534 throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().'); |
620 } | |
621 | |
622 /** | |
623 * Returns WebAssert object. | |
624 * | |
625 * @param string $name | |
626 * (optional) Name of the session. Defaults to the active session. | |
627 * | |
628 * @return \Drupal\Tests\WebAssert | |
629 * A new web-assert option for asserting the presence of elements with. | |
630 */ | |
631 public function assertSession($name = NULL) { | |
632 $this->addToAssertionCount(1); | |
633 return new WebAssert($this->getSession($name), $this->baseUrl); | |
634 } | |
635 | |
636 /** | |
637 * Prepare for a request to testing site. | |
638 * | |
639 * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is | |
640 * checked by drupal_valid_test_ua(). | |
641 * | |
642 * @see drupal_valid_test_ua() | |
643 */ | |
644 protected function prepareRequest() { | |
645 $session = $this->getSession(); | |
646 $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); | |
647 } | |
648 | |
649 /** | |
650 * Builds an a absolute URL from a system path or a URL object. | |
651 * | |
652 * @param string|\Drupal\Core\Url $path | |
653 * A system path or a URL. | |
654 * @param array $options | |
655 * Options to be passed to Url::fromUri(). | |
656 * | |
657 * @return string | |
658 * An absolute URL stsring. | |
659 */ | |
660 protected function buildUrl($path, array $options = []) { | |
661 if ($path instanceof Url) { | |
662 $url_options = $path->getOptions(); | |
663 $options = $url_options + $options; | |
664 $path->setOptions($options); | |
665 return $path->setAbsolute()->toString(); | |
666 } | |
667 // The URL generator service is not necessarily available yet; e.g., in | |
668 // interactive installer tests. | |
669 elseif ($this->container->has('url_generator')) { | |
670 $force_internal = isset($options['external']) && $options['external'] == FALSE; | |
671 if (!$force_internal && UrlHelper::isExternal($path)) { | |
672 return Url::fromUri($path, $options)->toString(); | |
673 } | |
674 else { | |
675 $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path; | |
676 // Path processing is needed for language prefixing. Skip it when a | |
677 // path that may look like an external URL is being used as internal. | |
678 $options['path_processing'] = !$force_internal; | |
679 return Url::fromUri($uri, $options) | |
680 ->setAbsolute() | |
681 ->toString(); | |
682 } | |
683 } | |
684 else { | |
685 return $this->getAbsoluteUrl($path); | |
686 } | |
687 } | |
688 | |
689 /** | |
690 * Retrieves a Drupal path or an absolute path. | |
691 * | |
692 * @param string|\Drupal\Core\Url $path | |
693 * Drupal path or URL to load into Mink controlled browser. | |
694 * @param array $options | |
695 * (optional) Options to be forwarded to the url generator. | |
696 * @param string[] $headers | |
697 * An array containing additional HTTP request headers, the array keys are | |
698 * the header names and the array values the header values. This is useful | |
699 * to set for example the "Accept-Language" header for requesting the page | |
700 * in a different language. Note that not all headers are supported, for | |
701 * example the "Accept" header is always overridden by the browser. For | |
702 * testing REST APIs it is recommended to obtain a separate HTTP client | |
703 * using getHttpClient() and performing requests that way. | |
704 * | |
705 * @return string | |
706 * The retrieved HTML string, also available as $this->getRawContent() | |
707 * | |
708 * @see \Drupal\Tests\BrowserTestBase::getHttpClient() | |
709 */ | |
710 protected function drupalGet($path, array $options = [], array $headers = []) { | |
711 $options['absolute'] = TRUE; | |
712 $url = $this->buildUrl($path, $options); | |
713 | |
714 $session = $this->getSession(); | |
715 | |
716 $this->prepareRequest(); | |
717 foreach ($headers as $header_name => $header_value) { | |
718 $session->setRequestHeader($header_name, $header_value); | |
719 } | |
720 | |
721 $session->visit($url); | |
722 $out = $session->getPage()->getContent(); | |
723 | |
724 // Ensure that any changes to variables in the other thread are picked up. | |
725 $this->refreshVariables(); | |
726 | |
727 // Replace original page output with new output from redirected page(s). | |
728 if ($new = $this->checkForMetaRefresh()) { | |
729 $out = $new; | |
730 // We are finished with all meta refresh redirects, so reset the counter. | |
731 $this->metaRefreshCount = 0; | |
732 } | |
733 | |
734 // Log only for JavascriptTestBase tests because for Goutte we log with | |
735 // ::getResponseLogHandler. | |
736 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
737 $html_output = 'GET request to: ' . $url . | |
738 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
739 $html_output .= '<hr />' . $out; | |
740 $html_output .= $this->getHtmlOutputHeaders(); | |
741 $this->htmlOutput($html_output); | |
742 } | |
743 | |
744 return $out; | |
745 } | |
746 | |
747 /** | |
748 * Takes a path and returns an absolute path. | |
749 * | |
750 * @param string $path | |
751 * A path from the Mink controlled browser content. | |
752 * | |
753 * @return string | |
754 * The $path with $base_url prepended, if necessary. | |
755 */ | |
756 protected function getAbsoluteUrl($path) { | |
757 global $base_url, $base_path; | |
758 | |
759 $parts = parse_url($path); | |
760 if (empty($parts['host'])) { | |
761 // Ensure that we have a string (and no xpath object). | |
762 $path = (string) $path; | |
763 // Strip $base_path, if existent. | |
764 $length = strlen($base_path); | |
765 if (substr($path, 0, $length) === $base_path) { | |
766 $path = substr($path, $length); | |
767 } | |
768 // Ensure that we have an absolute path. | |
769 if (empty($path) || $path[0] !== '/') { | |
770 $path = '/' . $path; | |
771 } | |
772 // Finally, prepend the $base_url. | |
773 $path = $base_url . $path; | |
774 } | |
775 return $path; | |
776 } | |
777 | |
778 /** | |
779 * Logs in a user using the Mink controlled browser. | |
780 * | |
781 * If a user is already logged in, then the current user is logged out before | |
782 * logging in the specified user. | |
783 * | |
784 * Please note that neither the current user nor the passed-in user object is | |
785 * populated with data of the logged in user. If you need full access to the | |
786 * user object after logging in, it must be updated manually. If you also need | |
787 * access to the plain-text password of the user (set by drupalCreateUser()), | |
788 * e.g. to log in the same user again, then it must be re-assigned manually. | |
789 * For example: | |
790 * @code | |
791 * // Create a user. | |
792 * $account = $this->drupalCreateUser(array()); | |
793 * $this->drupalLogin($account); | |
794 * // Load real user object. | |
795 * $pass_raw = $account->passRaw; | |
796 * $account = User::load($account->id()); | |
797 * $account->passRaw = $pass_raw; | |
798 * @endcode | |
799 * | |
800 * @param \Drupal\Core\Session\AccountInterface $account | |
801 * User object representing the user to log in. | |
802 * | |
803 * @see drupalCreateUser() | |
804 */ | |
805 protected function drupalLogin(AccountInterface $account) { | |
806 if ($this->loggedInUser) { | |
807 $this->drupalLogout(); | |
808 } | |
809 | |
810 $this->drupalGet('user/login'); | |
811 $this->submitForm([ | |
812 'name' => $account->getUsername(), | |
813 'pass' => $account->passRaw, | |
814 ], t('Log in')); | |
815 | |
816 // @see BrowserTestBase::drupalUserIsLoggedIn() | |
817 $account->sessionId = $this->getSession()->getCookie($this->getSessionName()); | |
818 $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()])); | |
819 | |
820 $this->loggedInUser = $account; | |
821 $this->container->get('current_user')->setAccount($account); | |
822 } | |
823 | |
824 /** | |
825 * Logs a user out of the Mink controlled browser and confirms. | |
826 * | |
827 * Confirms logout by checking the login page. | |
828 */ | |
829 protected function drupalLogout() { | |
830 // Make a request to the logout page, and redirect to the user page, the | |
831 // idea being if you were properly logged out you should be seeing a login | |
832 // screen. | |
833 $assert_session = $this->assertSession(); | |
834 $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); | |
835 $assert_session->fieldExists('name'); | |
836 $assert_session->fieldExists('pass'); | |
837 | |
838 // @see BrowserTestBase::drupalUserIsLoggedIn() | |
839 unset($this->loggedInUser->sessionId); | |
840 $this->loggedInUser = FALSE; | |
841 $this->container->get('current_user')->setAccount(new AnonymousUserSession()); | |
842 } | |
843 | |
844 /** | |
845 * Fills and submits a form. | |
846 * | |
847 * @param array $edit | |
848 * Field data in an associative array. Changes the current input fields | |
849 * (where possible) to the values indicated. | |
850 * | |
851 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
852 * be unchecked. | |
853 * @param string $submit | |
854 * Value of the submit button whose click is to be emulated. For example, | |
855 * 'Save'. The processing of the request depends on this value. For example, | |
856 * a form may have one button with the value 'Save' and another button with | |
857 * the value 'Delete', and execute different code depending on which one is | |
858 * clicked. | |
859 * @param string $form_html_id | |
860 * (optional) HTML ID of the form to be submitted. On some pages | |
861 * there are many identical forms, so just using the value of the submit | |
862 * button is not enough. For example: 'trigger-node-presave-assign-form'. | |
863 * Note that this is not the Drupal $form_id, but rather the HTML ID of the | |
864 * form, which is typically the same thing but with hyphens replacing the | |
865 * underscores. | |
866 */ | |
867 protected function submitForm(array $edit, $submit, $form_html_id = NULL) { | |
868 $assert_session = $this->assertSession(); | |
869 | |
870 // Get the form. | |
871 if (isset($form_html_id)) { | |
872 $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']"); | |
873 $submit_button = $assert_session->buttonExists($submit, $form); | |
874 $action = $form->getAttribute('action'); | |
875 } | |
876 else { | |
877 $submit_button = $assert_session->buttonExists($submit); | |
878 $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); | |
879 $action = $form->getAttribute('action'); | |
880 } | |
881 | |
882 // Edit the form values. | |
883 foreach ($edit as $name => $value) { | |
884 $field = $assert_session->fieldExists($name, $form); | |
885 | |
886 // Provide support for the values '1' and '0' for checkboxes instead of | |
887 // TRUE and FALSE. | |
888 // @todo Get rid of supporting 1/0 by converting all tests cases using | |
889 // this to boolean values. | |
890 $field_type = $field->getAttribute('type'); | |
891 if ($field_type === 'checkbox') { | |
892 $value = (bool) $value; | |
893 } | |
894 | |
895 $field->setValue($value); | |
896 } | |
897 | |
898 // Submit form. | |
899 $this->prepareRequest(); | |
900 $submit_button->press(); | |
901 | |
902 // Ensure that any changes to variables in the other thread are picked up. | |
903 $this->refreshVariables(); | |
904 | |
905 // Check if there are any meta refresh redirects (like Batch API pages). | |
906 if ($this->checkForMetaRefresh()) { | |
907 // We are finished with all meta refresh redirects, so reset the counter. | |
908 $this->metaRefreshCount = 0; | |
909 } | |
910 | |
911 // Log only for JavascriptTestBase tests because for Goutte we log with | |
912 // ::getResponseLogHandler. | |
913 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
914 $out = $this->getSession()->getPage()->getContent(); | |
915 $html_output = 'POST request to: ' . $action . | |
916 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
917 $html_output .= '<hr />' . $out; | |
918 $html_output .= $this->getHtmlOutputHeaders(); | |
919 $this->htmlOutput($html_output); | |
920 } | |
921 | |
922 } | |
923 | |
924 /** | |
925 * Executes a form submission. | |
926 * | |
927 * It will be done as usual POST request with Mink. | |
928 * | |
929 * @param \Drupal\Core\Url|string $path | |
930 * Location of the post form. Either a Drupal path or an absolute path or | |
931 * NULL to post to the current page. For multi-stage forms you can set the | |
932 * path to NULL and have it post to the last received page. Example: | |
933 * | |
934 * @code | |
935 * // First step in form. | |
936 * $edit = array(...); | |
937 * $this->drupalPostForm('some_url', $edit, 'Save'); | |
938 * | |
939 * // Second step in form. | |
940 * $edit = array(...); | |
941 * $this->drupalPostForm(NULL, $edit, 'Save'); | |
942 * @endcode | |
943 * @param array $edit | |
944 * Field data in an associative array. Changes the current input fields | |
945 * (where possible) to the values indicated. | |
946 * | |
947 * When working with form tests, the keys for an $edit element should match | |
948 * the 'name' parameter of the HTML of the form. For example, the 'body' | |
949 * field for a node has the following HTML: | |
950 * @code | |
951 * <textarea id="edit-body-und-0-value" class="text-full form-textarea | |
952 * resize-vertical" placeholder="" cols="60" rows="9" | |
953 * name="body[0][value]"></textarea> | |
954 * @endcode | |
955 * When testing this field using an $edit parameter, the code becomes: | |
956 * @code | |
957 * $edit["body[0][value]"] = 'My test value'; | |
958 * @endcode | |
959 * | |
960 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
961 * be unchecked. Multiple select fields can be tested using 'name[]' and | |
962 * setting each of the desired values in an array: | |
963 * @code | |
964 * $edit = array(); | |
965 * $edit['name[]'] = array('value1', 'value2'); | |
966 * @endcode | |
967 * @todo change $edit to disallow NULL as a value for Drupal 9. | |
968 * https://www.drupal.org/node/2802401 | |
969 * @param string $submit | |
970 * Value of the submit button whose click is to be emulated. For example, | |
971 * 'Save'. The processing of the request depends on this value. For example, | |
972 * a form may have one button with the value 'Save' and another button with | |
973 * the value 'Delete', and execute different code depending on which one is | |
974 * clicked. | |
975 * | |
976 * This function can also be called to emulate an Ajax submission. In this | |
977 * case, this value needs to be an array with the following keys: | |
978 * - path: A path to submit the form values to for Ajax-specific processing. | |
979 * - triggering_element: If the value for the 'path' key is a generic Ajax | |
980 * processing path, this needs to be set to the name of the element. If | |
981 * the name doesn't identify the element uniquely, then this should | |
982 * instead be an array with a single key/value pair, corresponding to the | |
983 * element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder | |
984 * uses this to find the #ajax information for the element, including | |
985 * which specific callback to use for processing the request. | |
986 * | |
987 * This can also be set to NULL in order to emulate an Internet Explorer | |
988 * submission of a form with a single text field, and pressing ENTER in that | |
989 * textfield: under these conditions, no button information is added to the | |
990 * POST data. | |
991 * @param array $options | |
992 * Options to be forwarded to the url generator. | |
993 * | |
994 * @return string | |
995 * (deprecated) The response content after submit form. It is necessary for | |
996 * backwards compatibility and will be removed before Drupal 9.0. You should | |
997 * just use the webAssert object for your assertions. | |
998 */ | |
999 protected function drupalPostForm($path, $edit, $submit, array $options = []) { | |
1000 if (is_object($submit)) { | |
1001 // Cast MarkupInterface objects to string. | |
1002 $submit = (string) $submit; | |
1003 } | |
1004 if ($edit === NULL) { | |
1005 $edit = []; | |
1006 } | |
1007 if (is_array($edit)) { | |
1008 $edit = $this->castSafeStrings($edit); | |
1009 } | |
1010 | |
1011 if (isset($path)) { | |
1012 $this->drupalGet($path, $options); | |
1013 } | |
1014 | |
1015 $this->submitForm($edit, $submit); | |
1016 | |
1017 return $this->getSession()->getPage()->getContent(); | |
1018 } | 535 } |
1019 | 536 |
1020 /** | 537 /** |
1021 * Helper function to get the options of select field. | 538 * Helper function to get the options of select field. |
1022 * | 539 * |
1055 $this->installModulesFromClassProperty($container); | 572 $this->installModulesFromClassProperty($container); |
1056 $this->rebuildAll(); | 573 $this->rebuildAll(); |
1057 } | 574 } |
1058 | 575 |
1059 /** | 576 /** |
1060 * Returns whether a given user account is logged in. | |
1061 * | |
1062 * @param \Drupal\Core\Session\AccountInterface $account | |
1063 * The user account object to check. | |
1064 * | |
1065 * @return bool | |
1066 * Return TRUE if the user is logged in, FALSE otherwise. | |
1067 */ | |
1068 protected function drupalUserIsLoggedIn(AccountInterface $account) { | |
1069 $logged_in = FALSE; | |
1070 | |
1071 if (isset($account->sessionId)) { | |
1072 $session_handler = $this->container->get('session_handler.storage'); | |
1073 $logged_in = (bool) $session_handler->read($account->sessionId); | |
1074 } | |
1075 | |
1076 return $logged_in; | |
1077 } | |
1078 | |
1079 /** | |
1080 * Clicks the element with the given CSS selector. | |
1081 * | |
1082 * @param string $css_selector | |
1083 * The CSS selector identifying the element to click. | |
1084 */ | |
1085 protected function click($css_selector) { | |
1086 $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector)); | |
1087 } | |
1088 | |
1089 /** | |
1090 * Prevents serializing any properties. | 577 * Prevents serializing any properties. |
1091 * | 578 * |
1092 * Browser tests are run in a separate process. To do this PHPUnit creates a | 579 * Browser tests are run in a separate process. To do this PHPUnit creates a |
1093 * script to run the test. If it fails, the test result object will contain a | 580 * script to run the test. If it fails, the test result object will contain a |
1094 * stack trace which includes the test object. It will attempt to serialize | 581 * stack trace which includes the test object. It will attempt to serialize |
1100 * | 587 * |
1101 * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist | 588 * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist |
1102 */ | 589 */ |
1103 public function __sleep() { | 590 public function __sleep() { |
1104 return []; | 591 return []; |
1105 } | |
1106 | |
1107 /** | |
1108 * Logs a HTML output message in a text file. | |
1109 * | |
1110 * The link to the HTML output message will be printed by the results printer. | |
1111 * | |
1112 * @param string $message | |
1113 * The HTML output message to be stored. | |
1114 * | |
1115 * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult() | |
1116 */ | |
1117 protected function htmlOutput($message) { | |
1118 if (!$this->htmlOutputEnabled) { | |
1119 return; | |
1120 } | |
1121 $message = '<hr />ID #' . $this->htmlOutputCounter . ' (<a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter - 1) . '-' . $this->htmlOutputTestId . '.html">Previous</a> | <a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter + 1) . '-' . $this->htmlOutputTestId . '.html">Next</a>)<hr />' . $message; | |
1122 $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html'; | |
1123 file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message); | |
1124 file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++); | |
1125 file_put_contents($this->htmlOutputFile, file_create_url('sites/simpletest/browser_output/' . $html_output_filename) . "\n", FILE_APPEND); | |
1126 } | |
1127 | |
1128 /** | |
1129 * Returns headers in HTML output format. | |
1130 * | |
1131 * @return string | |
1132 * HTML output headers. | |
1133 */ | |
1134 protected function getHtmlOutputHeaders() { | |
1135 return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders()); | |
1136 } | |
1137 | |
1138 /** | |
1139 * Formats HTTP headers as string for HTML output logging. | |
1140 * | |
1141 * @param array[] $headers | |
1142 * Headers that should be formatted. | |
1143 * | |
1144 * @return string | |
1145 * The formatted HTML string. | |
1146 */ | |
1147 protected function formatHtmlOutputHeaders(array $headers) { | |
1148 $flattened_headers = array_map(function ($header) { | |
1149 if (is_array($header)) { | |
1150 return implode(';', array_map('trim', $header)); | |
1151 } | |
1152 else { | |
1153 return $header; | |
1154 } | |
1155 }, $headers); | |
1156 return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>'; | |
1157 } | 592 } |
1158 | 593 |
1159 /** | 594 /** |
1160 * Translates a CSS expression to its XPath equivalent. | 595 * Translates a CSS expression to its XPath equivalent. |
1161 * | 596 * |
1174 protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') { | 609 protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') { |
1175 return (new CssSelectorConverter($html))->toXPath($selector, $prefix); | 610 return (new CssSelectorConverter($html))->toXPath($selector, $prefix); |
1176 } | 611 } |
1177 | 612 |
1178 /** | 613 /** |
1179 * Searches elements using a CSS selector in the raw content. | |
1180 * | |
1181 * The search is relative to the root element (HTML tag normally) of the page. | |
1182 * | |
1183 * @param string $selector | |
1184 * CSS selector to use in the search. | |
1185 * | |
1186 * @return \Behat\Mink\Element\NodeElement[] | |
1187 * The list of elements on the page that match the selector. | |
1188 */ | |
1189 protected function cssSelect($selector) { | |
1190 return $this->getSession()->getPage()->findAll('css', $selector); | |
1191 } | |
1192 | |
1193 /** | |
1194 * Follows a link by complete name. | |
1195 * | |
1196 * Will click the first link found with this link text. | |
1197 * | |
1198 * If the link is discovered and clicked, the test passes. Fail otherwise. | |
1199 * | |
1200 * @param string|\Drupal\Component\Render\MarkupInterface $label | |
1201 * Text between the anchor tags. | |
1202 * @param int $index | |
1203 * (optional) The index number for cases where multiple links have the same | |
1204 * text. Defaults to 0. | |
1205 */ | |
1206 protected function clickLink($label, $index = 0) { | |
1207 $label = (string) $label; | |
1208 $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); | |
1209 $this->assertArrayHasKey($index, $links, 'The link ' . $label . ' was not found on the page.'); | |
1210 $links[$index]->click(); | |
1211 } | |
1212 | |
1213 /** | |
1214 * Retrieves the plain-text content from the current page. | |
1215 */ | |
1216 protected function getTextContent() { | |
1217 return $this->getSession()->getPage()->getText(); | |
1218 } | |
1219 | |
1220 /** | |
1221 * Performs an xpath search on the contents of the internal browser. | 614 * Performs an xpath search on the contents of the internal browser. |
1222 * | 615 * |
1223 * The search is relative to the root element (HTML tag normally) of the page. | 616 * The search is relative to the root element (HTML tag normally) of the page. |
1224 * | 617 * |
1225 * @param string $xpath | 618 * @param string $xpath |
1279 protected function drupalGetHeader($name) { | 672 protected function drupalGetHeader($name) { |
1280 return $this->getSession()->getResponseHeader($name); | 673 return $this->getSession()->getResponseHeader($name); |
1281 } | 674 } |
1282 | 675 |
1283 /** | 676 /** |
1284 * Get the current URL from the browser. | |
1285 * | |
1286 * @return string | |
1287 * The current URL. | |
1288 */ | |
1289 protected function getUrl() { | |
1290 return $this->getSession()->getCurrentUrl(); | |
1291 } | |
1292 | |
1293 /** | |
1294 * Gets the JavaScript drupalSettings variable for the currently-loaded page. | 677 * Gets the JavaScript drupalSettings variable for the currently-loaded page. |
1295 * | 678 * |
1296 * @return array | 679 * @return array |
1297 * The JSON decoded drupalSettings value from the current page. | 680 * The JSON decoded drupalSettings value from the current page. |
1298 */ | 681 */ |
1299 protected function getDrupalSettings() { | 682 protected function getDrupalSettings() { |
1300 $html = $this->getSession()->getPage()->getHtml(); | 683 $html = $this->getSession()->getPage()->getContent(); |
1301 if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) { | 684 if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) { |
1302 return Json::decode($matches[1]); | 685 return Json::decode($matches[1]); |
1303 } | 686 } |
1304 return []; | 687 return []; |
1305 } | 688 } |
1367 $edit[urldecode($key)] = urldecode($value); | 750 $edit[urldecode($key)] = urldecode($value); |
1368 } | 751 } |
1369 return $edit; | 752 return $edit; |
1370 } | 753 } |
1371 | 754 |
1372 /** | |
1373 * Checks for meta refresh tag and if found call drupalGet() recursively. | |
1374 * | |
1375 * This function looks for the http-equiv attribute to be set to "Refresh" and | |
1376 * is case-insensitive. | |
1377 * | |
1378 * @return string|false | |
1379 * Either the new page content or FALSE. | |
1380 */ | |
1381 protected function checkForMetaRefresh() { | |
1382 $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); | |
1383 if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { | |
1384 // Parse the content attribute of the meta tag for the format: | |
1385 // "[delay]: URL=[page_to_redirect_to]". | |
1386 if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) { | |
1387 $this->metaRefreshCount++; | |
1388 return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); | |
1389 } | |
1390 } | |
1391 return FALSE; | |
1392 } | |
1393 | |
1394 } | 755 } |