Interim commit.
[yaffs-website] / web / core / modules / simpletest / src / WebTestBase.php
1 <?php
2
3 namespace Drupal\simpletest;
4
5 use Drupal\block\Entity\Block;
6 use Drupal\Component\Serialization\Json;
7 use Drupal\Component\Utility\Html;
8 use Drupal\Component\Utility\NestedArray;
9 use Drupal\Component\Utility\UrlHelper;
10 use Drupal\Component\Utility\SafeMarkup;
11 use Drupal\Core\Database\Database;
12 use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
13 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
14 use Drupal\Core\Session\AccountInterface;
15 use Drupal\Core\Session\AnonymousUserSession;
16 use Drupal\Core\Test\AssertMailTrait;
17 use Drupal\Core\Test\FunctionalTestSetupTrait;
18 use Drupal\Core\Url;
19 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
20 use Drupal\Tests\EntityViewTrait;
21 use Drupal\Tests\Traits\Core\CronRunTrait;
22 use Drupal\Tests\TestFileCreationTrait;
23 use Drupal\Tests\XdebugRequestTrait;
24 use Zend\Diactoros\Uri;
25
26 /**
27  * Test case for typical Drupal tests.
28  *
29  * @ingroup testing
30  */
31 abstract class WebTestBase extends TestBase {
32
33   use FunctionalTestSetupTrait;
34   use AssertContentTrait;
35   use TestFileCreationTrait {
36     getTestFiles as drupalGetTestFiles;
37     compareFiles as drupalCompareFiles;
38   }
39   use AssertPageCacheContextsAndTagsTrait;
40   use BlockCreationTrait {
41     placeBlock as drupalPlaceBlock;
42   }
43   use ContentTypeCreationTrait {
44     createContentType as drupalCreateContentType;
45   }
46   use CronRunTrait;
47   use AssertMailTrait {
48     getMails as drupalGetMails;
49   }
50   use NodeCreationTrait {
51     getNodeByTitle as drupalGetNodeByTitle;
52     createNode as drupalCreateNode;
53   }
54   use UserCreationTrait {
55     createUser as drupalCreateUser;
56     createRole as drupalCreateRole;
57     createAdminRole as drupalCreateAdminRole;
58   }
59
60   use XdebugRequestTrait;
61   use EntityViewTrait {
62     buildEntityView as drupalBuildEntityView;
63   }
64
65   /**
66    * The profile to install as a basis for testing.
67    *
68    * @var string
69    */
70   protected $profile = 'testing';
71
72   /**
73    * The URL currently loaded in the internal browser.
74    *
75    * @var string
76    */
77   protected $url;
78
79   /**
80    * The handle of the current cURL connection.
81    *
82    * @var resource
83    */
84   protected $curlHandle;
85
86   /**
87    * Whether or not to assert the presence of the X-Drupal-Ajax-Token.
88    *
89    * @var bool
90    */
91   protected $assertAjaxHeader = TRUE;
92
93   /**
94    * The headers of the page currently loaded in the internal browser.
95    *
96    * @var Array
97    */
98   protected $headers;
99
100   /**
101    * The cookies of the page currently loaded in the internal browser.
102    *
103    * @var array
104    */
105   protected $cookies = [];
106
107   /**
108    * Indicates that headers should be dumped if verbose output is enabled.
109    *
110    * Headers are dumped to verbose by drupalGet(), drupalHead(), and
111    * drupalPostForm().
112    *
113    * @var bool
114    */
115   protected $dumpHeaders = FALSE;
116
117   /**
118    * The current user logged in using the internal browser.
119    *
120    * @var \Drupal\Core\Session\AccountInterface|bool
121    */
122   protected $loggedInUser = FALSE;
123
124   /**
125    * The current cookie file used by cURL.
126    *
127    * We do not reuse the cookies in further runs, so we do not need a file
128    * but we still need cookie handling, so we set the jar to NULL.
129    */
130   protected $cookieFile = NULL;
131
132   /**
133    * Additional cURL options.
134    *
135    * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
136    * is set.
137    */
138   protected $additionalCurlOptions = [];
139
140   /**
141    * The original batch, before it was changed for testing purposes.
142    *
143    * @var array
144    */
145   protected $originalBatch;
146
147   /**
148    * The original user, before it was changed to a clean uid = 1 for testing.
149    *
150    * @var object
151    */
152   protected $originalUser = NULL;
153
154   /**
155    * The original shutdown handlers array, before it was cleaned for testing.
156    *
157    * @var array
158    */
159   protected $originalShutdownCallbacks = [];
160
161   /**
162    * The current session ID, if available.
163    */
164   protected $sessionId = NULL;
165
166   /**
167    * The maximum number of redirects to follow when handling responses.
168    */
169   protected $maximumRedirects = 5;
170
171   /**
172    * The number of redirects followed during the handling of a request.
173    */
174   protected $redirectCount;
175
176
177   /**
178    * The number of meta refresh redirects to follow, or NULL if unlimited.
179    *
180    * @var null|int
181    */
182   protected $maximumMetaRefreshCount = NULL;
183
184   /**
185    * The number of meta refresh redirects followed during ::drupalGet().
186    *
187    * @var int
188    */
189   protected $metaRefreshCount = 0;
190
191   /**
192    * Cookies to set on curl requests.
193    *
194    * @var array
195    */
196   protected $curlCookies = [];
197
198   /**
199    * An array of custom translations suitable for drupal_rewrite_settings().
200    *
201    * @var array
202    */
203   protected $customTranslations;
204
205   /**
206    * Constructor for \Drupal\simpletest\WebTestBase.
207    */
208   public function __construct($test_id = NULL) {
209     parent::__construct($test_id);
210     $this->skipClasses[__CLASS__] = TRUE;
211     $this->classLoader = require DRUPAL_ROOT . '/autoload.php';
212   }
213
214   /**
215    * Checks to see whether a block appears on the page.
216    *
217    * @param \Drupal\block\Entity\Block $block
218    *   The block entity to find on the page.
219    */
220   protected function assertBlockAppears(Block $block) {
221     $result = $this->findBlockInstance($block);
222     $this->assertTrue(!empty($result), format_string('Ensure the block @id appears on the page', ['@id' => $block->id()]));
223   }
224
225   /**
226    * Checks to see whether a block does not appears on the page.
227    *
228    * @param \Drupal\block\Entity\Block $block
229    *   The block entity to find on the page.
230    */
231   protected function assertNoBlockAppears(Block $block) {
232     $result = $this->findBlockInstance($block);
233     $this->assertFalse(!empty($result), format_string('Ensure the block @id does not appear on the page', ['@id' => $block->id()]));
234   }
235
236   /**
237    * Find a block instance on the page.
238    *
239    * @param \Drupal\block\Entity\Block $block
240    *   The block entity to find on the page.
241    *
242    * @return array
243    *   The result from the xpath query.
244    */
245   protected function findBlockInstance(Block $block) {
246     return $this->xpath('//div[@id = :id]', [':id' => 'block-' . $block->id()]);
247   }
248
249   /**
250    * Log in a user with the internal browser.
251    *
252    * If a user is already logged in, then the current user is logged out before
253    * logging in the specified user.
254    *
255    * Please note that neither the current user nor the passed-in user object is
256    * populated with data of the logged in user. If you need full access to the
257    * user object after logging in, it must be updated manually. If you also need
258    * access to the plain-text password of the user (set by drupalCreateUser()),
259    * e.g. to log in the same user again, then it must be re-assigned manually.
260    * For example:
261    * @code
262    *   // Create a user.
263    *   $account = $this->drupalCreateUser(array());
264    *   $this->drupalLogin($account);
265    *   // Load real user object.
266    *   $pass_raw = $account->pass_raw;
267    *   $account = User::load($account->id());
268    *   $account->pass_raw = $pass_raw;
269    * @endcode
270    *
271    * @param \Drupal\Core\Session\AccountInterface $account
272    *   User object representing the user to log in.
273    *
274    * @see drupalCreateUser()
275    */
276   protected function drupalLogin(AccountInterface $account) {
277     if ($this->loggedInUser) {
278       $this->drupalLogout();
279     }
280
281     $edit = [
282       'name' => $account->getUsername(),
283       'pass' => $account->pass_raw
284     ];
285     $this->drupalPostForm('user/login', $edit, t('Log in'));
286
287     // @see WebTestBase::drupalUserIsLoggedIn()
288     if (isset($this->sessionId)) {
289       $account->session_id = $this->sessionId;
290     }
291     $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', ['%name' => $account->getUsername()]), 'User login');
292     if ($pass) {
293       $this->loggedInUser = $account;
294       $this->container->get('current_user')->setAccount($account);
295     }
296   }
297
298   /**
299    * Returns whether a given user account is logged in.
300    *
301    * @param \Drupal\user\UserInterface $account
302    *   The user account object to check.
303    */
304   protected function drupalUserIsLoggedIn($account) {
305     $logged_in = FALSE;
306
307     if (isset($account->session_id)) {
308       $session_handler = $this->container->get('session_handler.storage');
309       $logged_in = (bool) $session_handler->read($account->session_id);
310     }
311
312     return $logged_in;
313   }
314
315   /**
316    * Logs a user out of the internal browser and confirms.
317    *
318    * Confirms logout by checking the login page.
319    */
320   protected function drupalLogout() {
321     // Make a request to the logout page, and redirect to the user page, the
322     // idea being if you were properly logged out you should be seeing a login
323     // screen.
324     $this->drupalGet('user/logout', ['query' => ['destination' => 'user/login']]);
325     $this->assertResponse(200, 'User was logged out.');
326     $pass = $this->assertField('name', 'Username field found.', 'Logout');
327     $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
328
329     if ($pass) {
330       // @see WebTestBase::drupalUserIsLoggedIn()
331       unset($this->loggedInUser->session_id);
332       $this->loggedInUser = FALSE;
333       $this->container->get('current_user')->setAccount(new AnonymousUserSession());
334     }
335   }
336
337   /**
338    * Sets up a Drupal site for running functional and integration tests.
339    *
340    * Installs Drupal with the installation profile specified in
341    * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.
342    *
343    * Afterwards, installs any additional modules specified in the static
344    * \Drupal\simpletest\WebTestBase::$modules property of each class in the
345    * class hierarchy.
346    *
347    * After installation all caches are flushed and several configuration values
348    * are reset to the values of the parent site executing the test, since the
349    * default values may be incompatible with the environment in which tests are
350    * being executed.
351    */
352   protected function setUp() {
353     // Set an explicit time zone to not rely on the system one, which may vary
354     // from setup to setup. The Australia/Sydney time zone is chosen so all
355     // tests are run using an edge case scenario (UTC+10 and DST). This choice
356     // is made to prevent time zone related regressions and reduce the
357     // fragility of the testing system in general. This is also set in config in
358     // \Drupal\simpletest\WebTestBase::initConfig().
359     date_default_timezone_set('Australia/Sydney');
360
361     // Preserve original batch for later restoration.
362     $this->setBatch();
363
364     // Initialize user 1 and session name.
365     $this->initUserSession();
366
367     // Prepare the child site settings.
368     $this->prepareSettings();
369
370     // Execute the non-interactive installer.
371     $this->doInstall();
372
373     // Import new settings.php written by the installer.
374     $this->initSettings();
375
376     // Initialize the request and container post-install.
377     $container = $this->initKernel(\Drupal::request());
378
379     // Initialize and override certain configurations.
380     $this->initConfig($container);
381
382     // Collect modules to install.
383     $this->installModulesFromClassProperty($container);
384
385     // Restore the original batch.
386     $this->restoreBatch();
387
388     // Reset/rebuild everything.
389     $this->rebuildAll();
390   }
391
392   /**
393    * Returns the parameters that will be used when Simpletest installs Drupal.
394    *
395    * @see install_drupal()
396    * @see install_state_defaults()
397    *
398    * @return array
399    *   Array of parameters for use in install_drupal().
400    */
401   protected function installParameters() {
402     $connection_info = Database::getConnectionInfo();
403     $driver = $connection_info['default']['driver'];
404     $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
405     unset($connection_info['default']['driver']);
406     unset($connection_info['default']['namespace']);
407     unset($connection_info['default']['pdo']);
408     unset($connection_info['default']['init_commands']);
409     // Remove database connection info that is not used by SQLite.
410     if ($driver == 'sqlite') {
411       unset($connection_info['default']['username']);
412       unset($connection_info['default']['password']);
413       unset($connection_info['default']['host']);
414       unset($connection_info['default']['port']);
415     }
416     $parameters = [
417       'interactive' => FALSE,
418       'parameters' => [
419         'profile' => $this->profile,
420         'langcode' => 'en',
421       ],
422       'forms' => [
423         'install_settings_form' => [
424           'driver' => $driver,
425           $driver => $connection_info['default'],
426         ],
427         'install_configure_form' => [
428           'site_name' => 'Drupal',
429           'site_mail' => 'simpletest@example.com',
430           'account' => [
431             'name' => $this->rootUser->name,
432             'mail' => $this->rootUser->getEmail(),
433             'pass' => [
434               'pass1' => $this->rootUser->pass_raw,
435               'pass2' => $this->rootUser->pass_raw,
436             ],
437           ],
438           // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires
439           // NULL instead of FALSE values for programmatic form submissions to
440           // disable a checkbox.
441           'enable_update_status_module' => NULL,
442           'enable_update_status_emails' => NULL,
443         ],
444       ],
445     ];
446
447     // If we only have one db driver available, we cannot set the driver.
448     include_once DRUPAL_ROOT . '/core/includes/install.inc';
449     if (count($this->getDatabaseTypes()) == 1) {
450       unset($parameters['forms']['install_settings_form']['driver']);
451     }
452     return $parameters;
453   }
454
455   /**
456    * Preserve the original batch, and instantiate the test batch.
457    */
458   protected function setBatch() {
459     // When running tests through the Simpletest UI (vs. on the command line),
460     // Simpletest's batch conflicts with the installer's batch. Batch API does
461     // not support the concept of nested batches (in which the nested is not
462     // progressive), so we need to temporarily pretend there was no batch.
463     // Backup the currently running Simpletest batch.
464     $this->originalBatch = batch_get();
465
466     // Reset the static batch to remove Simpletest's batch operations.
467     $batch = &batch_get();
468     $batch = [];
469   }
470
471   /**
472    * Restore the original batch.
473    *
474    * @see ::setBatch
475    */
476   protected function restoreBatch() {
477     // Restore the original Simpletest batch.
478     $batch = &batch_get();
479     $batch = $this->originalBatch;
480   }
481
482   /**
483    * Returns all supported database driver installer objects.
484    *
485    * This wraps drupal_get_database_types() for use without a current container.
486    *
487    * @return \Drupal\Core\Database\Install\Tasks[]
488    *   An array of available database driver installer objects.
489    */
490   protected function getDatabaseTypes() {
491     \Drupal::setContainer($this->originalContainer);
492     $database_types = drupal_get_database_types();
493     \Drupal::unsetContainer();
494     return $database_types;
495   }
496
497   /**
498    * Queues custom translations to be written to settings.php.
499    *
500    * Use WebTestBase::writeCustomTranslations() to apply and write the queued
501    * translations.
502    *
503    * @param string $langcode
504    *   The langcode to add translations for.
505    * @param array $values
506    *   Array of values containing the untranslated string and its translation.
507    *   For example:
508    *   @code
509    *   array(
510    *     '' => array('Sunday' => 'domingo'),
511    *     'Long month name' => array('March' => 'marzo'),
512    *   );
513    *   @endcode
514    *   Pass an empty array to remove all existing custom translations for the
515    *   given $langcode.
516    */
517   protected function addCustomTranslations($langcode, array $values) {
518     // If $values is empty, then the test expects all custom translations to be
519     // cleared.
520     if (empty($values)) {
521       $this->customTranslations[$langcode] = [];
522     }
523     // Otherwise, $values are expected to be merged into previously passed
524     // values, while retaining keys that are not explicitly set.
525     else {
526       foreach ($values as $context => $translations) {
527         foreach ($translations as $original => $translation) {
528           $this->customTranslations[$langcode][$context][$original] = $translation;
529         }
530       }
531     }
532   }
533
534   /**
535    * Writes custom translations to the test site's settings.php.
536    *
537    * Use TestBase::addCustomTranslations() to queue custom translations before
538    * calling this method.
539    */
540   protected function writeCustomTranslations() {
541     $settings = [];
542     foreach ($this->customTranslations as $langcode => $values) {
543       $settings_key = 'locale_custom_strings_' . $langcode;
544
545       // Update in-memory settings directly.
546       $this->settingsSet($settings_key, $values);
547
548       $settings['settings'][$settings_key] = (object) [
549         'value' => $values,
550         'required' => TRUE,
551       ];
552     }
553     // Only rewrite settings if there are any translation changes to write.
554     if (!empty($settings)) {
555       $this->writeSettings($settings);
556     }
557   }
558
559   /**
560    * Cleans up after testing.
561    *
562    * Deletes created files and temporary files directory, deletes the tables
563    * created by setUp(), and resets the database prefix.
564    */
565   protected function tearDown() {
566     // Destroy the testing kernel.
567     if (isset($this->kernel)) {
568       $this->kernel->shutdown();
569     }
570     parent::tearDown();
571
572     // Ensure that the maximum meta refresh count is reset.
573     $this->maximumMetaRefreshCount = NULL;
574
575     // Ensure that internal logged in variable and cURL options are reset.
576     $this->loggedInUser = FALSE;
577     $this->additionalCurlOptions = [];
578
579     // Close the CURL handler and reset the cookies array used for upgrade
580     // testing so test classes containing multiple tests are not polluted.
581     $this->curlClose();
582     $this->curlCookies = [];
583     $this->cookies = [];
584   }
585
586   /**
587    * Initializes the cURL connection.
588    *
589    * If the simpletest_httpauth_credentials variable is set, this function will
590    * add HTTP authentication headers. This is necessary for testing sites that
591    * are protected by login credentials from public access.
592    * See the description of $curl_options for other options.
593    */
594   protected function curlInitialize() {
595     global $base_url;
596
597     if (!isset($this->curlHandle)) {
598       $this->curlHandle = curl_init();
599
600       // Some versions/configurations of cURL break on a NULL cookie jar, so
601       // supply a real file.
602       if (empty($this->cookieFile)) {
603         $this->cookieFile = $this->publicFilesDirectory . '/cookie.jar';
604       }
605
606       $curl_options = [
607         CURLOPT_COOKIEJAR => $this->cookieFile,
608         CURLOPT_URL => $base_url,
609         CURLOPT_FOLLOWLOCATION => FALSE,
610         CURLOPT_RETURNTRANSFER => TRUE,
611         // Required to make the tests run on HTTPS.
612         CURLOPT_SSL_VERIFYPEER => FALSE,
613         // Required to make the tests run on HTTPS.
614         CURLOPT_SSL_VERIFYHOST => FALSE,
615         CURLOPT_HEADERFUNCTION => [&$this, 'curlHeaderCallback'],
616         CURLOPT_USERAGENT => $this->databasePrefix,
617         // Disable support for the @ prefix for uploading files.
618         CURLOPT_SAFE_UPLOAD => TRUE,
619       ];
620       if (isset($this->httpAuthCredentials)) {
621         $curl_options[CURLOPT_HTTPAUTH] = $this->httpAuthMethod;
622         $curl_options[CURLOPT_USERPWD] = $this->httpAuthCredentials;
623       }
624       // curl_setopt_array() returns FALSE if any of the specified options
625       // cannot be set, and stops processing any further options.
626       $result = curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
627       if (!$result) {
628         throw new \UnexpectedValueException('One or more cURL options could not be set.');
629       }
630     }
631     // We set the user agent header on each request so as to use the current
632     // time and a new uniqid.
633     curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
634   }
635
636   /**
637    * Initializes and executes a cURL request.
638    *
639    * @param $curl_options
640    *   An associative array of cURL options to set, where the keys are constants
641    *   defined by the cURL library. For a list of valid options, see
642    *   http://php.net/manual/function.curl-setopt.php
643    * @param $redirect
644    *   FALSE if this is an initial request, TRUE if this request is the result
645    *   of a redirect.
646    *
647    * @return
648    *   The content returned from the call to curl_exec().
649    *
650    * @see curlInitialize()
651    */
652   protected function curlExec($curl_options, $redirect = FALSE) {
653     $this->curlInitialize();
654
655     if (!empty($curl_options[CURLOPT_URL])) {
656       // cURL incorrectly handles URLs with a fragment by including the
657       // fragment in the request to the server, causing some web servers
658       // to reject the request citing "400 - Bad Request". To prevent
659       // this, we strip the fragment from the request.
660       // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
661       if (strpos($curl_options[CURLOPT_URL], '#')) {
662         $original_url = $curl_options[CURLOPT_URL];
663         $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#');
664       }
665     }
666
667     $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
668
669     if (!empty($curl_options[CURLOPT_POST])) {
670       // This is a fix for the Curl library to prevent Expect: 100-continue
671       // headers in POST requests, that may cause unexpected HTTP response
672       // codes from some webservers (like lighttpd that returns a 417 error
673       // code). It is done by setting an empty "Expect" header field that is
674       // not overwritten by Curl.
675       $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:';
676     }
677
678     $cookies = [];
679     if (!empty($this->curlCookies)) {
680       $cookies = $this->curlCookies;
681     }
682
683     foreach ($this->extractCookiesFromRequest(\Drupal::request()) as $cookie_name => $values) {
684       foreach ($values as $value) {
685         $cookies[] = $cookie_name . '=' . $value;
686       }
687     }
688
689     // Merge additional cookies in.
690     if (!empty($cookies)) {
691       $curl_options += [
692         CURLOPT_COOKIE => '',
693       ];
694       // Ensure any existing cookie data string ends with the correct separator.
695       if (!empty($curl_options[CURLOPT_COOKIE])) {
696         $curl_options[CURLOPT_COOKIE] = rtrim($curl_options[CURLOPT_COOKIE], '; ') . '; ';
697       }
698       $curl_options[CURLOPT_COOKIE] .= implode('; ', $cookies) . ';';
699     }
700
701     curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
702
703     if (!$redirect) {
704       // Reset headers, the session ID and the redirect counter.
705       $this->sessionId = NULL;
706       $this->headers = [];
707       $this->redirectCount = 0;
708     }
709
710     $content = curl_exec($this->curlHandle);
711     $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
712
713     // cURL incorrectly handles URLs with fragments, so instead of
714     // letting cURL handle redirects we take of them ourselves to
715     // to prevent fragments being sent to the web server as part
716     // of the request.
717     // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
718     if (in_array($status, [300, 301, 302, 303, 305, 307]) && $this->redirectCount < $this->maximumRedirects) {
719       if ($this->drupalGetHeader('location')) {
720         $this->redirectCount++;
721         $curl_options = [];
722         $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location');
723         $curl_options[CURLOPT_HTTPGET] = TRUE;
724         return $this->curlExec($curl_options, TRUE);
725       }
726     }
727
728     $this->setRawContent($content);
729     $this->url = isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL);
730
731     $message_vars = [
732       '@method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'),
733       '@url' => isset($original_url) ? $original_url : $url,
734       '@status' => $status,
735       '@length' => format_size(strlen($this->getRawContent()))
736     ];
737     $message = SafeMarkup::format('@method @url returned @status (@length).', $message_vars);
738     $this->assertTrue($this->getRawContent() !== FALSE, $message, 'Browser');
739     return $this->getRawContent();
740   }
741
742   /**
743    * Reads headers and registers errors received from the tested site.
744    *
745    * @param $curlHandler
746    *   The cURL handler.
747    * @param $header
748    *   An header.
749    *
750    * @see _drupal_log_error()
751    */
752   protected function curlHeaderCallback($curlHandler, $header) {
753     // Header fields can be extended over multiple lines by preceding each
754     // extra line with at least one SP or HT. They should be joined on receive.
755     // Details are in RFC2616 section 4.
756     if ($header[0] == ' ' || $header[0] == "\t") {
757       // Normalize whitespace between chucks.
758       $this->headers[] = array_pop($this->headers) . ' ' . trim($header);
759     }
760     else {
761       $this->headers[] = $header;
762     }
763
764     // Errors are being sent via X-Drupal-Assertion-* headers,
765     // generated by _drupal_log_error() in the exact form required
766     // by \Drupal\simpletest\WebTestBase::error().
767     if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
768       // Call \Drupal\simpletest\WebTestBase::error() with the parameters from
769       // the header.
770       call_user_func_array([&$this, 'error'], unserialize(urldecode($matches[1])));
771     }
772
773     // Save cookies.
774     if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) {
775       $name = $matches[1];
776       $parts = array_map('trim', explode(';', $matches[2]));
777       $value = array_shift($parts);
778       $this->cookies[$name] = ['value' => $value, 'secure' => in_array('secure', $parts)];
779       if ($name === $this->getSessionName()) {
780         if ($value != 'deleted') {
781           $this->sessionId = $value;
782         }
783         else {
784           $this->sessionId = NULL;
785         }
786       }
787     }
788
789     // This is required by cURL.
790     return strlen($header);
791   }
792
793   /**
794    * Close the cURL handler and unset the handler.
795    */
796   protected function curlClose() {
797     if (isset($this->curlHandle)) {
798       curl_close($this->curlHandle);
799       unset($this->curlHandle);
800     }
801   }
802
803   /**
804    * Returns whether the test is being executed from within a test site.
805    *
806    * Mainly used by recursive tests (i.e. to test the testing framework).
807    *
808    * @return bool
809    *   TRUE if this test was instantiated in a request within the test site,
810    *   FALSE otherwise.
811    *
812    * @see \Drupal\Core\DrupalKernel::bootConfiguration()
813    */
814   protected function isInChildSite() {
815     return DRUPAL_TEST_IN_CHILD_SITE;
816   }
817
818   /**
819    * Retrieves a Drupal path or an absolute path.
820    *
821    * @param \Drupal\Core\Url|string $path
822    *   Drupal path or URL to load into internal browser
823    * @param $options
824    *   Options to be forwarded to the url generator.
825    * @param $headers
826    *   An array containing additional HTTP request headers, each formatted as
827    *   "name: value".
828    *
829    * @return string
830    *   The retrieved HTML string, also available as $this->getRawContent()
831    */
832   protected function drupalGet($path, array $options = [], array $headers = []) {
833     // We re-using a CURL connection here. If that connection still has certain
834     // options set, it might change the GET into a POST. Make sure we clear out
835     // previous options.
836     $out = $this->curlExec([CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->buildUrl($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers]);
837     // Ensure that any changes to variables in the other thread are picked up.
838     $this->refreshVariables();
839
840     // Replace original page output with new output from redirected page(s).
841     if ($new = $this->checkForMetaRefresh()) {
842       $out = $new;
843       // We are finished with all meta refresh redirects, so reset the counter.
844       $this->metaRefreshCount = 0;
845     }
846
847     if ($path instanceof Url) {
848       $path = $path->setAbsolute()->toString(TRUE)->getGeneratedUrl();
849     }
850
851     $verbose = 'GET request to: ' . $path .
852                '<hr />Ending URL: ' . $this->getUrl();
853     if ($this->dumpHeaders) {
854       $verbose .= '<hr />Headers: <pre>' . Html::escape(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>';
855     }
856     $verbose .= '<hr />' . $out;
857
858     $this->verbose($verbose);
859     return $out;
860   }
861
862   /**
863    * Retrieves a Drupal path or an absolute path and JSON decodes the result.
864    *
865    * @param \Drupal\Core\Url|string $path
866    *   Drupal path or URL to request AJAX from.
867    * @param array $options
868    *   Array of URL options.
869    * @param array $headers
870    *   Array of headers. Eg array('Accept: application/vnd.drupal-ajax').
871    *
872    * @return array
873    *   Decoded json.
874    */
875   protected function drupalGetJSON($path, array $options = [], array $headers = []) {
876     return Json::decode($this->drupalGetWithFormat($path, 'json', $options, $headers));
877   }
878
879   /**
880    * Retrieves a Drupal path or an absolute path for a given format.
881    *
882    * @param \Drupal\Core\Url|string $path
883    *   Drupal path or URL to request given format from.
884    * @param string $format
885    *   The wanted request format.
886    * @param array $options
887    *   Array of URL options.
888    * @param array $headers
889    *   Array of headers.
890    *
891    * @return mixed
892    *   The result of the request.
893    */
894   protected function drupalGetWithFormat($path, $format, array $options = [], array $headers = []) {
895     $options += ['query' => ['_format' => $format]];
896     return $this->drupalGet($path, $options, $headers);
897   }
898
899   /**
900    * Requests a path or URL in drupal_ajax format and JSON-decodes the response.
901    *
902    * @param \Drupal\Core\Url|string $path
903    *   Drupal path or URL to request from.
904    * @param array $options
905    *   Array of URL options.
906    * @param array $headers
907    *   Array of headers.
908    *
909    * @return array
910    *   Decoded JSON.
911    */
912   protected function drupalGetAjax($path, array $options = [], array $headers = []) {
913     if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT])) {
914       $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax';
915     }
916     return Json::decode($this->drupalGetXHR($path, $options, $headers));
917   }
918
919   /**
920    * Requests a Drupal path or an absolute path as if it is a XMLHttpRequest.
921    *
922    * @param \Drupal\Core\Url|string $path
923    *   Drupal path or URL to request from.
924    * @param array $options
925    *   Array of URL options.
926    * @param array $headers
927    *   Array of headers.
928    *
929    * @return string
930    *   The retrieved content.
931    */
932   protected function drupalGetXHR($path, array $options = [], array $headers = []) {
933     $headers[] = 'X-Requested-With: XMLHttpRequest';
934     return $this->drupalGet($path, $options, $headers);
935   }
936
937   /**
938    * Executes a form submission.
939    *
940    * It will be done as usual POST request with SimpleBrowser.
941    *
942    * @param \Drupal\Core\Url|string $path
943    *   Location of the post form. Either a Drupal path or an absolute path or
944    *   NULL to post to the current page. For multi-stage forms you can set the
945    *   path to NULL and have it post to the last received page. Example:
946    *
947    *   @code
948    *   // First step in form.
949    *   $edit = array(...);
950    *   $this->drupalPostForm('some_url', $edit, t('Save'));
951    *
952    *   // Second step in form.
953    *   $edit = array(...);
954    *   $this->drupalPostForm(NULL, $edit, t('Save'));
955    *   @endcode
956    * @param $edit
957    *   Field data in an associative array. Changes the current input fields
958    *   (where possible) to the values indicated.
959    *
960    *   When working with form tests, the keys for an $edit element should match
961    *   the 'name' parameter of the HTML of the form. For example, the 'body'
962    *   field for a node has the following HTML:
963    *   @code
964    *   <textarea id="edit-body-und-0-value" class="text-full form-textarea
965    *    resize-vertical" placeholder="" cols="60" rows="9"
966    *    name="body[0][value]"></textarea>
967    *   @endcode
968    *   When testing this field using an $edit parameter, the code becomes:
969    *   @code
970    *   $edit["body[0][value]"] = 'My test value';
971    *   @endcode
972    *
973    *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
974    *   be unchecked. Multiple select fields can be tested using 'name[]' and
975    *   setting each of the desired values in an array:
976    *   @code
977    *   $edit = array();
978    *   $edit['name[]'] = array('value1', 'value2');
979    *   @endcode
980    * @param $submit
981    *   Value of the submit button whose click is to be emulated. For example,
982    *   t('Save'). The processing of the request depends on this value. For
983    *   example, a form may have one button with the value t('Save') and another
984    *   button with the value t('Delete'), and execute different code depending
985    *   on which one is clicked.
986    *
987    *   This function can also be called to emulate an Ajax submission. In this
988    *   case, this value needs to be an array with the following keys:
989    *   - path: A path to submit the form values to for Ajax-specific processing.
990    *   - triggering_element: If the value for the 'path' key is a generic Ajax
991    *     processing path, this needs to be set to the name of the element. If
992    *     the name doesn't identify the element uniquely, then this should
993    *     instead be an array with a single key/value pair, corresponding to the
994    *     element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
995    *     uses this to find the #ajax information for the element, including
996    *     which specific callback to use for processing the request.
997    *
998    *   This can also be set to NULL in order to emulate an Internet Explorer
999    *   submission of a form with a single text field, and pressing ENTER in that
1000    *   textfield: under these conditions, no button information is added to the
1001    *   POST data.
1002    * @param $options
1003    *   Options to be forwarded to the url generator.
1004    * @param $headers
1005    *   An array containing additional HTTP request headers, each formatted as
1006    *   "name: value".
1007    * @param $form_html_id
1008    *   (optional) HTML ID of the form to be submitted. On some pages
1009    *   there are many identical forms, so just using the value of the submit
1010    *   button is not enough. For example: 'trigger-node-presave-assign-form'.
1011    *   Note that this is not the Drupal $form_id, but rather the HTML ID of the
1012    *   form, which is typically the same thing but with hyphens replacing the
1013    *   underscores.
1014    * @param $extra_post
1015    *   (optional) A string of additional data to append to the POST submission.
1016    *   This can be used to add POST data for which there are no HTML fields, as
1017    *   is done by drupalPostAjaxForm(). This string is literally appended to the
1018    *   POST data, so it must already be urlencoded and contain a leading "&"
1019    *   (e.g., "&extra_var1=hello+world&extra_var2=you%26me").
1020    */
1021   protected function drupalPostForm($path, $edit, $submit, array $options = [], array $headers = [], $form_html_id = NULL, $extra_post = NULL) {
1022     if (is_object($submit)) {
1023       // Cast MarkupInterface objects to string.
1024       $submit = (string) $submit;
1025     }
1026     if (is_array($edit)) {
1027       $edit = $this->castSafeStrings($edit);
1028     }
1029
1030     $submit_matches = FALSE;
1031     $ajax = is_array($submit);
1032     if (isset($path)) {
1033       $this->drupalGet($path, $options);
1034     }
1035
1036     if ($this->parse()) {
1037       $edit_save = $edit;
1038       // Let's iterate over all the forms.
1039       $xpath = "//form";
1040       if (!empty($form_html_id)) {
1041         $xpath .= "[@id='" . $form_html_id . "']";
1042       }
1043       $forms = $this->xpath($xpath);
1044       foreach ($forms as $form) {
1045         // We try to set the fields of this form as specified in $edit.
1046         $edit = $edit_save;
1047         $post = [];
1048         $upload = [];
1049         $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form);
1050         $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl();
1051         if ($ajax) {
1052           if (empty($submit['path'])) {
1053             throw new \Exception('No #ajax path specified.');
1054           }
1055           $action = $this->getAbsoluteUrl($submit['path']);
1056           // Ajax callbacks verify the triggering element if necessary, so while
1057           // we may eventually want extra code that verifies it in the
1058           // handleForm() function, it's not currently a requirement.
1059           $submit_matches = TRUE;
1060         }
1061         // We post only if we managed to handle every field in edit and the
1062         // submit button matches.
1063         if (!$edit && ($submit_matches || !isset($submit))) {
1064           $post_array = $post;
1065           if ($upload) {
1066             foreach ($upload as $key => $file) {
1067               if (is_array($file) && count($file)) {
1068                 // There seems to be no way via php's API to cURL to upload
1069                 // several files with the same post field name. However, Drupal
1070                 // still sees array-index syntax in a similar way.
1071                 for ($i = 0; $i < count($file); $i++) {
1072                   $postfield = str_replace('[]', '', $key) . '[' . $i . ']';
1073                   $file_path = $this->container->get('file_system')->realpath($file[$i]);
1074                   $post[$postfield] = curl_file_create($file_path);
1075                 }
1076               }
1077               else {
1078                 $file = $this->container->get('file_system')->realpath($file);
1079                 if ($file && is_file($file)) {
1080                   $post[$key] = curl_file_create($file);
1081                 }
1082               }
1083             }
1084           }
1085           else {
1086             $post = $this->serializePostValues($post) . $extra_post;
1087           }
1088           $out = $this->curlExec([CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers]);
1089           // Ensure that any changes to variables in the other thread are picked
1090           // up.
1091           $this->refreshVariables();
1092
1093           // Replace original page output with new output from redirected
1094           // page(s).
1095           if ($new = $this->checkForMetaRefresh()) {
1096             $out = $new;
1097           }
1098
1099           if ($path instanceof Url) {
1100             $path = $path->toString();
1101           }
1102           $verbose = 'POST request to: ' . $path;
1103           $verbose .= '<hr />Ending URL: ' . $this->getUrl();
1104           if ($this->dumpHeaders) {
1105             $verbose .= '<hr />Headers: <pre>' . Html::escape(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>';
1106           }
1107           $verbose .= '<hr />Fields: ' . highlight_string('<?php ' . var_export($post_array, TRUE), TRUE);
1108           $verbose .= '<hr />' . $out;
1109
1110           $this->verbose($verbose);
1111           return $out;
1112         }
1113       }
1114       // We have not found a form which contained all fields of $edit.
1115       foreach ($edit as $name => $value) {
1116         $this->fail(SafeMarkup::format('Failed to set field @name to @value', ['@name' => $name, '@value' => $value]));
1117       }
1118       if (!$ajax && isset($submit)) {
1119         $this->assertTrue($submit_matches, format_string('Found the @submit button', ['@submit' => $submit]));
1120       }
1121       $this->fail(format_string('Found the requested form fields at @path', ['@path' => ($path instanceof Url) ? $path->toString() : $path]));
1122     }
1123   }
1124
1125   /**
1126    * Executes an Ajax form submission.
1127    *
1128    * This executes a POST as ajax.js does. The returned JSON data is used to
1129    * update $this->content via drupalProcessAjaxResponse(). It also returns
1130    * the array of AJAX commands received.
1131    *
1132    * @param \Drupal\Core\Url|string $path
1133    *   Location of the form containing the Ajax enabled element to test. Can be
1134    *   either a Drupal path or an absolute path or NULL to use the current page.
1135    * @param $edit
1136    *   Field data in an associative array. Changes the current input fields
1137    *   (where possible) to the values indicated.
1138    * @param $triggering_element
1139    *   The name of the form element that is responsible for triggering the Ajax
1140    *   functionality to test. May be a string or, if the triggering element is
1141    *   a button, an associative array where the key is the name of the button
1142    *   and the value is the button label. i.e.) array('op' => t('Refresh')).
1143    * @param $ajax_path
1144    *   (optional) Override the path set by the Ajax settings of the triggering
1145    *   element.
1146    * @param $options
1147    *   (optional) Options to be forwarded to the url generator.
1148    * @param $headers
1149    *   (optional) An array containing additional HTTP request headers, each
1150    *   formatted as "name: value". Forwarded to drupalPostForm().
1151    * @param $form_html_id
1152    *   (optional) HTML ID of the form to be submitted, use when there is more
1153    *   than one identical form on the same page and the value of the triggering
1154    *   element is not enough to identify the form. Note this is not the Drupal
1155    *   ID of the form but rather the HTML ID of the form.
1156    * @param $ajax_settings
1157    *   (optional) An array of Ajax settings which if specified will be used in
1158    *   place of the Ajax settings of the triggering element.
1159    *
1160    * @return
1161    *   An array of Ajax commands.
1162    *
1163    * @see drupalPostForm()
1164    * @see drupalProcessAjaxResponse()
1165    * @see ajax.js
1166    */
1167   protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_path = NULL, array $options = [], array $headers = [], $form_html_id = NULL, $ajax_settings = NULL) {
1168
1169     // Get the content of the initial page prior to calling drupalPostForm(),
1170     // since drupalPostForm() replaces $this->content.
1171     if (isset($path)) {
1172       // Avoid sending the wrapper query argument to drupalGet so we can fetch
1173       // the form and populate the internal WebTest values.
1174       $get_options = $options;
1175       unset($get_options['query'][MainContentViewSubscriber::WRAPPER_FORMAT]);
1176       $this->drupalGet($path, $get_options);
1177     }
1178     $content = $this->content;
1179     $drupal_settings = $this->drupalSettings;
1180
1181     // Provide a default value for the wrapper envelope.
1182     $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] =
1183       isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT]) ?
1184         $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] :
1185         'drupal_ajax';
1186
1187     // Get the Ajax settings bound to the triggering element.
1188     if (!isset($ajax_settings)) {
1189       if (is_array($triggering_element)) {
1190         $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]';
1191       }
1192       else {
1193         $xpath = '//*[@name="' . $triggering_element . '"]';
1194       }
1195       if (isset($form_html_id)) {
1196         $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath;
1197       }
1198       $element = $this->xpath($xpath);
1199       $element_id = (string) $element[0]['id'];
1200       $ajax_settings = $drupal_settings['ajax'][$element_id];
1201     }
1202
1203     // Add extra information to the POST data as ajax.js does.
1204     $extra_post = [];
1205     if (isset($ajax_settings['submit'])) {
1206       foreach ($ajax_settings['submit'] as $key => $value) {
1207         $extra_post[$key] = $value;
1208       }
1209     }
1210     $extra_post[AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER] = 1;
1211     $extra_post += $this->getAjaxPageStatePostData();
1212     // Now serialize all the $extra_post values, and prepend it with an '&'.
1213     $extra_post = '&' . $this->serializePostValues($extra_post);
1214
1215     // Unless a particular path is specified, use the one specified by the
1216     // Ajax settings.
1217     if (!isset($ajax_path)) {
1218       if (isset($ajax_settings['url'])) {
1219         // In order to allow to set for example the wrapper envelope query
1220         // parameter we need to get the system path again.
1221         $parsed_url = UrlHelper::parse($ajax_settings['url']);
1222         $options['query'] = $parsed_url['query'] + $options['query'];
1223         $options += ['fragment' => $parsed_url['fragment']];
1224
1225         // We know that $parsed_url['path'] is already with the base path
1226         // attached.
1227         $ajax_path = preg_replace(
1228           '/^' . preg_quote(base_path(), '/') . '/',
1229           '',
1230           $parsed_url['path']
1231         );
1232       }
1233     }
1234
1235     if (empty($ajax_path)) {
1236       throw new \Exception('No #ajax path specified.');
1237     }
1238
1239     $ajax_path = $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options);
1240
1241     // Submit the POST request.
1242     $return = Json::decode($this->drupalPostForm(NULL, $edit, ['path' => $ajax_path, 'triggering_element' => $triggering_element], $options, $headers, $form_html_id, $extra_post));
1243     if ($this->assertAjaxHeader) {
1244       $this->assertIdentical($this->drupalGetHeader('X-Drupal-Ajax-Token'), '1', 'Ajax response header found.');
1245     }
1246
1247     // Change the page content by applying the returned commands.
1248     if (!empty($ajax_settings) && !empty($return)) {
1249       $this->drupalProcessAjaxResponse($content, $return, $ajax_settings, $drupal_settings);
1250     }
1251
1252     $verbose = 'AJAX POST request to: ' . $path;
1253     $verbose .= '<br />AJAX controller path: ' . $ajax_path;
1254     $verbose .= '<hr />Ending URL: ' . $this->getUrl();
1255     $verbose .= '<hr />' . $this->content;
1256
1257     $this->verbose($verbose);
1258
1259     return $return;
1260   }
1261
1262   /**
1263    * Processes an AJAX response into current content.
1264    *
1265    * This processes the AJAX response as ajax.js does. It uses the response's
1266    * JSON data, an array of commands, to update $this->content using equivalent
1267    * DOM manipulation as is used by ajax.js.
1268    * It does not apply custom AJAX commands though, because emulation is only
1269    * implemented for the AJAX commands that ship with Drupal core.
1270    *
1271    * @param string $content
1272    *   The current HTML content.
1273    * @param array $ajax_response
1274    *   An array of AJAX commands.
1275    * @param array $ajax_settings
1276    *   An array of AJAX settings which will be used to process the response.
1277    * @param array $drupal_settings
1278    *   An array of settings to update the value of drupalSettings for the
1279    *   currently-loaded page.
1280    *
1281    * @see drupalPostAjaxForm()
1282    * @see ajax.js
1283    */
1284   protected function drupalProcessAjaxResponse($content, array $ajax_response, array $ajax_settings, array $drupal_settings) {
1285
1286     // ajax.js applies some defaults to the settings object, so do the same
1287     // for what's used by this function.
1288     $ajax_settings += [
1289       'method' => 'replaceWith',
1290     ];
1291     // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
1292     // them.
1293     $dom = new \DOMDocument();
1294     @$dom->loadHTML($content);
1295     // XPath allows for finding wrapper nodes better than DOM does.
1296     $xpath = new \DOMXPath($dom);
1297     foreach ($ajax_response as $command) {
1298       // Error messages might be not commands.
1299       if (!is_array($command)) {
1300         continue;
1301       }
1302       switch ($command['command']) {
1303         case 'settings':
1304           $drupal_settings = NestedArray::mergeDeepArray([$drupal_settings, $command['settings']], TRUE);
1305           break;
1306
1307         case 'insert':
1308           $wrapperNode = NULL;
1309           // When a command doesn't specify a selector, use the
1310           // #ajax['wrapper'] which is always an HTML ID.
1311           if (!isset($command['selector'])) {
1312             $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0);
1313           }
1314           // @todo Ajax commands can target any jQuery selector, but these are
1315           //   hard to fully emulate with XPath. For now, just handle 'head'
1316           //   and 'body', since these are used by the Ajax renderer.
1317           elseif (in_array($command['selector'], ['head', 'body'])) {
1318             $wrapperNode = $xpath->query('//' . $command['selector'])->item(0);
1319           }
1320           if ($wrapperNode) {
1321             // ajax.js adds an enclosing DIV to work around a Safari bug.
1322             $newDom = new \DOMDocument();
1323             // DOM can load HTML soup. But, HTML soup can throw warnings,
1324             // suppress them.
1325             @$newDom->loadHTML('<div>' . $command['data'] . '</div>');
1326             // Suppress warnings thrown when duplicate HTML IDs are encountered.
1327             // This probably means we are replacing an element with the same ID.
1328             $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE);
1329             $method = isset($command['method']) ? $command['method'] : $ajax_settings['method'];
1330             // The "method" is a jQuery DOM manipulation function. Emulate
1331             // each one using PHP's DOMNode API.
1332             switch ($method) {
1333               case 'replaceWith':
1334                 $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode);
1335                 break;
1336               case 'append':
1337                 $wrapperNode->appendChild($newNode);
1338                 break;
1339               case 'prepend':
1340                 // If no firstChild, insertBefore() falls back to
1341                 // appendChild().
1342                 $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild);
1343                 break;
1344               case 'before':
1345                 $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode);
1346                 break;
1347               case 'after':
1348                 // If no nextSibling, insertBefore() falls back to
1349                 // appendChild().
1350                 $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling);
1351                 break;
1352               case 'html':
1353                 foreach ($wrapperNode->childNodes as $childNode) {
1354                   $wrapperNode->removeChild($childNode);
1355                 }
1356                 $wrapperNode->appendChild($newNode);
1357                 break;
1358             }
1359           }
1360           break;
1361
1362         // @todo Add suitable implementations for these commands in order to
1363         //   have full test coverage of what ajax.js can do.
1364         case 'remove':
1365           break;
1366         case 'changed':
1367           break;
1368         case 'css':
1369           break;
1370         case 'data':
1371           break;
1372         case 'restripe':
1373           break;
1374         case 'add_css':
1375           break;
1376         case 'update_build_id':
1377           $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0);
1378           if ($buildId) {
1379             $buildId->setAttribute('value', $command['new']);
1380           }
1381           break;
1382       }
1383     }
1384     $content = $dom->saveHTML();
1385     $this->setRawContent($content);
1386     $this->setDrupalSettings($drupal_settings);
1387   }
1388
1389   /**
1390    * Perform a POST HTTP request.
1391    *
1392    * @param string|\Drupal\Core\Url $path
1393    *   Drupal path or absolute path where the request should be POSTed.
1394    * @param string $accept
1395    *   The value for the "Accept" header. Usually either 'application/json' or
1396    *   'application/vnd.drupal-ajax'.
1397    * @param array $post
1398    *   The POST data. When making a 'application/vnd.drupal-ajax' request, the
1399    *   Ajax page state data should be included. Use getAjaxPageStatePostData()
1400    *   for that.
1401    * @param array $options
1402    *   (optional) Options to be forwarded to the url generator. The 'absolute'
1403    *   option will automatically be enabled.
1404    *
1405    * @return
1406    *   The content returned from the call to curl_exec().
1407    *
1408    * @see WebTestBase::getAjaxPageStatePostData()
1409    * @see WebTestBase::curlExec()
1410    */
1411   protected function drupalPost($path, $accept, array $post, $options = []) {
1412     return $this->curlExec([
1413       CURLOPT_URL => $this->buildUrl($path, $options),
1414       CURLOPT_POST => TRUE,
1415       CURLOPT_POSTFIELDS => $this->serializePostValues($post),
1416       CURLOPT_HTTPHEADER => [
1417         'Accept: ' . $accept,
1418         'Content-Type: application/x-www-form-urlencoded',
1419       ],
1420     ]);
1421   }
1422
1423   /**
1424    * Performs a POST HTTP request with a specific format.
1425    *
1426    * @param string|\Drupal\Core\Url $path
1427    *   Drupal path or absolute path where the request should be POSTed.
1428    * @param string $format
1429    *   The request format.
1430    * @param array $post
1431    *   The POST data. When making a 'application/vnd.drupal-ajax' request, the
1432    *   Ajax page state data should be included. Use getAjaxPageStatePostData()
1433    *   for that.
1434    * @param array $options
1435    *   (optional) Options to be forwarded to the url generator. The 'absolute'
1436    *   option will automatically be enabled.
1437    *
1438    * @return string
1439    *   The content returned from the call to curl_exec().
1440    *
1441    * @see WebTestBase::drupalPost
1442    * @see WebTestBase::getAjaxPageStatePostData()
1443    * @see WebTestBase::curlExec()
1444    */
1445   protected function drupalPostWithFormat($path, $format, array $post, $options = []) {
1446     $options['query']['_format'] = $format;
1447     return $this->drupalPost($path, '', $post, $options);
1448   }
1449
1450   /**
1451    * Get the Ajax page state from drupalSettings and prepare it for POSTing.
1452    *
1453    * @return array
1454    *   The Ajax page state POST data.
1455    */
1456   protected function getAjaxPageStatePostData() {
1457     $post = [];
1458     $drupal_settings = $this->drupalSettings;
1459     if (isset($drupal_settings['ajaxPageState']['theme'])) {
1460       $post['ajax_page_state[theme]'] = $drupal_settings['ajaxPageState']['theme'];
1461     }
1462     if (isset($drupal_settings['ajaxPageState']['theme_token'])) {
1463       $post['ajax_page_state[theme_token]'] = $drupal_settings['ajaxPageState']['theme_token'];
1464     }
1465     if (isset($drupal_settings['ajaxPageState']['libraries'])) {
1466       $post['ajax_page_state[libraries]'] = $drupal_settings['ajaxPageState']['libraries'];
1467     }
1468     return $post;
1469   }
1470
1471   /**
1472    * Serialize POST HTTP request values.
1473    *
1474    * Encode according to application/x-www-form-urlencoded. Both names and
1475    * values needs to be urlencoded, according to
1476    * http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
1477    *
1478    * @param array $post
1479    *   The array of values to be POSTed.
1480    *
1481    * @return string
1482    *   The serialized result.
1483    */
1484   protected function serializePostValues($post = []) {
1485     foreach ($post as $key => $value) {
1486       $post[$key] = urlencode($key) . '=' . urlencode($value);
1487     }
1488     return implode('&', $post);
1489   }
1490
1491   /**
1492    * Transforms a nested array into a flat array suitable for WebTestBase::drupalPostForm().
1493    *
1494    * @param array $values
1495    *   A multi-dimensional form values array to convert.
1496    *
1497    * @return array
1498    *   The flattened $edit array suitable for WebTestBase::drupalPostForm().
1499    */
1500   protected function translatePostValues(array $values) {
1501     $edit = [];
1502     // The easiest and most straightforward way to translate values suitable for
1503     // WebTestBase::drupalPostForm() is to actually build the POST data string
1504     // and convert the resulting key/value pairs back into a flat array.
1505     $query = http_build_query($values);
1506     foreach (explode('&', $query) as $item) {
1507       list($key, $value) = explode('=', $item);
1508       $edit[urldecode($key)] = urldecode($value);
1509     }
1510     return $edit;
1511   }
1512
1513   /**
1514    * Checks for meta refresh tag and if found call drupalGet() recursively.
1515    *
1516    * This function looks for the http-equiv attribute to be set to "Refresh" and
1517    * is case-sensitive.
1518    *
1519    * @return
1520    *   Either the new page content or FALSE.
1521    */
1522   protected function checkForMetaRefresh() {
1523     if (strpos($this->getRawContent(), '<meta ') && $this->parse() && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
1524       $refresh = $this->xpath('//meta[@http-equiv="Refresh"]');
1525       if (!empty($refresh)) {
1526         // Parse the content attribute of the meta tag for the format:
1527         // "[delay]: URL=[page_to_redirect_to]".
1528         if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]['content'], $match)) {
1529           $this->metaRefreshCount++;
1530           return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
1531         }
1532       }
1533     }
1534     return FALSE;
1535   }
1536
1537   /**
1538    * Retrieves only the headers for a Drupal path or an absolute path.
1539    *
1540    * @param $path
1541    *   Drupal path or URL to load into internal browser
1542    * @param $options
1543    *   Options to be forwarded to the url generator.
1544    * @param $headers
1545    *   An array containing additional HTTP request headers, each formatted as
1546    *   "name: value".
1547    *
1548    * @return
1549    *   The retrieved headers, also available as $this->getRawContent()
1550    */
1551   protected function drupalHead($path, array $options = [], array $headers = []) {
1552     $options['absolute'] = TRUE;
1553     $url = $this->buildUrl($path, $options);
1554     $out = $this->curlExec([CURLOPT_NOBODY => TRUE, CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers]);
1555     // Ensure that any changes to variables in the other thread are picked up.
1556     $this->refreshVariables();
1557
1558     if ($this->dumpHeaders) {
1559       $this->verbose('GET request to: ' . $path .
1560                      '<hr />Ending URL: ' . $this->getUrl() .
1561                      '<hr />Headers: <pre>' . Html::escape(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>');
1562     }
1563
1564     return $out;
1565   }
1566
1567   /**
1568    * Handles form input related to drupalPostForm().
1569    *
1570    * Ensure that the specified fields exist and attempt to create POST data in
1571    * the correct manner for the particular field type.
1572    *
1573    * @param $post
1574    *   Reference to array of post values.
1575    * @param $edit
1576    *   Reference to array of edit values to be checked against the form.
1577    * @param $submit
1578    *   Form submit button value.
1579    * @param $form
1580    *   Array of form elements.
1581    *
1582    * @return
1583    *   Submit value matches a valid submit input in the form.
1584    */
1585   protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
1586     // Retrieve the form elements.
1587     $elements = $form->xpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]');
1588     $submit_matches = FALSE;
1589     foreach ($elements as $element) {
1590       // SimpleXML objects need string casting all the time.
1591       $name = (string) $element['name'];
1592       // This can either be the type of <input> or the name of the tag itself
1593       // for <select> or <textarea>.
1594       $type = isset($element['type']) ? (string) $element['type'] : $element->getName();
1595       $value = isset($element['value']) ? (string) $element['value'] : '';
1596       $done = FALSE;
1597       if (isset($edit[$name])) {
1598         switch ($type) {
1599           case 'text':
1600           case 'tel':
1601           case 'textarea':
1602           case 'url':
1603           case 'number':
1604           case 'range':
1605           case 'color':
1606           case 'hidden':
1607           case 'password':
1608           case 'email':
1609           case 'search':
1610           case 'date':
1611           case 'time':
1612           case 'datetime':
1613           case 'datetime-local';
1614             $post[$name] = $edit[$name];
1615             unset($edit[$name]);
1616             break;
1617           case 'radio':
1618             if ($edit[$name] == $value) {
1619               $post[$name] = $edit[$name];
1620               unset($edit[$name]);
1621             }
1622             break;
1623           case 'checkbox':
1624             // To prevent checkbox from being checked.pass in a FALSE,
1625             // otherwise the checkbox will be set to its value regardless
1626             // of $edit.
1627             if ($edit[$name] === FALSE) {
1628               unset($edit[$name]);
1629               continue 2;
1630             }
1631             else {
1632               unset($edit[$name]);
1633               $post[$name] = $value;
1634             }
1635             break;
1636           case 'select':
1637             $new_value = $edit[$name];
1638             $options = $this->getAllOptions($element);
1639             if (is_array($new_value)) {
1640               // Multiple select box.
1641               if (!empty($new_value)) {
1642                 $index = 0;
1643                 $key = preg_replace('/\[\]$/', '', $name);
1644                 foreach ($options as $option) {
1645                   $option_value = (string) $option['value'];
1646                   if (in_array($option_value, $new_value)) {
1647                     $post[$key . '[' . $index++ . ']'] = $option_value;
1648                     $done = TRUE;
1649                     unset($edit[$name]);
1650                   }
1651                 }
1652               }
1653               else {
1654                 // No options selected: do not include any POST data for the
1655                 // element.
1656                 $done = TRUE;
1657                 unset($edit[$name]);
1658               }
1659             }
1660             else {
1661               // Single select box.
1662               foreach ($options as $option) {
1663                 if ($new_value == $option['value']) {
1664                   $post[$name] = $new_value;
1665                   unset($edit[$name]);
1666                   $done = TRUE;
1667                   break;
1668                 }
1669               }
1670             }
1671             break;
1672           case 'file':
1673             $upload[$name] = $edit[$name];
1674             unset($edit[$name]);
1675             break;
1676         }
1677       }
1678       if (!isset($post[$name]) && !$done) {
1679         switch ($type) {
1680           case 'textarea':
1681             $post[$name] = (string) $element;
1682             break;
1683           case 'select':
1684             $single = empty($element['multiple']);
1685             $first = TRUE;
1686             $index = 0;
1687             $key = preg_replace('/\[\]$/', '', $name);
1688             $options = $this->getAllOptions($element);
1689             foreach ($options as $option) {
1690               // For single select, we load the first option, if there is a
1691               // selected option that will overwrite it later.
1692               if ($option['selected'] || ($first && $single)) {
1693                 $first = FALSE;
1694                 if ($single) {
1695                   $post[$name] = (string) $option['value'];
1696                 }
1697                 else {
1698                   $post[$key . '[' . $index++ . ']'] = (string) $option['value'];
1699                 }
1700               }
1701             }
1702             break;
1703           case 'file':
1704             break;
1705           case 'submit':
1706           case 'image':
1707             if (isset($submit) && $submit == $value) {
1708               $post[$name] = $value;
1709               $submit_matches = TRUE;
1710             }
1711             break;
1712           case 'radio':
1713           case 'checkbox':
1714             if (!isset($element['checked'])) {
1715               break;
1716             }
1717             // Deliberate no break.
1718           default:
1719             $post[$name] = $value;
1720         }
1721       }
1722     }
1723     // An empty name means the value is not sent.
1724     unset($post['']);
1725     return $submit_matches;
1726   }
1727
1728   /**
1729    * Follows a link by complete name.
1730    *
1731    * Will click the first link found with this link text by default, or a later
1732    * one if an index is given. Match is case sensitive with normalized space.
1733    * The label is translated label.
1734    *
1735    * If the link is discovered and clicked, the test passes. Fail otherwise.
1736    *
1737    * @param string|\Drupal\Component\Render\MarkupInterface $label
1738    *   Text between the anchor tags.
1739    * @param int $index
1740    *   Link position counting from zero.
1741    *
1742    * @return string|bool
1743    *   Page contents on success, or FALSE on failure.
1744    */
1745   protected function clickLink($label, $index = 0) {
1746     return $this->clickLinkHelper($label, $index, '//a[normalize-space()=:label]');
1747   }
1748
1749   /**
1750    * Follows a link by partial name.
1751    *
1752    * If the link is discovered and clicked, the test passes. Fail otherwise.
1753    *
1754    * @param string|\Drupal\Component\Render\MarkupInterface $label
1755    *   Text between the anchor tags, uses starts-with().
1756    * @param int $index
1757    *   Link position counting from zero.
1758    *
1759    * @return string|bool
1760    *   Page contents on success, or FALSE on failure.
1761    *
1762    * @see ::clickLink()
1763    */
1764   protected function clickLinkPartialName($label, $index = 0) {
1765     return $this->clickLinkHelper($label, $index, '//a[starts-with(normalize-space(), :label)]');
1766   }
1767
1768   /**
1769    * Provides a helper for ::clickLink() and ::clickLinkPartialName().
1770    *
1771    * @param string|\Drupal\Component\Render\MarkupInterface $label
1772    *   Text between the anchor tags, uses starts-with().
1773    * @param int $index
1774    *   Link position counting from zero.
1775    * @param string $pattern
1776    *   A pattern to use for the XPath.
1777    *
1778    * @return bool|string
1779    *   Page contents on success, or FALSE on failure.
1780    */
1781   protected function clickLinkHelper($label, $index, $pattern) {
1782     // Cast MarkupInterface objects to string.
1783     $label = (string) $label;
1784     $url_before = $this->getUrl();
1785     $urls = $this->xpath($pattern, [':label' => $label]);
1786     if (isset($urls[$index])) {
1787       $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
1788       $this->pass(SafeMarkup::format('Clicked link %label (@url_target) from @url_before', ['%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before]), 'Browser');
1789       return $this->drupalGet($url_target);
1790     }
1791     $this->fail(SafeMarkup::format('Link %label does not exist on @url_before', ['%label' => $label, '@url_before' => $url_before]), 'Browser');
1792     return FALSE;
1793   }
1794
1795   /**
1796    * Takes a path and returns an absolute path.
1797    *
1798    * This method is implemented in the way that browsers work, see
1799    * https://url.spec.whatwg.org/#relative-state for more information about the
1800    * possible cases.
1801    *
1802    * @param string $path
1803    *   A path from the internal browser content.
1804    *
1805    * @return string
1806    *   The $path with $base_url prepended, if necessary.
1807    */
1808   protected function getAbsoluteUrl($path) {
1809     global $base_url, $base_path;
1810
1811     $parts = parse_url($path);
1812
1813     // In case the $path has a host, it is already an absolute URL and we are
1814     // done.
1815     if (!empty($parts['host'])) {
1816       return $path;
1817     }
1818
1819     // In case the $path contains just a query, we turn it into an absolute URL
1820     // with the same scheme, host and path, see
1821     // https://url.spec.whatwg.org/#relative-state.
1822     if (array_keys($parts) === ['query']) {
1823       $current_uri = new Uri($this->getUrl());
1824       return (string) $current_uri->withQuery($parts['query']);
1825     }
1826
1827     if (empty($parts['host'])) {
1828       // Ensure that we have a string (and no xpath object).
1829       $path = (string) $path;
1830       // Strip $base_path, if existent.
1831       $length = strlen($base_path);
1832       if (substr($path, 0, $length) === $base_path) {
1833         $path = substr($path, $length);
1834       }
1835       // Ensure that we have an absolute path.
1836       if (empty($path) || $path[0] !== '/') {
1837         $path = '/' . $path;
1838       }
1839       // Finally, prepend the $base_url.
1840       $path = $base_url . $path;
1841     }
1842     return $path;
1843   }
1844
1845   /**
1846    * Gets the HTTP response headers of the requested page.
1847    *
1848    * Normally we are only interested in the headers returned by the last
1849    * request. However, if a page is redirected or HTTP authentication is in use,
1850    * multiple requests will be required to retrieve the page. Headers from all
1851    * requests may be requested by passing TRUE to this function.
1852    *
1853    * @param $all_requests
1854    *   Boolean value specifying whether to return headers from all requests
1855    *   instead of just the last request. Defaults to FALSE.
1856    *
1857    * @return
1858    *   A name/value array if headers from only the last request are requested.
1859    *   If headers from all requests are requested, an array of name/value
1860    *   arrays, one for each request.
1861    *
1862    *   The pseudonym ":status" is used for the HTTP status line.
1863    *
1864    *   Values for duplicate headers are stored as a single comma-separated list.
1865    */
1866   protected function drupalGetHeaders($all_requests = FALSE) {
1867     $request = 0;
1868     $headers = [$request => []];
1869     foreach ($this->headers as $header) {
1870       $header = trim($header);
1871       if ($header === '') {
1872         $request++;
1873       }
1874       else {
1875         if (strpos($header, 'HTTP/') === 0) {
1876           $name = ':status';
1877           $value = $header;
1878         }
1879         else {
1880           list($name, $value) = explode(':', $header, 2);
1881           $name = strtolower($name);
1882         }
1883         if (isset($headers[$request][$name])) {
1884           $headers[$request][$name] .= ',' . trim($value);
1885         }
1886         else {
1887           $headers[$request][$name] = trim($value);
1888         }
1889       }
1890     }
1891     if (!$all_requests) {
1892       $headers = array_pop($headers);
1893     }
1894     return $headers;
1895   }
1896
1897   /**
1898    * Gets the value of an HTTP response header.
1899    *
1900    * If multiple requests were required to retrieve the page, only the headers
1901    * from the last request will be checked by default. However, if TRUE is
1902    * passed as the second argument, all requests will be processed from last to
1903    * first until the header is found.
1904    *
1905    * @param $name
1906    *   The name of the header to retrieve. Names are case-insensitive (see RFC
1907    *   2616 section 4.2).
1908    * @param $all_requests
1909    *   Boolean value specifying whether to check all requests if the header is
1910    *   not found in the last request. Defaults to FALSE.
1911    *
1912    * @return
1913    *   The HTTP header value or FALSE if not found.
1914    */
1915   protected function drupalGetHeader($name, $all_requests = FALSE) {
1916     $name = strtolower($name);
1917     $header = FALSE;
1918     if ($all_requests) {
1919       foreach (array_reverse($this->drupalGetHeaders(TRUE)) as $headers) {
1920         if (isset($headers[$name])) {
1921           $header = $headers[$name];
1922           break;
1923         }
1924       }
1925     }
1926     else {
1927       $headers = $this->drupalGetHeaders();
1928       if (isset($headers[$name])) {
1929         $header = $headers[$name];
1930       }
1931     }
1932     return $header;
1933   }
1934
1935   /**
1936    * Check if a HTTP response header exists and has the expected value.
1937    *
1938    * @param string $header
1939    *   The header key, example: Content-Type
1940    * @param string $value
1941    *   The header value.
1942    * @param string $message
1943    *   (optional) A message to display with the assertion.
1944    * @param string $group
1945    *   (optional) The group this message is in, which is displayed in a column
1946    *   in test output. Use 'Debug' to indicate this is debugging output. Do not
1947    *   translate this string. Defaults to 'Other'; most tests do not override
1948    *   this default.
1949    *
1950    * @return bool
1951    *   TRUE if the assertion succeeded, FALSE otherwise.
1952    */
1953   protected function assertHeader($header, $value, $message = '', $group = 'Browser') {
1954     $header_value = $this->drupalGetHeader($header);
1955     return $this->assertTrue($header_value == $value, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found, actual value: ' . $header_value, $group);
1956   }
1957
1958   /**
1959    * Passes if the internal browser's URL matches the given path.
1960    *
1961    * @param \Drupal\Core\Url|string $path
1962    *   The expected system path or URL.
1963    * @param $options
1964    *   (optional) Any additional options to pass for $path to the url generator.
1965    * @param $message
1966    *   (optional) A message to display with the assertion. Do not translate
1967    *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
1968    *   variables in the message text, not t(). If left blank, a default message
1969    *   will be displayed.
1970    * @param $group
1971    *   (optional) The group this message is in, which is displayed in a column
1972    *   in test output. Use 'Debug' to indicate this is debugging output. Do not
1973    *   translate this string. Defaults to 'Other'; most tests do not override
1974    *   this default.
1975    *
1976    * @return
1977    *   TRUE on pass, FALSE on fail.
1978    */
1979   protected function assertUrl($path, array $options = [], $message = '', $group = 'Other') {
1980     if ($path instanceof Url) {
1981       $url_obj = $path;
1982     }
1983     elseif (UrlHelper::isExternal($path)) {
1984       $url_obj = Url::fromUri($path, $options);
1985     }
1986     else {
1987       $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path;
1988       // This is needed for language prefixing.
1989       $options['path_processing'] = TRUE;
1990       $url_obj = Url::fromUri($uri, $options);
1991     }
1992     $url = $url_obj->setAbsolute()->toString();
1993     if (!$message) {
1994       $message = SafeMarkup::format('Expected @url matches current URL (@current_url).', [
1995         '@url' => var_export($url, TRUE),
1996         '@current_url' => $this->getUrl(),
1997       ]);
1998     }
1999     // Paths in query strings can be encoded or decoded with no functional
2000     // difference, decode them for comparison purposes.
2001     $actual_url = urldecode($this->getUrl());
2002     $expected_url = urldecode($url);
2003     return $this->assertEqual($actual_url, $expected_url, $message, $group);
2004   }
2005
2006   /**
2007    * Asserts the page responds with the specified response code.
2008    *
2009    * @param $code
2010    *   Response code. For example 200 is a successful page request. For a list
2011    *   of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
2012    * @param $message
2013    *   (optional) A message to display with the assertion. Do not translate
2014    *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
2015    *   variables in the message text, not t(). If left blank, a default message
2016    *   will be displayed.
2017    * @param $group
2018    *   (optional) The group this message is in, which is displayed in a column
2019    *   in test output. Use 'Debug' to indicate this is debugging output. Do not
2020    *   translate this string. Defaults to 'Browser'; most tests do not override
2021    *   this default.
2022    *
2023    * @return
2024    *   Assertion result.
2025    */
2026   protected function assertResponse($code, $message = '', $group = 'Browser') {
2027     $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
2028     $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
2029     return $this->assertTrue($match, $message ? $message : SafeMarkup::format('HTTP response expected @code, actual @curl_code', ['@code' => $code, '@curl_code' => $curl_code]), $group);
2030   }
2031
2032   /**
2033    * Asserts the page did not return the specified response code.
2034    *
2035    * @param $code
2036    *   Response code. For example 200 is a successful page request. For a list
2037    *   of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
2038    * @param $message
2039    *   (optional) A message to display with the assertion. Do not translate
2040    *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
2041    *   variables in the message text, not t(). If left blank, a default message
2042    *   will be displayed.
2043    * @param $group
2044    *   (optional) The group this message is in, which is displayed in a column
2045    *   in test output. Use 'Debug' to indicate this is debugging output. Do not
2046    *   translate this string. Defaults to 'Browser'; most tests do not override
2047    *   this default.
2048    *
2049    * @return
2050    *   Assertion result.
2051    */
2052   protected function assertNoResponse($code, $message = '', $group = 'Browser') {
2053     $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
2054     $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
2055     return $this->assertFalse($match, $message ? $message : SafeMarkup::format('HTTP response not expected @code, actual @curl_code', ['@code' => $code, '@curl_code' => $curl_code]), $group);
2056   }
2057
2058   /**
2059    * Builds an a absolute URL from a system path or a URL object.
2060    *
2061    * @param string|\Drupal\Core\Url $path
2062    *   A system path or a URL.
2063    * @param array $options
2064    *   Options to be passed to Url::fromUri().
2065    *
2066    * @return string
2067    *   An absolute URL string.
2068    */
2069   protected function buildUrl($path, array $options = []) {
2070     if ($path instanceof Url) {
2071       $url_options = $path->getOptions();
2072       $options = $url_options + $options;
2073       $path->setOptions($options);
2074       return $path->setAbsolute()->toString(TRUE)->getGeneratedUrl();
2075     }
2076     // The URL generator service is not necessarily available yet; e.g., in
2077     // interactive installer tests.
2078     elseif ($this->container->has('url_generator')) {
2079       $force_internal = isset($options['external']) && $options['external'] == FALSE;
2080       if (!$force_internal && UrlHelper::isExternal($path)) {
2081         return Url::fromUri($path, $options)->toString();
2082       }
2083       else {
2084         $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path;
2085         // Path processing is needed for language prefixing.  Skip it when a
2086         // path that may look like an external URL is being used as internal.
2087         $options['path_processing'] = !$force_internal;
2088         return Url::fromUri($uri, $options)
2089           ->setAbsolute()
2090           ->toString();
2091       }
2092     }
2093     else {
2094       return $this->getAbsoluteUrl($path);
2095     }
2096   }
2097
2098   /**
2099    * Asserts whether an expected cache tag was present in the last response.
2100    *
2101    * @param string $expected_cache_tag
2102    *   The expected cache tag.
2103    */
2104   protected function assertCacheTag($expected_cache_tag) {
2105     $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
2106     $this->assertTrue(in_array($expected_cache_tag, $cache_tags), "'" . $expected_cache_tag . "' is present in the X-Drupal-Cache-Tags header.");
2107   }
2108
2109   /**
2110    * Asserts whether an expected cache tag was absent in the last response.
2111    *
2112    * @param string $cache_tag
2113    *   The cache tag to check.
2114    */
2115   protected function assertNoCacheTag($cache_tag) {
2116     $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
2117     $this->assertFalse(in_array($cache_tag, $cache_tags), "'" . $cache_tag . "' is absent in the X-Drupal-Cache-Tags header.");
2118   }
2119
2120   /**
2121    * Enables/disables the cacheability headers.
2122    *
2123    * Sets the http.response.debug_cacheability_headers container parameter.
2124    *
2125    * @param bool $value
2126    *   (optional) Whether the debugging cacheability headers should be sent.
2127    */
2128   protected function setHttpResponseDebugCacheabilityHeaders($value = TRUE) {
2129     $this->setContainerParameter('http.response.debug_cacheability_headers', $value);
2130     $this->rebuildContainer();
2131     $this->resetAll();
2132   }
2133
2134 }