Mercurial > hg > isophonics-drupal-site
comparison core/tests/Drupal/Tests/BrowserTestBase.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests; | |
4 | |
5 use Behat\Mink\Driver\GoutteDriver; | |
6 use Behat\Mink\Element\Element; | |
7 use Behat\Mink\Mink; | |
8 use Behat\Mink\Selector\SelectorsHandler; | |
9 use Behat\Mink\Session; | |
10 use Drupal\Component\Render\FormattableMarkup; | |
11 use Drupal\Component\Serialization\Json; | |
12 use Drupal\Component\Utility\Html; | |
13 use Drupal\Component\Utility\UrlHelper; | |
14 use Drupal\Core\Database\Database; | |
15 use Drupal\Core\Session\AccountInterface; | |
16 use Drupal\Core\Session\AnonymousUserSession; | |
17 use Drupal\Core\Test\FunctionalTestSetupTrait; | |
18 use Drupal\Core\Test\TestSetupTrait; | |
19 use Drupal\Core\Url; | |
20 use Drupal\Core\Utility\Error; | |
21 use Drupal\FunctionalTests\AssertLegacyTrait; | |
22 use Drupal\Tests\block\Traits\BlockCreationTrait; | |
23 use Drupal\Tests\node\Traits\ContentTypeCreationTrait; | |
24 use Drupal\Tests\node\Traits\NodeCreationTrait; | |
25 use Drupal\Tests\user\Traits\UserCreationTrait; | |
26 use PHPUnit\Framework\TestCase; | |
27 use Psr\Http\Message\RequestInterface; | |
28 use Psr\Http\Message\ResponseInterface; | |
29 use Symfony\Component\CssSelector\CssSelectorConverter; | |
30 | |
31 /** | |
32 * Provides a test case for functional Drupal tests. | |
33 * | |
34 * Tests extending BrowserTestBase must exist in the | |
35 * Drupal\Tests\yourmodule\Functional namespace and live in the | |
36 * modules/yourmodule/tests/src/Functional directory. | |
37 * | |
38 * @ingroup testing | |
39 */ | |
40 abstract class BrowserTestBase extends TestCase { | |
41 | |
42 use FunctionalTestSetupTrait; | |
43 use TestSetupTrait; | |
44 use AssertHelperTrait; | |
45 use BlockCreationTrait { | |
46 placeBlock as drupalPlaceBlock; | |
47 } | |
48 use AssertLegacyTrait; | |
49 use RandomGeneratorTrait; | |
50 use SessionTestTrait; | |
51 use NodeCreationTrait { | |
52 getNodeByTitle as drupalGetNodeByTitle; | |
53 createNode as drupalCreateNode; | |
54 } | |
55 use ContentTypeCreationTrait { | |
56 createContentType as drupalCreateContentType; | |
57 } | |
58 use ConfigTestTrait; | |
59 use TestRequirementsTrait; | |
60 use UserCreationTrait { | |
61 createRole as drupalCreateRole; | |
62 createUser as drupalCreateUser; | |
63 } | |
64 use XdebugRequestTrait; | |
65 | |
66 /** | |
67 * The database prefix of this test run. | |
68 * | |
69 * @var string | |
70 */ | |
71 protected $databasePrefix; | |
72 | |
73 /** | |
74 * Time limit in seconds for the test. | |
75 * | |
76 * @var int | |
77 */ | |
78 protected $timeLimit = 500; | |
79 | |
80 /** | |
81 * The translation file directory for the test environment. | |
82 * | |
83 * This is set in BrowserTestBase::prepareEnvironment(). | |
84 * | |
85 * @var string | |
86 */ | |
87 protected $translationFilesDirectory; | |
88 | |
89 /** | |
90 * The config importer that can be used in a test. | |
91 * | |
92 * @var \Drupal\Core\Config\ConfigImporter | |
93 */ | |
94 protected $configImporter; | |
95 | |
96 /** | |
97 * Modules to enable. | |
98 * | |
99 * The test runner will merge the $modules lists from this class, the class | |
100 * it extends, and so on up the class hierarchy. It is not necessary to | |
101 * include modules in your list that a parent class has already declared. | |
102 * | |
103 * @var string[] | |
104 * | |
105 * @see \Drupal\Tests\BrowserTestBase::installDrupal() | |
106 */ | |
107 protected static $modules = []; | |
108 | |
109 /** | |
110 * The profile to install as a basis for testing. | |
111 * | |
112 * @var string | |
113 */ | |
114 protected $profile = 'testing'; | |
115 | |
116 /** | |
117 * The current user logged in using the Mink controlled browser. | |
118 * | |
119 * @var \Drupal\user\UserInterface | |
120 */ | |
121 protected $loggedInUser = FALSE; | |
122 | |
123 /** | |
124 * An array of custom translations suitable for drupal_rewrite_settings(). | |
125 * | |
126 * @var array | |
127 */ | |
128 protected $customTranslations; | |
129 | |
130 /* | |
131 * Mink class for the default driver to use. | |
132 * | |
133 * Shoud be a fully qualified class name that implements | |
134 * Behat\Mink\Driver\DriverInterface. | |
135 * | |
136 * Value can be overridden using the environment variable MINK_DRIVER_CLASS. | |
137 * | |
138 * @var string. | |
139 */ | |
140 protected $minkDefaultDriverClass = GoutteDriver::class; | |
141 | |
142 /* | |
143 * Mink default driver params. | |
144 * | |
145 * If it's an array its contents are used as constructor params when default | |
146 * Mink driver class is instantiated. | |
147 * | |
148 * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this | |
149 * case that variable should be a JSON array, for example: | |
150 * '["firefox", null, "http://localhost:4444/wd/hub"]'. | |
151 * | |
152 * | |
153 * @var array | |
154 */ | |
155 protected $minkDefaultDriverArgs; | |
156 | |
157 /** | |
158 * Mink session manager. | |
159 * | |
160 * This will not be initialized if there was an error during the test setup. | |
161 * | |
162 * @var \Behat\Mink\Mink|null | |
163 */ | |
164 protected $mink; | |
165 | |
166 /** | |
167 * {@inheritdoc} | |
168 * | |
169 * Browser tests are run in separate processes to prevent collisions between | |
170 * code that may be loaded by tests. | |
171 */ | |
172 protected $runTestInSeparateProcess = TRUE; | |
173 | |
174 /** | |
175 * {@inheritdoc} | |
176 */ | |
177 protected $preserveGlobalState = FALSE; | |
178 | |
179 /** | |
180 * Class name for HTML output logging. | |
181 * | |
182 * @var string | |
183 */ | |
184 protected $htmlOutputClassName; | |
185 | |
186 /** | |
187 * Directory name for HTML output logging. | |
188 * | |
189 * @var string | |
190 */ | |
191 protected $htmlOutputDirectory; | |
192 | |
193 /** | |
194 * Counter storage for HTML output logging. | |
195 * | |
196 * @var string | |
197 */ | |
198 protected $htmlOutputCounterStorage; | |
199 | |
200 /** | |
201 * Counter for HTML output logging. | |
202 * | |
203 * @var int | |
204 */ | |
205 protected $htmlOutputCounter = 1; | |
206 | |
207 /** | |
208 * HTML output output enabled. | |
209 * | |
210 * @var bool | |
211 */ | |
212 protected $htmlOutputEnabled = FALSE; | |
213 | |
214 /** | |
215 * The file name to write the list of URLs to. | |
216 * | |
217 * This file is read by the PHPUnit result printer. | |
218 * | |
219 * @var string | |
220 * | |
221 * @see \Drupal\Tests\Listeners\HtmlOutputPrinter | |
222 */ | |
223 protected $htmlOutputFile; | |
224 | |
225 /** | |
226 * HTML output test ID. | |
227 * | |
228 * @var int | |
229 */ | |
230 protected $htmlOutputTestId; | |
231 | |
232 /** | |
233 * The base URL. | |
234 * | |
235 * @var string | |
236 */ | |
237 protected $baseUrl; | |
238 | |
239 /** | |
240 * The original array of shutdown function callbacks. | |
241 * | |
242 * @var array | |
243 */ | |
244 protected $originalShutdownCallbacks = []; | |
245 | |
246 /** | |
247 * The number of meta refresh redirects to follow, or NULL if unlimited. | |
248 * | |
249 * @var null|int | |
250 */ | |
251 protected $maximumMetaRefreshCount = NULL; | |
252 | |
253 /** | |
254 * The number of meta refresh redirects followed during ::drupalGet(). | |
255 * | |
256 * @var int | |
257 */ | |
258 protected $metaRefreshCount = 0; | |
259 | |
260 /** | |
261 * The app root. | |
262 * | |
263 * @var string | |
264 */ | |
265 protected $root; | |
266 | |
267 /** | |
268 * The original container. | |
269 * | |
270 * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no | |
271 * longer provides the same value. | |
272 * | |
273 * @var \Symfony\Component\DependencyInjection\ContainerInterface | |
274 */ | |
275 protected $originalContainer; | |
276 | |
277 /** | |
278 * {@inheritdoc} | |
279 */ | |
280 public function __construct($name = NULL, array $data = [], $dataName = '') { | |
281 parent::__construct($name, $data, $dataName); | |
282 | |
283 $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); | |
284 } | |
285 | |
286 /** | |
287 * Initializes Mink sessions. | |
288 */ | |
289 protected function initMink() { | |
290 $driver = $this->getDefaultDriverInstance(); | |
291 | |
292 if ($driver instanceof GoutteDriver) { | |
293 // Turn off curl timeout. Having a timeout is not a problem in a normal | |
294 // test running, but it is a problem when debugging. Also, disable SSL | |
295 // peer verification so that testing under HTTPS always works. | |
296 /** @var \GuzzleHttp\Client $client */ | |
297 $client = $this->container->get('http_client_factory')->fromOptions([ | |
298 'timeout' => NULL, | |
299 'verify' => FALSE, | |
300 ]); | |
301 | |
302 // Inject a Guzzle middleware to generate debug output for every request | |
303 // performed in the test. | |
304 $handler_stack = $client->getConfig('handler'); | |
305 $handler_stack->push($this->getResponseLogHandler()); | |
306 | |
307 $driver->getClient()->setClient($client); | |
308 } | |
309 | |
310 $selectors_handler = new SelectorsHandler([ | |
311 'hidden_field_selector' => new HiddenFieldSelector() | |
312 ]); | |
313 $session = new Session($driver, $selectors_handler); | |
314 $this->mink = new Mink(); | |
315 $this->mink->registerSession('default', $session); | |
316 $this->mink->setDefaultSessionName('default'); | |
317 $this->registerSessions(); | |
318 | |
319 // According to the W3C WebDriver specification a cookie can only be set if | |
320 // the cookie domain is equal to the domain of the active document. When the | |
321 // browser starts up the active document is not our domain but 'about:blank' | |
322 // or similar. To be able to set our User-Agent and Xdebug cookies at the | |
323 // start of the test we now do a request to the front page so the active | |
324 // document matches the domain. | |
325 // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie | |
326 // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975 | |
327 $session = $this->getSession(); | |
328 $session->visit($this->baseUrl); | |
329 | |
330 return $session; | |
331 } | |
332 | |
333 /** | |
334 * Gets an instance of the default Mink driver. | |
335 * | |
336 * @return Behat\Mink\Driver\DriverInterface | |
337 * Instance of default Mink driver. | |
338 * | |
339 * @throws \InvalidArgumentException | |
340 * When provided default Mink driver class can't be instantiated. | |
341 */ | |
342 protected function getDefaultDriverInstance() { | |
343 // Get default driver params from environment if availables. | |
344 if ($arg_json = getenv('MINK_DRIVER_ARGS')) { | |
345 $this->minkDefaultDriverArgs = json_decode($arg_json); | |
346 } | |
347 | |
348 // Get and check default driver class from environment if availables. | |
349 if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) { | |
350 if (class_exists($minkDriverClass)) { | |
351 $this->minkDefaultDriverClass = $minkDriverClass; | |
352 } | |
353 else { | |
354 throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class."); | |
355 } | |
356 } | |
357 | |
358 if (is_array($this->minkDefaultDriverArgs)) { | |
359 // Use ReflectionClass to instantiate class with received params. | |
360 $reflector = new \ReflectionClass($this->minkDefaultDriverClass); | |
361 $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs); | |
362 } | |
363 else { | |
364 $driver = new $this->minkDefaultDriverClass(); | |
365 } | |
366 return $driver; | |
367 } | |
368 | |
369 /** | |
370 * Creates the directory to store browser output. | |
371 * | |
372 * Creates the directory to store browser output in if a file to write | |
373 * URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter. | |
374 */ | |
375 protected function initBrowserOutputFile() { | |
376 $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE'); | |
377 $this->htmlOutputEnabled = is_file($browser_output_file); | |
378 if ($this->htmlOutputEnabled) { | |
379 $this->htmlOutputFile = $browser_output_file; | |
380 $this->htmlOutputClassName = str_replace("\\", "_", get_called_class()); | |
381 $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output'; | |
382 if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) { | |
383 file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n"); | |
384 } | |
385 $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter'; | |
386 $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory); | |
387 if (is_file($this->htmlOutputCounterStorage)) { | |
388 $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1; | |
389 } | |
390 } | |
391 } | |
392 | |
393 /** | |
394 * Provides a Guzzle middleware handler to log every response received. | |
395 * | |
396 * @return callable | |
397 * The callable handler that will do the logging. | |
398 */ | |
399 protected function getResponseLogHandler() { | |
400 return function (callable $handler) { | |
401 return function (RequestInterface $request, array $options) use ($handler) { | |
402 return $handler($request, $options) | |
403 ->then(function (ResponseInterface $response) use ($request) { | |
404 if ($this->htmlOutputEnabled) { | |
405 | |
406 $caller = $this->getTestMethodCaller(); | |
407 $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line']; | |
408 $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri(); | |
409 | |
410 // On redirect responses (status code starting with '3') we need | |
411 // to remove the meta tag that would do a browser refresh. We | |
412 // don't want to redirect developers away when they look at the | |
413 // debug output file in their browser. | |
414 $body = $response->getBody(); | |
415 $status_code = (string) $response->getStatusCode(); | |
416 if ($status_code[0] === '3') { | |
417 $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1); | |
418 } | |
419 $html_output .= '<hr />' . $body; | |
420 $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders()); | |
421 | |
422 $this->htmlOutput($html_output); | |
423 } | |
424 return $response; | |
425 }); | |
426 }; | |
427 }; | |
428 } | |
429 | |
430 /** | |
431 * Registers additional Mink sessions. | |
432 * | |
433 * Tests wishing to use a different driver or change the default driver should | |
434 * override this method. | |
435 * | |
436 * @code | |
437 * // Register a new session that uses the MinkPonyDriver. | |
438 * $pony = new MinkPonyDriver(); | |
439 * $session = new Session($pony); | |
440 * $this->mink->registerSession('pony', $session); | |
441 * @endcode | |
442 */ | |
443 protected function registerSessions() {} | |
444 | |
445 /** | |
446 * {@inheritdoc} | |
447 */ | |
448 protected function setUp() { | |
449 parent::setUp(); | |
450 | |
451 $this->setupBaseUrl(); | |
452 | |
453 // Install Drupal test site. | |
454 $this->prepareEnvironment(); | |
455 $this->installDrupal(); | |
456 | |
457 // Setup Mink. | |
458 $session = $this->initMink(); | |
459 | |
460 $cookies = $this->extractCookiesFromRequest(\Drupal::request()); | |
461 foreach ($cookies as $cookie_name => $values) { | |
462 foreach ($values as $value) { | |
463 $session->setCookie($cookie_name, $value); | |
464 } | |
465 } | |
466 | |
467 // Set up the browser test output file. | |
468 $this->initBrowserOutputFile(); | |
469 } | |
470 | |
471 /** | |
472 * Ensures test files are deletable within file_unmanaged_delete_recursive(). | |
473 * | |
474 * Some tests chmod generated files to be read only. During | |
475 * BrowserTestBase::cleanupEnvironment() and other cleanup operations, | |
476 * these files need to get deleted too. | |
477 * | |
478 * @param string $path | |
479 * The file path. | |
480 */ | |
481 public static function filePreDeleteCallback($path) { | |
482 // When the webserver runs with the same system user as phpunit, we can | |
483 // make read-only files writable again. If not, chmod will fail while the | |
484 // file deletion still works if file permissions have been configured | |
485 // correctly. Thus, we ignore any problems while running chmod. | |
486 @chmod($path, 0700); | |
487 } | |
488 | |
489 /** | |
490 * Clean up the Simpletest environment. | |
491 */ | |
492 protected function cleanupEnvironment() { | |
493 // Remove all prefixed tables. | |
494 $original_connection_info = Database::getConnectionInfo('simpletest_original_default'); | |
495 $original_prefix = $original_connection_info['default']['prefix']['default']; | |
496 $test_connection_info = Database::getConnectionInfo('default'); | |
497 $test_prefix = $test_connection_info['default']['prefix']['default']; | |
498 if ($original_prefix != $test_prefix) { | |
499 $tables = Database::getConnection()->schema()->findTables('%'); | |
500 foreach ($tables as $table) { | |
501 if (Database::getConnection()->schema()->dropTable($table)) { | |
502 unset($tables[$table]); | |
503 } | |
504 } | |
505 } | |
506 | |
507 // Delete test site directory. | |
508 file_unmanaged_delete_recursive($this->siteDirectory, [$this, 'filePreDeleteCallback']); | |
509 } | |
510 | |
511 /** | |
512 * {@inheritdoc} | |
513 */ | |
514 protected function tearDown() { | |
515 parent::tearDown(); | |
516 | |
517 // Destroy the testing kernel. | |
518 if (isset($this->kernel)) { | |
519 $this->cleanupEnvironment(); | |
520 $this->kernel->shutdown(); | |
521 } | |
522 | |
523 // Ensure that internal logged in variable is reset. | |
524 $this->loggedInUser = FALSE; | |
525 | |
526 if ($this->mink) { | |
527 $this->mink->stopSessions(); | |
528 } | |
529 | |
530 // Restore original shutdown callbacks. | |
531 if (function_exists('drupal_register_shutdown_function')) { | |
532 $callbacks = &drupal_register_shutdown_function(); | |
533 $callbacks = $this->originalShutdownCallbacks; | |
534 } | |
535 } | |
536 | |
537 /** | |
538 * Returns Mink session. | |
539 * | |
540 * @param string $name | |
541 * (optional) Name of the session. Defaults to the active session. | |
542 * | |
543 * @return \Behat\Mink\Session | |
544 * The active Mink session object. | |
545 */ | |
546 public function getSession($name = NULL) { | |
547 return $this->mink->getSession($name); | |
548 } | |
549 | |
550 /** | |
551 * Returns WebAssert object. | |
552 * | |
553 * @param string $name | |
554 * (optional) Name of the session. Defaults to the active session. | |
555 * | |
556 * @return \Drupal\Tests\WebAssert | |
557 * A new web-assert option for asserting the presence of elements with. | |
558 */ | |
559 public function assertSession($name = NULL) { | |
560 return new WebAssert($this->getSession($name), $this->baseUrl); | |
561 } | |
562 | |
563 /** | |
564 * Prepare for a request to testing site. | |
565 * | |
566 * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is | |
567 * checked by drupal_valid_test_ua(). | |
568 * | |
569 * @see drupal_valid_test_ua() | |
570 */ | |
571 protected function prepareRequest() { | |
572 $session = $this->getSession(); | |
573 $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); | |
574 } | |
575 | |
576 /** | |
577 * Builds an a absolute URL from a system path or a URL object. | |
578 * | |
579 * @param string|\Drupal\Core\Url $path | |
580 * A system path or a URL. | |
581 * @param array $options | |
582 * Options to be passed to Url::fromUri(). | |
583 * | |
584 * @return string | |
585 * An absolute URL stsring. | |
586 */ | |
587 protected function buildUrl($path, array $options = []) { | |
588 if ($path instanceof Url) { | |
589 $url_options = $path->getOptions(); | |
590 $options = $url_options + $options; | |
591 $path->setOptions($options); | |
592 return $path->setAbsolute()->toString(); | |
593 } | |
594 // The URL generator service is not necessarily available yet; e.g., in | |
595 // interactive installer tests. | |
596 elseif ($this->container->has('url_generator')) { | |
597 $force_internal = isset($options['external']) && $options['external'] == FALSE; | |
598 if (!$force_internal && UrlHelper::isExternal($path)) { | |
599 return Url::fromUri($path, $options)->toString(); | |
600 } | |
601 else { | |
602 $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path; | |
603 // Path processing is needed for language prefixing. Skip it when a | |
604 // path that may look like an external URL is being used as internal. | |
605 $options['path_processing'] = !$force_internal; | |
606 return Url::fromUri($uri, $options) | |
607 ->setAbsolute() | |
608 ->toString(); | |
609 } | |
610 } | |
611 else { | |
612 return $this->getAbsoluteUrl($path); | |
613 } | |
614 } | |
615 | |
616 /** | |
617 * Retrieves a Drupal path or an absolute path. | |
618 * | |
619 * @param string|\Drupal\Core\Url $path | |
620 * Drupal path or URL to load into Mink controlled browser. | |
621 * @param array $options | |
622 * (optional) Options to be forwarded to the url generator. | |
623 * @param string[] $headers | |
624 * An array containing additional HTTP request headers, the array keys are | |
625 * the header names and the array values the header values. This is useful | |
626 * to set for example the "Accept-Language" header for requesting the page | |
627 * in a different language. Note that not all headers are supported, for | |
628 * example the "Accept" header is always overridden by the browser. For | |
629 * testing REST APIs it is recommended to directly use an HTTP client such | |
630 * as Guzzle instead. | |
631 * | |
632 * @return string | |
633 * The retrieved HTML string, also available as $this->getRawContent() | |
634 */ | |
635 protected function drupalGet($path, array $options = [], array $headers = []) { | |
636 $options['absolute'] = TRUE; | |
637 $url = $this->buildUrl($path, $options); | |
638 | |
639 $session = $this->getSession(); | |
640 | |
641 $this->prepareRequest(); | |
642 foreach ($headers as $header_name => $header_value) { | |
643 $session->setRequestHeader($header_name, $header_value); | |
644 } | |
645 | |
646 $session->visit($url); | |
647 $out = $session->getPage()->getContent(); | |
648 | |
649 // Ensure that any changes to variables in the other thread are picked up. | |
650 $this->refreshVariables(); | |
651 | |
652 // Replace original page output with new output from redirected page(s). | |
653 if ($new = $this->checkForMetaRefresh()) { | |
654 $out = $new; | |
655 // We are finished with all meta refresh redirects, so reset the counter. | |
656 $this->metaRefreshCount = 0; | |
657 } | |
658 | |
659 // Log only for JavascriptTestBase tests because for Goutte we log with | |
660 // ::getResponseLogHandler. | |
661 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
662 $html_output = 'GET request to: ' . $url . | |
663 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
664 $html_output .= '<hr />' . $out; | |
665 $html_output .= $this->getHtmlOutputHeaders(); | |
666 $this->htmlOutput($html_output); | |
667 } | |
668 | |
669 return $out; | |
670 } | |
671 | |
672 /** | |
673 * Takes a path and returns an absolute path. | |
674 * | |
675 * @param string $path | |
676 * A path from the Mink controlled browser content. | |
677 * | |
678 * @return string | |
679 * The $path with $base_url prepended, if necessary. | |
680 */ | |
681 protected function getAbsoluteUrl($path) { | |
682 global $base_url, $base_path; | |
683 | |
684 $parts = parse_url($path); | |
685 if (empty($parts['host'])) { | |
686 // Ensure that we have a string (and no xpath object). | |
687 $path = (string) $path; | |
688 // Strip $base_path, if existent. | |
689 $length = strlen($base_path); | |
690 if (substr($path, 0, $length) === $base_path) { | |
691 $path = substr($path, $length); | |
692 } | |
693 // Ensure that we have an absolute path. | |
694 if (empty($path) || $path[0] !== '/') { | |
695 $path = '/' . $path; | |
696 } | |
697 // Finally, prepend the $base_url. | |
698 $path = $base_url . $path; | |
699 } | |
700 return $path; | |
701 } | |
702 | |
703 /** | |
704 * Logs in a user using the Mink controlled browser. | |
705 * | |
706 * If a user is already logged in, then the current user is logged out before | |
707 * logging in the specified user. | |
708 * | |
709 * Please note that neither the current user nor the passed-in user object is | |
710 * populated with data of the logged in user. If you need full access to the | |
711 * user object after logging in, it must be updated manually. If you also need | |
712 * access to the plain-text password of the user (set by drupalCreateUser()), | |
713 * e.g. to log in the same user again, then it must be re-assigned manually. | |
714 * For example: | |
715 * @code | |
716 * // Create a user. | |
717 * $account = $this->drupalCreateUser(array()); | |
718 * $this->drupalLogin($account); | |
719 * // Load real user object. | |
720 * $pass_raw = $account->passRaw; | |
721 * $account = User::load($account->id()); | |
722 * $account->passRaw = $pass_raw; | |
723 * @endcode | |
724 * | |
725 * @param \Drupal\Core\Session\AccountInterface $account | |
726 * User object representing the user to log in. | |
727 * | |
728 * @see drupalCreateUser() | |
729 */ | |
730 protected function drupalLogin(AccountInterface $account) { | |
731 if ($this->loggedInUser) { | |
732 $this->drupalLogout(); | |
733 } | |
734 | |
735 $this->drupalGet('user/login'); | |
736 $this->submitForm([ | |
737 'name' => $account->getUsername(), | |
738 'pass' => $account->passRaw, | |
739 ], t('Log in')); | |
740 | |
741 // @see BrowserTestBase::drupalUserIsLoggedIn() | |
742 $account->sessionId = $this->getSession()->getCookie($this->getSessionName()); | |
743 $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()])); | |
744 | |
745 $this->loggedInUser = $account; | |
746 $this->container->get('current_user')->setAccount($account); | |
747 } | |
748 | |
749 /** | |
750 * Logs a user out of the Mink controlled browser and confirms. | |
751 * | |
752 * Confirms logout by checking the login page. | |
753 */ | |
754 protected function drupalLogout() { | |
755 // Make a request to the logout page, and redirect to the user page, the | |
756 // idea being if you were properly logged out you should be seeing a login | |
757 // screen. | |
758 $assert_session = $this->assertSession(); | |
759 $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); | |
760 $assert_session->fieldExists('name'); | |
761 $assert_session->fieldExists('pass'); | |
762 | |
763 // @see BrowserTestBase::drupalUserIsLoggedIn() | |
764 unset($this->loggedInUser->sessionId); | |
765 $this->loggedInUser = FALSE; | |
766 $this->container->get('current_user')->setAccount(new AnonymousUserSession()); | |
767 } | |
768 | |
769 /** | |
770 * Fills and submits a form. | |
771 * | |
772 * @param array $edit | |
773 * Field data in an associative array. Changes the current input fields | |
774 * (where possible) to the values indicated. | |
775 * | |
776 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
777 * be unchecked. | |
778 * @param string $submit | |
779 * Value of the submit button whose click is to be emulated. For example, | |
780 * t('Save'). The processing of the request depends on this value. For | |
781 * example, a form may have one button with the value t('Save') and another | |
782 * button with the value t('Delete'), and execute different code depending | |
783 * on which one is clicked. | |
784 * @param string $form_html_id | |
785 * (optional) HTML ID of the form to be submitted. On some pages | |
786 * there are many identical forms, so just using the value of the submit | |
787 * button is not enough. For example: 'trigger-node-presave-assign-form'. | |
788 * Note that this is not the Drupal $form_id, but rather the HTML ID of the | |
789 * form, which is typically the same thing but with hyphens replacing the | |
790 * underscores. | |
791 */ | |
792 protected function submitForm(array $edit, $submit, $form_html_id = NULL) { | |
793 $assert_session = $this->assertSession(); | |
794 | |
795 // Get the form. | |
796 if (isset($form_html_id)) { | |
797 $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']"); | |
798 $submit_button = $assert_session->buttonExists($submit, $form); | |
799 $action = $form->getAttribute('action'); | |
800 } | |
801 else { | |
802 $submit_button = $assert_session->buttonExists($submit); | |
803 $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); | |
804 $action = $form->getAttribute('action'); | |
805 } | |
806 | |
807 // Edit the form values. | |
808 foreach ($edit as $name => $value) { | |
809 $field = $assert_session->fieldExists($name, $form); | |
810 | |
811 // Provide support for the values '1' and '0' for checkboxes instead of | |
812 // TRUE and FALSE. | |
813 // @todo Get rid of supporting 1/0 by converting all tests cases using | |
814 // this to boolean values. | |
815 $field_type = $field->getAttribute('type'); | |
816 if ($field_type === 'checkbox') { | |
817 $value = (bool) $value; | |
818 } | |
819 | |
820 $field->setValue($value); | |
821 } | |
822 | |
823 // Submit form. | |
824 $this->prepareRequest(); | |
825 $submit_button->press(); | |
826 | |
827 // Ensure that any changes to variables in the other thread are picked up. | |
828 $this->refreshVariables(); | |
829 | |
830 // Check if there are any meta refresh redirects (like Batch API pages). | |
831 if ($this->checkForMetaRefresh()) { | |
832 // We are finished with all meta refresh redirects, so reset the counter. | |
833 $this->metaRefreshCount = 0; | |
834 } | |
835 | |
836 // Log only for JavascriptTestBase tests because for Goutte we log with | |
837 // ::getResponseLogHandler. | |
838 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { | |
839 $out = $this->getSession()->getPage()->getContent(); | |
840 $html_output = 'POST request to: ' . $action . | |
841 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); | |
842 $html_output .= '<hr />' . $out; | |
843 $html_output .= $this->getHtmlOutputHeaders(); | |
844 $this->htmlOutput($html_output); | |
845 } | |
846 | |
847 } | |
848 | |
849 /** | |
850 * Executes a form submission. | |
851 * | |
852 * It will be done as usual POST request with Mink. | |
853 * | |
854 * @param \Drupal\Core\Url|string $path | |
855 * Location of the post form. Either a Drupal path or an absolute path or | |
856 * NULL to post to the current page. For multi-stage forms you can set the | |
857 * path to NULL and have it post to the last received page. Example: | |
858 * | |
859 * @code | |
860 * // First step in form. | |
861 * $edit = array(...); | |
862 * $this->drupalPostForm('some_url', $edit, t('Save')); | |
863 * | |
864 * // Second step in form. | |
865 * $edit = array(...); | |
866 * $this->drupalPostForm(NULL, $edit, t('Save')); | |
867 * @endcode | |
868 * @param array $edit | |
869 * Field data in an associative array. Changes the current input fields | |
870 * (where possible) to the values indicated. | |
871 * | |
872 * When working with form tests, the keys for an $edit element should match | |
873 * the 'name' parameter of the HTML of the form. For example, the 'body' | |
874 * field for a node has the following HTML: | |
875 * @code | |
876 * <textarea id="edit-body-und-0-value" class="text-full form-textarea | |
877 * resize-vertical" placeholder="" cols="60" rows="9" | |
878 * name="body[0][value]"></textarea> | |
879 * @endcode | |
880 * When testing this field using an $edit parameter, the code becomes: | |
881 * @code | |
882 * $edit["body[0][value]"] = 'My test value'; | |
883 * @endcode | |
884 * | |
885 * A checkbox can be set to TRUE to be checked and should be set to FALSE to | |
886 * be unchecked. Multiple select fields can be tested using 'name[]' and | |
887 * setting each of the desired values in an array: | |
888 * @code | |
889 * $edit = array(); | |
890 * $edit['name[]'] = array('value1', 'value2'); | |
891 * @endcode | |
892 * @todo change $edit to disallow NULL as a value for Drupal 9. | |
893 * https://www.drupal.org/node/2802401 | |
894 * @param string $submit | |
895 * Value of the submit button whose click is to be emulated. For example, | |
896 * t('Save'). The processing of the request depends on this value. For | |
897 * example, a form may have one button with the value t('Save') and another | |
898 * button with the value t('Delete'), and execute different code depending | |
899 * on which one is clicked. | |
900 * | |
901 * This function can also be called to emulate an Ajax submission. In this | |
902 * case, this value needs to be an array with the following keys: | |
903 * - path: A path to submit the form values to for Ajax-specific processing. | |
904 * - triggering_element: If the value for the 'path' key is a generic Ajax | |
905 * processing path, this needs to be set to the name of the element. If | |
906 * the name doesn't identify the element uniquely, then this should | |
907 * instead be an array with a single key/value pair, corresponding to the | |
908 * element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder | |
909 * uses this to find the #ajax information for the element, including | |
910 * which specific callback to use for processing the request. | |
911 * | |
912 * This can also be set to NULL in order to emulate an Internet Explorer | |
913 * submission of a form with a single text field, and pressing ENTER in that | |
914 * textfield: under these conditions, no button information is added to the | |
915 * POST data. | |
916 * @param array $options | |
917 * Options to be forwarded to the url generator. | |
918 * | |
919 * @return string | |
920 * (deprecated) The response content after submit form. It is necessary for | |
921 * backwards compatibility and will be removed before Drupal 9.0. You should | |
922 * just use the webAssert object for your assertions. | |
923 */ | |
924 protected function drupalPostForm($path, $edit, $submit, array $options = []) { | |
925 if (is_object($submit)) { | |
926 // Cast MarkupInterface objects to string. | |
927 $submit = (string) $submit; | |
928 } | |
929 if ($edit === NULL) { | |
930 $edit = []; | |
931 } | |
932 if (is_array($edit)) { | |
933 $edit = $this->castSafeStrings($edit); | |
934 } | |
935 | |
936 if (isset($path)) { | |
937 $this->drupalGet($path, $options); | |
938 } | |
939 | |
940 $this->submitForm($edit, $submit); | |
941 | |
942 return $this->getSession()->getPage()->getContent(); | |
943 } | |
944 | |
945 /** | |
946 * Helper function to get the options of select field. | |
947 * | |
948 * @param \Behat\Mink\Element\NodeElement|string $select | |
949 * Name, ID, or Label of select field to assert. | |
950 * @param \Behat\Mink\Element\Element $container | |
951 * (optional) Container element to check against. Defaults to current page. | |
952 * | |
953 * @return array | |
954 * Associative array of option keys and values. | |
955 */ | |
956 protected function getOptions($select, Element $container = NULL) { | |
957 if (is_string($select)) { | |
958 $select = $this->assertSession()->selectExists($select, $container); | |
959 } | |
960 $options = []; | |
961 /* @var \Behat\Mink\Element\NodeElement $option */ | |
962 foreach ($select->findAll('xpath', '//option') as $option) { | |
963 $label = $option->getText(); | |
964 $value = $option->getAttribute('value') ?: $label; | |
965 $options[$value] = $label; | |
966 } | |
967 return $options; | |
968 } | |
969 | |
970 /** | |
971 * Installs Drupal into the Simpletest site. | |
972 */ | |
973 public function installDrupal() { | |
974 $this->initUserSession(); | |
975 $this->prepareSettings(); | |
976 $this->doInstall(); | |
977 $this->initSettings(); | |
978 $container = $this->initKernel(\Drupal::request()); | |
979 $this->initConfig($container); | |
980 $this->installModulesFromClassProperty($container); | |
981 $this->rebuildAll(); | |
982 } | |
983 | |
984 /** | |
985 * Returns whether a given user account is logged in. | |
986 * | |
987 * @param \Drupal\Core\Session\AccountInterface $account | |
988 * The user account object to check. | |
989 * | |
990 * @return bool | |
991 * Return TRUE if the user is logged in, FALSE otherwise. | |
992 */ | |
993 protected function drupalUserIsLoggedIn(AccountInterface $account) { | |
994 $logged_in = FALSE; | |
995 | |
996 if (isset($account->sessionId)) { | |
997 $session_handler = $this->container->get('session_handler.storage'); | |
998 $logged_in = (bool) $session_handler->read($account->sessionId); | |
999 } | |
1000 | |
1001 return $logged_in; | |
1002 } | |
1003 | |
1004 /** | |
1005 * Clicks the element with the given CSS selector. | |
1006 * | |
1007 * @param string $css_selector | |
1008 * The CSS selector identifying the element to click. | |
1009 */ | |
1010 protected function click($css_selector) { | |
1011 $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector)); | |
1012 } | |
1013 | |
1014 /** | |
1015 * Prevents serializing any properties. | |
1016 * | |
1017 * Browser tests are run in a separate process. To do this PHPUnit creates a | |
1018 * script to run the test. If it fails, the test result object will contain a | |
1019 * stack trace which includes the test object. It will attempt to serialize | |
1020 * it. Returning an empty array prevents it from serializing anything it | |
1021 * should not. | |
1022 * | |
1023 * @return array | |
1024 * An empty array. | |
1025 * | |
1026 * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist | |
1027 */ | |
1028 public function __sleep() { | |
1029 return []; | |
1030 } | |
1031 | |
1032 /** | |
1033 * Logs a HTML output message in a text file. | |
1034 * | |
1035 * The link to the HTML output message will be printed by the results printer. | |
1036 * | |
1037 * @param string $message | |
1038 * The HTML output message to be stored. | |
1039 * | |
1040 * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult() | |
1041 */ | |
1042 protected function htmlOutput($message) { | |
1043 if (!$this->htmlOutputEnabled) { | |
1044 return; | |
1045 } | |
1046 $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; | |
1047 $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html'; | |
1048 file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message); | |
1049 file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++); | |
1050 file_put_contents($this->htmlOutputFile, file_create_url('sites/simpletest/browser_output/' . $html_output_filename) . "\n", FILE_APPEND); | |
1051 } | |
1052 | |
1053 /** | |
1054 * Returns headers in HTML output format. | |
1055 * | |
1056 * @return string | |
1057 * HTML output headers. | |
1058 */ | |
1059 protected function getHtmlOutputHeaders() { | |
1060 return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders()); | |
1061 } | |
1062 | |
1063 /** | |
1064 * Formats HTTP headers as string for HTML output logging. | |
1065 * | |
1066 * @param array[] $headers | |
1067 * Headers that should be formatted. | |
1068 * | |
1069 * @return string | |
1070 * The formatted HTML string. | |
1071 */ | |
1072 protected function formatHtmlOutputHeaders(array $headers) { | |
1073 $flattened_headers = array_map(function ($header) { | |
1074 if (is_array($header)) { | |
1075 return implode(';', array_map('trim', $header)); | |
1076 } | |
1077 else { | |
1078 return $header; | |
1079 } | |
1080 }, $headers); | |
1081 return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>'; | |
1082 } | |
1083 | |
1084 /** | |
1085 * Translates a CSS expression to its XPath equivalent. | |
1086 * | |
1087 * The search is relative to the root element (HTML tag normally) of the page. | |
1088 * | |
1089 * @param string $selector | |
1090 * CSS selector to use in the search. | |
1091 * @param bool $html | |
1092 * (optional) Enables HTML support. Disable it for XML documents. | |
1093 * @param string $prefix | |
1094 * (optional) The prefix for the XPath expression. | |
1095 * | |
1096 * @return string | |
1097 * The equivalent XPath of a CSS expression. | |
1098 */ | |
1099 protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') { | |
1100 return (new CssSelectorConverter($html))->toXPath($selector, $prefix); | |
1101 } | |
1102 | |
1103 /** | |
1104 * Searches elements using a CSS selector in the raw content. | |
1105 * | |
1106 * The search is relative to the root element (HTML tag normally) of the page. | |
1107 * | |
1108 * @param string $selector | |
1109 * CSS selector to use in the search. | |
1110 * | |
1111 * @return \Behat\Mink\Element\NodeElement[] | |
1112 * The list of elements on the page that match the selector. | |
1113 */ | |
1114 protected function cssSelect($selector) { | |
1115 return $this->getSession()->getPage()->findAll('css', $selector); | |
1116 } | |
1117 | |
1118 /** | |
1119 * Follows a link by complete name. | |
1120 * | |
1121 * Will click the first link found with this link text. | |
1122 * | |
1123 * If the link is discovered and clicked, the test passes. Fail otherwise. | |
1124 * | |
1125 * @param string|\Drupal\Component\Render\MarkupInterface $label | |
1126 * Text between the anchor tags. | |
1127 * @param int $index | |
1128 * (optional) The index number for cases where multiple links have the same | |
1129 * text. Defaults to 0. | |
1130 */ | |
1131 protected function clickLink($label, $index = 0) { | |
1132 $label = (string) $label; | |
1133 $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); | |
1134 $links[$index]->click(); | |
1135 } | |
1136 | |
1137 /** | |
1138 * Retrieves the plain-text content from the current page. | |
1139 */ | |
1140 protected function getTextContent() { | |
1141 return $this->getSession()->getPage()->getText(); | |
1142 } | |
1143 | |
1144 /** | |
1145 * Performs an xpath search on the contents of the internal browser. | |
1146 * | |
1147 * The search is relative to the root element (HTML tag normally) of the page. | |
1148 * | |
1149 * @param string $xpath | |
1150 * The xpath string to use in the search. | |
1151 * @param array $arguments | |
1152 * An array of arguments with keys in the form ':name' matching the | |
1153 * placeholders in the query. The values may be either strings or numeric | |
1154 * values. | |
1155 * | |
1156 * @return \Behat\Mink\Element\NodeElement[] | |
1157 * The list of elements matching the xpath expression. | |
1158 */ | |
1159 protected function xpath($xpath, array $arguments = []) { | |
1160 $xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments); | |
1161 return $this->getSession()->getPage()->findAll('xpath', $xpath); | |
1162 } | |
1163 | |
1164 /** | |
1165 * Configuration accessor for tests. Returns non-overridden configuration. | |
1166 * | |
1167 * @param string $name | |
1168 * Configuration name. | |
1169 * | |
1170 * @return \Drupal\Core\Config\Config | |
1171 * The configuration object with original configuration data. | |
1172 */ | |
1173 protected function config($name) { | |
1174 return $this->container->get('config.factory')->getEditable($name); | |
1175 } | |
1176 | |
1177 /** | |
1178 * Returns all response headers. | |
1179 * | |
1180 * @return array | |
1181 * The HTTP headers values. | |
1182 * | |
1183 * @deprecated Scheduled for removal in Drupal 9.0.0. | |
1184 * Use $this->getSession()->getResponseHeaders() instead. | |
1185 */ | |
1186 protected function drupalGetHeaders() { | |
1187 return $this->getSession()->getResponseHeaders(); | |
1188 } | |
1189 | |
1190 /** | |
1191 * Gets the value of an HTTP response header. | |
1192 * | |
1193 * If multiple requests were required to retrieve the page, only the headers | |
1194 * from the last request will be checked by default. | |
1195 * | |
1196 * @param string $name | |
1197 * The name of the header to retrieve. Names are case-insensitive (see RFC | |
1198 * 2616 section 4.2). | |
1199 * | |
1200 * @return string|null | |
1201 * The HTTP header value or NULL if not found. | |
1202 */ | |
1203 protected function drupalGetHeader($name) { | |
1204 return $this->getSession()->getResponseHeader($name); | |
1205 } | |
1206 | |
1207 /** | |
1208 * Get the current URL from the browser. | |
1209 * | |
1210 * @return string | |
1211 * The current URL. | |
1212 */ | |
1213 protected function getUrl() { | |
1214 return $this->getSession()->getCurrentUrl(); | |
1215 } | |
1216 | |
1217 /** | |
1218 * Gets the JavaScript drupalSettings variable for the currently-loaded page. | |
1219 * | |
1220 * @return array | |
1221 * The JSON decoded drupalSettings value from the current page. | |
1222 */ | |
1223 protected function getDrupalSettings() { | |
1224 $html = $this->getSession()->getPage()->getHtml(); | |
1225 if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) { | |
1226 return Json::decode($matches[1]); | |
1227 } | |
1228 return []; | |
1229 } | |
1230 | |
1231 /** | |
1232 * {@inheritdoc} | |
1233 */ | |
1234 public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) { | |
1235 // Cast objects implementing MarkupInterface to string instead of | |
1236 // relying on PHP casting them to string depending on what they are being | |
1237 // comparing with. | |
1238 $expected = static::castSafeStrings($expected); | |
1239 $actual = static::castSafeStrings($actual); | |
1240 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); | |
1241 } | |
1242 | |
1243 /** | |
1244 * Retrieves the current calling line in the class under test. | |
1245 * | |
1246 * @return array | |
1247 * An associative array with keys 'file', 'line' and 'function'. | |
1248 */ | |
1249 protected function getTestMethodCaller() { | |
1250 $backtrace = debug_backtrace(); | |
1251 // Find the test class that has the test method. | |
1252 while ($caller = Error::getLastCaller($backtrace)) { | |
1253 if (isset($caller['class']) && $caller['class'] === get_class($this)) { | |
1254 break; | |
1255 } | |
1256 // If the test method is implemented by a test class's parent then the | |
1257 // class name of $this will not be part of the backtrace. | |
1258 // In that case we process the backtrace until the caller is not a | |
1259 // subclass of $this and return the previous caller. | |
1260 if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) { | |
1261 // Return the last caller since that has to be the test class. | |
1262 $caller = $last_caller; | |
1263 break; | |
1264 } | |
1265 // Otherwise we have not reached our test class yet: save the last caller | |
1266 // and remove an element from to backtrace to process the next call. | |
1267 $last_caller = $caller; | |
1268 array_shift($backtrace); | |
1269 } | |
1270 | |
1271 return $caller; | |
1272 } | |
1273 | |
1274 /** | |
1275 * Checks for meta refresh tag and if found call drupalGet() recursively. | |
1276 * | |
1277 * This function looks for the http-equiv attribute to be set to "Refresh" and | |
1278 * is case-insensitive. | |
1279 * | |
1280 * @return string|false | |
1281 * Either the new page content or FALSE. | |
1282 */ | |
1283 protected function checkForMetaRefresh() { | |
1284 $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); | |
1285 if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { | |
1286 // Parse the content attribute of the meta tag for the format: | |
1287 // "[delay]: URL=[page_to_redirect_to]". | |
1288 if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) { | |
1289 $this->metaRefreshCount++; | |
1290 return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); | |
1291 } | |
1292 } | |
1293 return FALSE; | |
1294 } | |
1295 | |
1296 } |