BrowserTestBase.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. <?php
  2. namespace Drupal\Tests;
  3. use Behat\Mink\Driver\GoutteDriver;
  4. use Behat\Mink\Element\Element;
  5. use Behat\Mink\Mink;
  6. use Behat\Mink\Selector\SelectorsHandler;
  7. use Behat\Mink\Session;
  8. use Drupal\Component\Serialization\Json;
  9. use Drupal\Core\Database\Database;
  10. use Drupal\Core\Test\FunctionalTestSetupTrait;
  11. use Drupal\Core\Test\TestSetupTrait;
  12. use Drupal\Core\Utility\Error;
  13. use Drupal\FunctionalTests\AssertLegacyTrait;
  14. use Drupal\Tests\block\Traits\BlockCreationTrait;
  15. use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
  16. use Drupal\Tests\node\Traits\NodeCreationTrait;
  17. use Drupal\Tests\user\Traits\UserCreationTrait;
  18. use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
  19. use GuzzleHttp\Cookie\CookieJar;
  20. use PHPUnit\Framework\TestCase;
  21. use Psr\Http\Message\RequestInterface;
  22. use Psr\Http\Message\ResponseInterface;
  23. use Symfony\Component\CssSelector\CssSelectorConverter;
  24. /**
  25. * Provides a test case for functional Drupal tests.
  26. *
  27. * Tests extending BrowserTestBase must exist in the
  28. * Drupal\Tests\yourmodule\Functional namespace and live in the
  29. * modules/yourmodule/tests/src/Functional directory.
  30. *
  31. * Tests extending this base class should only translate text when testing
  32. * translation functionality. For example, avoid wrapping test text with t()
  33. * or TranslatableMarkup().
  34. *
  35. * @ingroup testing
  36. */
  37. abstract class BrowserTestBase extends TestCase {
  38. use FunctionalTestSetupTrait;
  39. use UiHelperTrait {
  40. FunctionalTestSetupTrait::refreshVariables insteadof UiHelperTrait;
  41. }
  42. use TestSetupTrait;
  43. use BlockCreationTrait {
  44. placeBlock as drupalPlaceBlock;
  45. }
  46. use AssertLegacyTrait;
  47. use RandomGeneratorTrait;
  48. use NodeCreationTrait {
  49. getNodeByTitle as drupalGetNodeByTitle;
  50. createNode as drupalCreateNode;
  51. }
  52. use ContentTypeCreationTrait {
  53. createContentType as drupalCreateContentType;
  54. }
  55. use ConfigTestTrait;
  56. use TestRequirementsTrait;
  57. use UserCreationTrait {
  58. createRole as drupalCreateRole;
  59. createUser as drupalCreateUser;
  60. }
  61. use XdebugRequestTrait;
  62. use PhpunitCompatibilityTrait;
  63. /**
  64. * The database prefix of this test run.
  65. *
  66. * @var string
  67. */
  68. protected $databasePrefix;
  69. /**
  70. * Time limit in seconds for the test.
  71. *
  72. * @var int
  73. */
  74. protected $timeLimit = 500;
  75. /**
  76. * The translation file directory for the test environment.
  77. *
  78. * This is set in BrowserTestBase::prepareEnvironment().
  79. *
  80. * @var string
  81. */
  82. protected $translationFilesDirectory;
  83. /**
  84. * The config importer that can be used in a test.
  85. *
  86. * @var \Drupal\Core\Config\ConfigImporter
  87. */
  88. protected $configImporter;
  89. /**
  90. * Modules to enable.
  91. *
  92. * The test runner will merge the $modules lists from this class, the class
  93. * it extends, and so on up the class hierarchy. It is not necessary to
  94. * include modules in your list that a parent class has already declared.
  95. *
  96. * @var string[]
  97. *
  98. * @see \Drupal\Tests\BrowserTestBase::installDrupal()
  99. */
  100. protected static $modules = [];
  101. /**
  102. * The profile to install as a basis for testing.
  103. *
  104. * @var string
  105. */
  106. protected $profile = 'testing';
  107. /**
  108. * The theme to install as the default for testing.
  109. *
  110. * Defaults to the install profile's default theme, if it specifies any.
  111. *
  112. * @var string
  113. */
  114. protected $defaultTheme;
  115. /**
  116. * An array of custom translations suitable for drupal_rewrite_settings().
  117. *
  118. * @var array
  119. */
  120. protected $customTranslations;
  121. /*
  122. * Mink class for the default driver to use.
  123. *
  124. * Should be a fully-qualified class name that implements
  125. * Behat\Mink\Driver\DriverInterface.
  126. *
  127. * Value can be overridden using the environment variable MINK_DRIVER_CLASS.
  128. *
  129. * @var string
  130. */
  131. protected $minkDefaultDriverClass = GoutteDriver::class;
  132. /*
  133. * Mink default driver params.
  134. *
  135. * If it's an array its contents are used as constructor params when default
  136. * Mink driver class is instantiated.
  137. *
  138. * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
  139. * case that variable should be a JSON array, for example:
  140. * '["firefox", null, "http://localhost:4444/wd/hub"]'.
  141. *
  142. *
  143. * @var array
  144. */
  145. protected $minkDefaultDriverArgs;
  146. /**
  147. * Mink session manager.
  148. *
  149. * This will not be initialized if there was an error during the test setup.
  150. *
  151. * @var \Behat\Mink\Mink|null
  152. */
  153. protected $mink;
  154. /**
  155. * {@inheritdoc}
  156. *
  157. * Browser tests are run in separate processes to prevent collisions between
  158. * code that may be loaded by tests.
  159. */
  160. protected $runTestInSeparateProcess = TRUE;
  161. /**
  162. * {@inheritdoc}
  163. */
  164. protected $preserveGlobalState = FALSE;
  165. /**
  166. * The base URL.
  167. *
  168. * @var string
  169. */
  170. protected $baseUrl;
  171. /**
  172. * The original array of shutdown function callbacks.
  173. *
  174. * @var array
  175. */
  176. protected $originalShutdownCallbacks = [];
  177. /**
  178. * The app root.
  179. *
  180. * @var string
  181. */
  182. protected $root;
  183. /**
  184. * The original container.
  185. *
  186. * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no
  187. * longer provides the same value.
  188. *
  189. * @var \Symfony\Component\DependencyInjection\ContainerInterface
  190. */
  191. protected $originalContainer;
  192. /**
  193. * {@inheritdoc}
  194. */
  195. public function __construct($name = NULL, array $data = [], $dataName = '') {
  196. parent::__construct($name, $data, $dataName);
  197. $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
  198. }
  199. /**
  200. * Initializes Mink sessions.
  201. */
  202. protected function initMink() {
  203. $driver = $this->getDefaultDriverInstance();
  204. if ($driver instanceof GoutteDriver) {
  205. // Turn off curl timeout. Having a timeout is not a problem in a normal
  206. // test running, but it is a problem when debugging. Also, disable SSL
  207. // peer verification so that testing under HTTPS always works.
  208. /** @var \GuzzleHttp\Client $client */
  209. $client = $this->container->get('http_client_factory')->fromOptions([
  210. 'timeout' => NULL,
  211. 'verify' => FALSE,
  212. ]);
  213. // Inject a Guzzle middleware to generate debug output for every request
  214. // performed in the test.
  215. $handler_stack = $client->getConfig('handler');
  216. $handler_stack->push($this->getResponseLogHandler());
  217. $driver->getClient()->setClient($client);
  218. }
  219. $selectors_handler = new SelectorsHandler([
  220. 'hidden_field_selector' => new HiddenFieldSelector(),
  221. ]);
  222. $session = new Session($driver, $selectors_handler);
  223. $this->mink = new Mink();
  224. $this->mink->registerSession('default', $session);
  225. $this->mink->setDefaultSessionName('default');
  226. $this->registerSessions();
  227. $this->initFrontPage();
  228. // Copies cookies from the current environment, for example, XDEBUG_SESSION
  229. // in order to support Xdebug.
  230. // @see BrowserTestBase::initFrontPage()
  231. $cookies = $this->extractCookiesFromRequest(\Drupal::request());
  232. foreach ($cookies as $cookie_name => $values) {
  233. foreach ($values as $value) {
  234. $session->setCookie($cookie_name, $value);
  235. }
  236. }
  237. return $session;
  238. }
  239. /**
  240. * Visits the front page when initializing Mink.
  241. *
  242. * According to the W3C WebDriver specification a cookie can only be set if
  243. * the cookie domain is equal to the domain of the active document. When the
  244. * browser starts up the active document is not our domain but 'about:blank'
  245. * or similar. To be able to set our User-Agent and Xdebug cookies at the
  246. * start of the test we now do a request to the front page so the active
  247. * document matches the domain.
  248. *
  249. * @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
  250. * @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
  251. */
  252. protected function initFrontPage() {
  253. $session = $this->getSession();
  254. $session->visit($this->baseUrl);
  255. }
  256. /**
  257. * Gets an instance of the default Mink driver.
  258. *
  259. * @return Behat\Mink\Driver\DriverInterface
  260. * Instance of default Mink driver.
  261. *
  262. * @throws \InvalidArgumentException
  263. * When provided default Mink driver class can't be instantiated.
  264. */
  265. protected function getDefaultDriverInstance() {
  266. // Get default driver params from environment if available.
  267. if ($arg_json = $this->getMinkDriverArgs()) {
  268. $this->minkDefaultDriverArgs = json_decode($arg_json, TRUE);
  269. }
  270. // Get and check default driver class from environment if available.
  271. if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
  272. if (class_exists($minkDriverClass)) {
  273. $this->minkDefaultDriverClass = $minkDriverClass;
  274. }
  275. else {
  276. throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class.");
  277. }
  278. }
  279. if (is_array($this->minkDefaultDriverArgs)) {
  280. // Use ReflectionClass to instantiate class with received params.
  281. $reflector = new \ReflectionClass($this->minkDefaultDriverClass);
  282. $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
  283. }
  284. else {
  285. $driver = new $this->minkDefaultDriverClass();
  286. }
  287. return $driver;
  288. }
  289. /**
  290. * Get the Mink driver args from an environment variable, if it is set. Can
  291. * be overridden in a derived class so it is possible to use a different
  292. * value for a subset of tests, e.g. the JavaScript tests.
  293. *
  294. * @return string|false
  295. * The JSON-encoded argument string. False if it is not set.
  296. */
  297. protected function getMinkDriverArgs() {
  298. return getenv('MINK_DRIVER_ARGS');
  299. }
  300. /**
  301. * Provides a Guzzle middleware handler to log every response received.
  302. *
  303. * @return callable
  304. * The callable handler that will do the logging.
  305. */
  306. protected function getResponseLogHandler() {
  307. return function (callable $handler) {
  308. return function (RequestInterface $request, array $options) use ($handler) {
  309. return $handler($request, $options)
  310. ->then(function (ResponseInterface $response) use ($request) {
  311. if ($this->htmlOutputEnabled) {
  312. $caller = $this->getTestMethodCaller();
  313. $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
  314. $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
  315. // Get the response body as a string. Any errors are silenced as
  316. // tests should not fail if there is a problem. On PHP 7.4
  317. // \Drupal\Tests\migrate\Functional\process\DownloadFunctionalTest
  318. // fails without the usage of a silence operator.
  319. $body = @(string) $response->getBody();
  320. // On redirect responses (status code starting with '3') we need
  321. // to remove the meta tag that would do a browser refresh. We
  322. // don't want to redirect developers away when they look at the
  323. // debug output file in their browser.
  324. $status_code = (string) $response->getStatusCode();
  325. if ($status_code[0] === '3') {
  326. $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
  327. }
  328. $html_output .= '<hr />' . $body;
  329. $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
  330. $this->htmlOutput($html_output);
  331. }
  332. return $response;
  333. });
  334. };
  335. };
  336. }
  337. /**
  338. * Registers additional Mink sessions.
  339. *
  340. * Tests wishing to use a different driver or change the default driver should
  341. * override this method.
  342. *
  343. * @code
  344. * // Register a new session that uses the MinkPonyDriver.
  345. * $pony = new MinkPonyDriver();
  346. * $session = new Session($pony);
  347. * $this->mink->registerSession('pony', $session);
  348. * @endcode
  349. */
  350. protected function registerSessions() {}
  351. /**
  352. * {@inheritdoc}
  353. */
  354. protected function setUp() {
  355. parent::setUp();
  356. // Allow tests to compare MarkupInterface objects via assertEquals().
  357. $this->registerComparator(new MarkupInterfaceComparator());
  358. $this->setupBaseUrl();
  359. // Install Drupal test site.
  360. $this->prepareEnvironment();
  361. $this->installDrupal();
  362. // Setup Mink.
  363. $this->initMink();
  364. // Set up the browser test output file.
  365. $this->initBrowserOutputFile();
  366. // Ensure that the test is not marked as risky because of no assertions. In
  367. // PHPUnit 6 tests that only make assertions using $this->assertSession()
  368. // can be marked as risky.
  369. $this->addToAssertionCount(1);
  370. }
  371. /**
  372. * Ensures test files are deletable.
  373. *
  374. * Some tests chmod generated files to be read only. During
  375. * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
  376. * these files need to get deleted too.
  377. *
  378. * @param string $path
  379. * The file path.
  380. *
  381. * @see \Drupal\Core\File\FileSystemInterface::deleteRecursive()
  382. */
  383. public static function filePreDeleteCallback($path) {
  384. // When the webserver runs with the same system user as phpunit, we can
  385. // make read-only files writable again. If not, chmod will fail while the
  386. // file deletion still works if file permissions have been configured
  387. // correctly. Thus, we ignore any problems while running chmod.
  388. @chmod($path, 0700);
  389. }
  390. /**
  391. * Clean up the Simpletest environment.
  392. */
  393. protected function cleanupEnvironment() {
  394. // Remove all prefixed tables.
  395. $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
  396. $original_prefix = $original_connection_info['default']['prefix']['default'];
  397. $test_connection_info = Database::getConnectionInfo('default');
  398. $test_prefix = $test_connection_info['default']['prefix']['default'];
  399. if ($original_prefix != $test_prefix) {
  400. $tables = Database::getConnection()->schema()->findTables('%');
  401. foreach ($tables as $table) {
  402. if (Database::getConnection()->schema()->dropTable($table)) {
  403. unset($tables[$table]);
  404. }
  405. }
  406. }
  407. // Delete test site directory.
  408. \Drupal::service('file_system')->deleteRecursive($this->siteDirectory, [$this, 'filePreDeleteCallback']);
  409. }
  410. /**
  411. * {@inheritdoc}
  412. */
  413. protected function tearDown() {
  414. parent::tearDown();
  415. // Destroy the testing kernel.
  416. if (isset($this->kernel)) {
  417. $this->cleanupEnvironment();
  418. $this->kernel->shutdown();
  419. }
  420. // Ensure that internal logged in variable is reset.
  421. $this->loggedInUser = FALSE;
  422. if ($this->mink) {
  423. $this->mink->stopSessions();
  424. }
  425. // Restore original shutdown callbacks.
  426. if (function_exists('drupal_register_shutdown_function')) {
  427. $callbacks = &drupal_register_shutdown_function();
  428. $callbacks = $this->originalShutdownCallbacks;
  429. }
  430. }
  431. /**
  432. * Returns Mink session.
  433. *
  434. * @param string $name
  435. * (optional) Name of the session. Defaults to the active session.
  436. *
  437. * @return \Behat\Mink\Session
  438. * The active Mink session object.
  439. */
  440. public function getSession($name = NULL) {
  441. return $this->mink->getSession($name);
  442. }
  443. /**
  444. * Get session cookies from current session.
  445. *
  446. * @return \GuzzleHttp\Cookie\CookieJar
  447. * A cookie jar with the current session.
  448. */
  449. protected function getSessionCookies() {
  450. $domain = parse_url($this->getUrl(), PHP_URL_HOST);
  451. $session_id = $this->getSession()->getCookie($this->getSessionName());
  452. $cookies = CookieJar::fromArray([$this->getSessionName() => $session_id], $domain);
  453. return $cookies;
  454. }
  455. /**
  456. * Obtain the HTTP client for the system under test.
  457. *
  458. * Use this method for arbitrary HTTP requests to the site under test. For
  459. * most tests, you should not get the HTTP client and instead use navigation
  460. * methods such as drupalGet() and clickLink() in order to benefit from
  461. * assertions.
  462. *
  463. * Subclasses which substitute a different Mink driver should override this
  464. * method and provide a Guzzle client if the Mink driver provides one.
  465. *
  466. * @return \GuzzleHttp\ClientInterface
  467. * The client with BrowserTestBase configuration.
  468. *
  469. * @throws \RuntimeException
  470. * If the Mink driver does not support a Guzzle HTTP client, throw an
  471. * exception.
  472. */
  473. protected function getHttpClient() {
  474. /* @var $mink_driver \Behat\Mink\Driver\DriverInterface */
  475. $mink_driver = $this->getSession()->getDriver();
  476. if ($mink_driver instanceof GoutteDriver) {
  477. return $mink_driver->getClient()->getClient();
  478. }
  479. throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().');
  480. }
  481. /**
  482. * Helper function to get the options of select field.
  483. *
  484. * @param \Behat\Mink\Element\NodeElement|string $select
  485. * Name, ID, or Label of select field to assert.
  486. * @param \Behat\Mink\Element\Element $container
  487. * (optional) Container element to check against. Defaults to current page.
  488. *
  489. * @return array
  490. * Associative array of option keys and values.
  491. */
  492. protected function getOptions($select, Element $container = NULL) {
  493. if (is_string($select)) {
  494. $select = $this->assertSession()->selectExists($select, $container);
  495. }
  496. $options = [];
  497. /* @var \Behat\Mink\Element\NodeElement $option */
  498. foreach ($select->findAll('xpath', '//option') as $option) {
  499. $label = $option->getText();
  500. $value = $option->getAttribute('value') ?: $label;
  501. $options[$value] = $label;
  502. }
  503. return $options;
  504. }
  505. /**
  506. * Installs Drupal into the Simpletest site.
  507. */
  508. public function installDrupal() {
  509. $this->initUserSession();
  510. $this->prepareSettings();
  511. $this->doInstall();
  512. $this->initSettings();
  513. $container = $this->initKernel(\Drupal::request());
  514. $this->initConfig($container);
  515. $this->installDefaultThemeFromClassProperty($container);
  516. $this->installModulesFromClassProperty($container);
  517. $this->rebuildAll();
  518. }
  519. /**
  520. * Prevents serializing any properties.
  521. *
  522. * Browser tests are run in a separate process. To do this PHPUnit creates a
  523. * script to run the test. If it fails, the test result object will contain a
  524. * stack trace which includes the test object. It will attempt to serialize
  525. * it. Returning an empty array prevents it from serializing anything it
  526. * should not.
  527. *
  528. * @return array
  529. * An empty array.
  530. *
  531. * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
  532. */
  533. public function __sleep() {
  534. return [];
  535. }
  536. /**
  537. * Translates a CSS expression to its XPath equivalent.
  538. *
  539. * The search is relative to the root element (HTML tag normally) of the page.
  540. *
  541. * @param string $selector
  542. * CSS selector to use in the search.
  543. * @param bool $html
  544. * (optional) Enables HTML support. Disable it for XML documents.
  545. * @param string $prefix
  546. * (optional) The prefix for the XPath expression.
  547. *
  548. * @return string
  549. * The equivalent XPath of a CSS expression.
  550. */
  551. protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') {
  552. return (new CssSelectorConverter($html))->toXPath($selector, $prefix);
  553. }
  554. /**
  555. * Performs an xpath search on the contents of the internal browser.
  556. *
  557. * The search is relative to the root element (HTML tag normally) of the page.
  558. *
  559. * @param string $xpath
  560. * The xpath string to use in the search.
  561. * @param array $arguments
  562. * An array of arguments with keys in the form ':name' matching the
  563. * placeholders in the query. The values may be either strings or numeric
  564. * values.
  565. *
  566. * @return \Behat\Mink\Element\NodeElement[]
  567. * The list of elements matching the xpath expression.
  568. */
  569. protected function xpath($xpath, array $arguments = []) {
  570. $xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments);
  571. return $this->getSession()->getPage()->findAll('xpath', $xpath);
  572. }
  573. /**
  574. * Configuration accessor for tests. Returns non-overridden configuration.
  575. *
  576. * @param string $name
  577. * Configuration name.
  578. *
  579. * @return \Drupal\Core\Config\Config
  580. * The configuration object with original configuration data.
  581. */
  582. protected function config($name) {
  583. return $this->container->get('config.factory')->getEditable($name);
  584. }
  585. /**
  586. * Returns all response headers.
  587. *
  588. * @return array
  589. * The HTTP headers values.
  590. *
  591. * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0.
  592. * Use $this->getSession()->getResponseHeaders() instead.
  593. *
  594. * @see https://www.drupal.org/node/3067207
  595. */
  596. protected function drupalGetHeaders() {
  597. @trigger_error('Drupal\Tests\BrowserTestBase::drupalGetHeaders() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use $this->getSession()->getResponseHeaders() instead. See https://www.drupal.org/node/3067207', E_USER_DEPRECATED);
  598. return $this->getSession()->getResponseHeaders();
  599. }
  600. /**
  601. * Gets the value of an HTTP response header.
  602. *
  603. * If multiple requests were required to retrieve the page, only the headers
  604. * from the last request will be checked by default.
  605. *
  606. * @param string $name
  607. * The name of the header to retrieve. Names are case-insensitive (see RFC
  608. * 2616 section 4.2).
  609. *
  610. * @return string|null
  611. * The HTTP header value or NULL if not found.
  612. */
  613. protected function drupalGetHeader($name) {
  614. return $this->getSession()->getResponseHeader($name);
  615. }
  616. /**
  617. * Gets the JavaScript drupalSettings variable for the currently-loaded page.
  618. *
  619. * @return array
  620. * The JSON decoded drupalSettings value from the current page.
  621. */
  622. protected function getDrupalSettings() {
  623. $html = $this->getSession()->getPage()->getContent();
  624. if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
  625. return Json::decode($matches[1]);
  626. }
  627. return [];
  628. }
  629. /**
  630. * Retrieves the current calling line in the class under test.
  631. *
  632. * @return array
  633. * An associative array with keys 'file', 'line' and 'function'.
  634. */
  635. protected function getTestMethodCaller() {
  636. $backtrace = debug_backtrace();
  637. // Find the test class that has the test method.
  638. while ($caller = Error::getLastCaller($backtrace)) {
  639. // If we match PHPUnit's TestCase::runTest, then the previously processed
  640. // caller entry is where our test method sits.
  641. if (isset($last_caller) && isset($caller['function']) && $caller['function'] === 'PHPUnit\Framework\TestCase->runTest()') {
  642. // Return the last caller since that has to be the test class.
  643. $caller = $last_caller;
  644. break;
  645. }
  646. // If the test method is implemented by a test class's parent then the
  647. // class name of $this will not be part of the backtrace.
  648. // In that case we process the backtrace until the caller is not a
  649. // subclass of $this and return the previous caller.
  650. if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
  651. // Return the last caller since that has to be the test class.
  652. $caller = $last_caller;
  653. break;
  654. }
  655. if (isset($caller['class']) && $caller['class'] === get_class($this)) {
  656. break;
  657. }
  658. // Otherwise we have not reached our test class yet: save the last caller
  659. // and remove an element from to backtrace to process the next call.
  660. $last_caller = $caller;
  661. array_shift($backtrace);
  662. }
  663. return $caller;
  664. }
  665. /**
  666. * Transforms a nested array into a flat array suitable for drupalPostForm().
  667. *
  668. * @param array $values
  669. * A multi-dimensional form values array to convert.
  670. *
  671. * @return array
  672. * The flattened $edit array suitable for BrowserTestBase::drupalPostForm().
  673. */
  674. protected function translatePostValues(array $values) {
  675. $edit = [];
  676. // The easiest and most straightforward way to translate values suitable for
  677. // BrowserTestBase::drupalPostForm() is to actually build the POST data
  678. // string and convert the resulting key/value pairs back into a flat array.
  679. $query = http_build_query($values);
  680. foreach (explode('&', $query) as $item) {
  681. list($key, $value) = explode('=', $item);
  682. $edit[urldecode($key)] = urldecode($value);
  683. }
  684. return $edit;
  685. }
  686. }