diff --git a/core/modules/action/src/Tests/ActionUninstallTest.php b/core/modules/action/tests/src/Functional/ActionUninstallTest.php similarity index 74% rename from core/modules/action/src/Tests/ActionUninstallTest.php rename to core/modules/action/tests/src/Functional/ActionUninstallTest.php index c4d5ba3..b718cb0 100644 --- a/core/modules/action/src/Tests/ActionUninstallTest.php +++ b/core/modules/action/tests/src/Functional/ActionUninstallTest.php @@ -1,8 +1,8 @@ container->get('entity_type.manager')->getStorage('action'); $storage->resetCache(['user_block_user_action']); - $this->assertTrue($storage->load('user_block_user_action'), 'Configuration entity \'user_block_user_action\' still exists after uninstalling action module.' ); + $this->assertNotNull($storage->load('user_block_user_action'), 'Configuration entity \'user_block_user_action\' still exists after uninstalling action module.' ); $admin_user = $this->drupalCreateUser(array('administer users')); $this->drupalLogin($admin_user); diff --git a/core/modules/action/src/Tests/BulkFormTest.php b/core/modules/action/tests/src/Functional/BulkFormTest.php similarity index 94% rename from core/modules/action/src/Tests/BulkFormTest.php rename to core/modules/action/tests/src/Functional/BulkFormTest.php index 773d605..2339bdd 100644 --- a/core/modules/action/src/Tests/BulkFormTest.php +++ b/core/modules/action/tests/src/Functional/BulkFormTest.php @@ -2,7 +2,7 @@ namespace Drupal\action\Tests; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; use Drupal\views\Views; /** @@ -11,7 +11,7 @@ * @group action * @see \Drupal\action\Plugin\views\field\BulkForm */ -class BulkFormTest extends WebTestBase { +class BulkFormTest extends BrowserTestBase { /** * Modules to install. @@ -47,7 +47,7 @@ public function testBulkForm() { // Test that the views edit header appears first. $first_form_element = $this->xpath('//form/div[1][@id = :id]', array(':id' => 'edit-header')); - $this->assertTrue($first_form_element, 'The views form edit header appears first.'); + $this->assertNotEmpty($first_form_element, 'The views form edit header appears first.'); $this->assertFieldById('edit-action', NULL, 'The action select field appears.'); @@ -123,7 +123,7 @@ public function testBulkForm() { // Check the default title. $this->drupalGet('test_bulk_form'); $result = $this->xpath('//label[@for="edit-action"]'); - $this->assertEqual('With selection', (string) $result[0]); + $this->assertEqual('With selection', $result[0]->getText()); // Setup up a different bulk form title. $view = Views::getView('test_bulk_form'); @@ -133,7 +133,7 @@ public function testBulkForm() { $this->drupalGet('test_bulk_form'); $result = $this->xpath('//label[@for="edit-action"]'); - $this->assertEqual('Test title', (string) $result[0]); + $this->assertEqual('Test title', $result[0]->getText()); $this->drupalGet('test_bulk_form'); // Call the node delete action. @@ -146,7 +146,7 @@ public function testBulkForm() { // Make sure we don't show an action message while we are still on the // confirmation page. $errors = $this->xpath('//div[contains(@class, "messages--status")]'); - $this->assertFalse($errors, 'No action message shown.'); + $this->assertEmpty($errors, 'No action message shown.'); $this->drupalPostForm(NULL, array(), t('Delete')); $this->assertText(t('Deleted 5 posts.')); // Check if we got redirected to the original page. diff --git a/core/modules/action/src/Tests/ConfigurationTest.php b/core/modules/action/tests/src/Functional/ConfigurationTest.php similarity index 95% rename from core/modules/action/src/Tests/ConfigurationTest.php rename to core/modules/action/tests/src/Functional/ConfigurationTest.php index 225a528..647e1a7 100644 --- a/core/modules/action/src/Tests/ConfigurationTest.php +++ b/core/modules/action/tests/src/Functional/ConfigurationTest.php @@ -3,8 +3,8 @@ namespace Drupal\action\Tests; use Drupal\Component\Utility\Crypt; -use Drupal\simpletest\WebTestBase; use Drupal\system\Entity\Action; +use Drupal\Tests\BrowserTestBase; /** * Tests complex actions configuration by adding, editing, and deleting a @@ -12,7 +12,7 @@ * * @group action */ -class ConfigurationTest extends WebTestBase { +class ConfigurationTest extends BrowserTestBase { /** * Modules to install. @@ -83,7 +83,7 @@ function testActionConfiguration() { $this->assertNoText($new_action_label, "Make sure the action label does not appear on the overview page after we've deleted the action."); $action = Action::load($aid); - $this->assertFalse($action, 'Make sure the action is gone after being deleted.'); + $this->assertNull($action, 'Make sure the action is gone after being deleted.'); } } diff --git a/core/tests/Drupal/Tests/BrowserAssertTrait.php b/core/tests/Drupal/Tests/BrowserAssertTrait.php new file mode 100644 index 0000000..22e9c5e --- /dev/null +++ b/core/tests/Drupal/Tests/BrowserAssertTrait.php @@ -0,0 +1,318 @@ +getSession($name)); + } + + /** + * Asserts that the element with the given CSS selector is present. + * + * @param string $css_selector + * The CSS selector identifying the element to check. + * @param string $message + * Optional message to show alongside the assertion. + */ + protected function assertElementPresent($css_selector, $message = '') { + $this->assertNotEmpty($this->getSession()->getDriver()->find($this->cssSelectToXpath($css_selector)), $message); + } + + /** + * Asserts that the element with the given CSS selector is not present. + * + * @param string $css_selector + * The CSS selector identifying the element to check. + * @param string $message + * Optional message to show alongside the assertion. + */ + protected function assertElementNotPresent($css_selector, $message = '') { + $this->assertEmpty($this->getSession()->getDriver()->find($this->cssSelectToXpath($css_selector)), $message); + } + + /** + * Passes if the page (with HTML stripped) contains the text. + * + * Note that stripping HTML tags also removes their attributes, such as + * the values of text fields. + * + * @param string $text + * Plain text to look for. + */ + protected function assertText($text) { + $content_type = $this->getSession()->getResponseHeader('Content-type'); + // In case of a Non-HTML response (example: XML) check the original + // response. + if (strpos($content_type, 'html') === FALSE) { + $this->assertSession()->responseContains($text); + } + else { + $this->assertSession()->pageTextContains($text); + } + } + + /** + * Passes if the page (with HTML stripped) does not contains the text. + * + * Note that stripping HTML tags also removes their attributes, such as + * the values of text fields. + * + * @param string $text + * Plain text to look for. + */ + protected function assertNoText($text) { + $content_type = $this->getSession()->getResponseHeader('Content-type'); + // In case of a Non-HTML response (example: XML) check the original + // response. + if (strpos($content_type, 'html') === FALSE) { + $this->assertSession()->responseNotContains($text); + } + else { + $this->assertSession()->pageTextNotContains($text); + } + } + + /** + * Asserts the page responds with the specified response code. + * + * @param int $code + * Response code. For example 200 is a successful page request. For a list + * of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html. + */ + protected function assertResponse($code) { + $this->assertSession()->statusCodeEquals($code); + } + + /** + * Pass if the page title is the given string. + * + * @param string $expected_title + * The string the page title should be. + */ + protected function assertTitle($expected_title) { + $title_element = $this->getSession()->getPage()->find('css', 'title'); + if (!$title_element) { + $this->fail('No title element found on the page.'); + return; + } + $actual_title = $title_element->getText(); + $this->assertSame($expected_title, $actual_title); + } + + /** + * Passes if a link with the specified label is found. + * + * An optional link index may be passed. + * + * @param string|\Drupal\Component\Render\MarkupInterface $label + * Text between the anchor tags. + * @param int $index + * Link position counting from zero. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use strtr() to embed variables in the message text, not + * t(). If left blank, a default message will be displayed. + */ + protected function assertLink($label, $index = 0, $message = '') { + // Cast MarkupInterface objects to string. + $label = (string) $label; + $message = ($message ? $message : strtr('Link with label %label found.', ['%label' => $label])); + $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); + if (empty($links[$index])) { + $this->fail($message); + } + $this->assertNotNull($links[$index], $message); + } + + /** + * Passes if the raw text IS NOT found escaped on the loaded page. + * + * Raw text refers to the raw HTML that the page generated. + * + * @param string $raw + * Raw (HTML) string to look for. + */ + protected function assertNoEscaped($raw) { + $this->assertSession()->pageTextNotContains(Html::escape($raw)); + } + + /** + * Passes if the raw text IS found on the loaded page, fail otherwise. + * + * Raw text refers to the raw HTML that the page generated. + * + * @param string $raw + * Raw (HTML) string to look for. + */ + protected function assertRaw($raw) { + $this->assertSession()->responseContains($raw); + } + + /** + * Asserts that a field exists with the given name and value. + * + * @param string $name + * Name of field to assert. + * @param string $value + * (optional) Value of the field to assert. You may pass in NULL (default) + * to skip checking the actual value, while still checking that the field + * exists. + */ + protected function assertFieldByName($name, $value = NULL) { + $this->assertSession()->fieldExists($name); + if ($value !== NULL) { + $this->assertSession()->fieldValueEquals($name, $value); + } + } + + /** + * Check to see if two values are equal. + * + * Compatibility function for ::assertEquals(). + * + * @param $first + * The first value to check. + * @param $second + * The second value to check. + * @param $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + */ + protected function assertEqual($first, $second, $message = '') { + // Cast objects implementing MarkupInterface to string instead of + // relying on PHP casting them to string depending on what they are being + // comparing with. + $first = $this->castSafeStrings($first); + $second = $this->castSafeStrings($second); + $this->assertEquals($second, $first, $message); + } + + /** + * Fire an assertion that is always positive. + * + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + */ + protected function pass($message = '') { + return $this->assertTrue(TRUE, $message); + } + + /** + * Asserts that a field exists with the given ID and value. + * + * @param string $id + * ID of field to assert. + * @param string|\Drupal\Component\Render\MarkupInterface $value + * (optional) Value for the field to assert. You may pass in NULL to skip + * checking the value, while still checking that the field exists. + * However, the default value ('') asserts that the field value is an empty + * string. + */ + protected function assertFieldById($id, $value = NULL) { + // Cast MarkupInterface objects to string. + if (isset($value)) { + $value = (string) $value; + $this->assertSession()->fieldValueEquals($id, $value); + } + else { + $this->assertSession()->fieldExists($id); + } + } + + /** + * Asserts that a select option in the current page exists. + * + * @param string $id + * ID of select field to assert. + * @param string $option + * Option to assert. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + */ + protected function assertOption($id, $option, $message = '') { + $options = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option)); + $this->assertTrue(isset($options[0]), $message ? $message : "Option $option for field $id exists."); + } + + /** + * Asserts that a select option in the current page does not exist. + * + * @param string $id + * ID of select field to assert. + * @param string $option + * Option to assert. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + */ + protected function assertNoOption($id, $option, $message = '') { + $selects = $this->xpath('//select[@id=:id]', array(':id' => $id)); + $options = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option)); + $this->assertTrue(isset($selects[0]) && !isset($options[0]), $message ? $message : "Option $option for field $id does not exist."); + } + + /** + * Passes if the internal browser's URL matches the given path. + * + * @param \Drupal\Core\Url|string $path + * The expected system path or URL. + * @param $options + * (optional) Any additional options to pass for $path to the url generator. + * @param $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + */ + protected function assertUrl($path, array $options = array(), $message = '') { + if ($path instanceof Url) { + $url_obj = $path; + } + elseif (UrlHelper::isExternal($path)) { + $url_obj = Url::fromUri($path, $options); + } + else { + $uri = $path === '' ? 'base:/' : 'base:/' . $path; + // This is needed for language prefixing. + $options['path_processing'] = TRUE; + $url_obj = Url::fromUri($uri, $options); + } + $url = $url_obj->setAbsolute()->toString(); + if (!$message) { + $message = "Expected $url matches current URL (" . $this->getSession()->getCurrentUrl() . ').'; + } + // Paths in query strings can be encoded or decoded with no functional + // difference, decode them for comparison purposes. + $actual_url = urldecode($this->getSession()->getCurrentUrl()); + $expected_url = urldecode($url); + $this->assertEquals($actual_url, $expected_url, $message); + } + +} diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index a8700b4..5cd634c 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -20,12 +20,16 @@ use Drupal\Core\Test\TestRunnerKernel; use Drupal\Core\Url; use Drupal\Core\Test\TestDatabase; +use Drupal\simpletest\AssertHelperTrait; +use Drupal\simpletest\BlockCreationTrait; +use Drupal\simpletest\ContentTypeCreationTrait; +use Drupal\simpletest\NodeCreationTrait; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; -use Drupal\user\UserInterface; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\HttpFoundation\Request; + /** * Provides a test case for functional Drupal tests. * @@ -36,8 +40,17 @@ * @ingroup testing */ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase { + use AssertHelperTrait; + use BlockCreationTrait; + use BrowserAssertTrait; use RandomGeneratorTrait; use SessionTestTrait; + use ContentTypeCreationTrait { + createContentType as drupalCreateContentType; + } + use NodeCreationTrait { + createNode as drupalCreateNode; + } /** * Class loader. @@ -274,6 +287,13 @@ protected $baseUrl; /** + * The original array of shutdown function callbacks. + * + * @var array + */ + protected $originalShutdownCallbacks = []; + + /** * Initializes Mink sessions. */ protected function initMink() { @@ -505,6 +525,10 @@ protected function tearDown() { if ($this->mink) { $this->mink->stopSessions(); } + + // Restore original shutdown callbacks. + $callbacks = &drupal_register_shutdown_function(); + $callbacks = $this->originalShutdownCallbacks; } /** @@ -521,19 +545,6 @@ public function getSession($name = NULL) { } /** - * Returns WebAssert object. - * - * @param string $name - * (optional) Name of the session. Defaults to the active session. - * - * @return \Drupal\Tests\WebAssert - * A new web-assert option for asserting the presence of elements with. - */ - public function assertSession($name = NULL) { - return new WebAssert($this->getSession($name)); - } - - /** * Prepare for a request to testing site. * * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is @@ -894,6 +905,90 @@ protected function submitForm(array $edit, $submit, $form_html_id = NULL) { } /** + * Executes a form submission. + * + * It will be done as usual POST request with Mink. + * + * @param \Drupal\Core\Url|string $path + * Location of the post form. Either a Drupal path or an absolute path or + * NULL to post to the current page. For multi-stage forms you can set the + * path to NULL and have it post to the last received page. Example: + * + * @code + * // First step in form. + * $edit = array(...); + * $this->drupalPostForm('some_url', $edit, t('Save')); + * + * // Second step in form. + * $edit = array(...); + * $this->drupalPostForm(NULL, $edit, t('Save')); + * @endcode + * @param array $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. + * + * When working with form tests, the keys for an $edit element should match + * the 'name' parameter of the HTML of the form. For example, the 'body' + * field for a node has the following HTML: + * @code + * + * @endcode + * When testing this field using an $edit parameter, the code becomes: + * @code + * $edit["body[0][value]"] = 'My test value'; + * @endcode + * + * A checkbox can be set to TRUE to be checked and should be set to FALSE to + * be unchecked. Multiple select fields can be tested using 'name[]' and + * setting each of the desired values in an array: + * @code + * $edit = array(); + * $edit['name[]'] = array('value1', 'value2'); + * @endcode + * @param string $submit + * Value of the submit button whose click is to be emulated. For example, + * t('Save'). The processing of the request depends on this value. For + * example, a form may have one button with the value t('Save') and another + * button with the value t('Delete'), and execute different code depending + * on which one is clicked. + * + * This function can also be called to emulate an Ajax submission. In this + * case, this value needs to be an array with the following keys: + * - path: A path to submit the form values to for Ajax-specific processing. + * - triggering_element: If the value for the 'path' key is a generic Ajax + * processing path, this needs to be set to the name of the element. If + * the name doesn't identify the element uniquely, then this should + * instead be an array with a single key/value pair, corresponding to the + * element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder + * uses this to find the #ajax information for the element, including + * which specific callback to use for processing the request. + * + * This can also be set to NULL in order to emulate an Internet Explorer + * submission of a form with a single text field, and pressing ENTER in that + * textfield: under these conditions, no button information is added to the + * POST data. + * @param array $options + * Options to be forwarded to the url generator. + */ + protected function drupalPostForm($path, array $edit, $submit, array $options = array()) { + if (is_object($submit)) { + // Cast MarkupInterface objects to string. + $submit = (string) $submit; + } + if (is_array($edit)) { + $edit = $this->castSafeStrings($edit); + } + + if (isset($path)) { + $this->drupalGet($path, $options); + } + + $this->submitForm($edit, $submit); + } + + /** * Helper function to get the options of select field. * * @param \Behat\Mink\Element\NodeElement|string $select @@ -951,6 +1046,10 @@ public function installDrupal() { 'value' => $this->publicFilesDirectory, 'required' => TRUE, ); + $settings['settings']['file_private_path'] = (object) [ + 'value' => $this->privateFilesDirectory, + 'required' => TRUE, + ]; $this->writeSettings($settings); // Allow for test-specific overrides. $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/settings.testing.php'; @@ -1000,13 +1099,13 @@ public function installDrupal() { $config = $container->get('config.factory'); // Manually create and configure private and temporary files directories. - // While these could be preset/enforced in settings.php like the public - // files directory above, some tests expect them to be configurable in the - // UI. If declared in settings.php, they would no longer be configurable. file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY); file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY); + // While the temporary files path could be preset/enforced in settings.php + // like the public files directory above, some tests expect it to be + // configurable in the UI. If declared in settings.php, it would no longer + // be configurable. $config->getEditable('system.file') - ->set('path.private', $this->privateFilesDirectory) ->set('path.temporary', $this->tempFilesDirectory) ->save(); @@ -1241,6 +1340,14 @@ protected function prepareEnvironment() { )); drupal_set_time_limit($this->timeLimit); + + // Save and clean the shutdown callbacks array because it is static cached + // and will be changed by the test run. Otherwise it will contain callbacks + // from both environments and the testing environment will try to call the + // handlers defined by the original one. + $callbacks = &drupal_register_shutdown_function(); + $this->originalShutdownCallbacks = $callbacks; + $callbacks = []; } /** @@ -1402,13 +1509,13 @@ protected function refreshVariables() { /** * Returns whether a given user account is logged in. * - * @param \Drupal\user\UserInterface $account + * @param \Drupal\Core\Session\AccountInterface $account * The user account object to check. * * @return bool * Return TRUE if the user is logged in, FALSE otherwise. */ - protected function drupalUserIsLoggedIn(UserInterface $account) { + protected function drupalUserIsLoggedIn(AccountInterface $account) { $logged_in = FALSE; if (isset($account->sessionId)) { @@ -1420,30 +1527,6 @@ protected function drupalUserIsLoggedIn(UserInterface $account) { } /** - * Asserts that the element with the given CSS selector is present. - * - * @param string $css_selector - * The CSS selector identifying the element to check. - * @param string $message - * Optional message to show alongside the assertion. - */ - protected function assertElementPresent($css_selector, $message = '') { - $this->assertNotEmpty($this->getSession()->getDriver()->find($this->cssSelectToXpath($css_selector)), $message); - } - - /** - * Asserts that the element with the given CSS selector is not present. - * - * @param string $css_selector - * The CSS selector identifying the element to check. - * @param string $message - * Optional message to show alongside the assertion. - */ - protected function assertElementNotPresent($css_selector, $message = '') { - $this->assertEmpty($this->getSession()->getDriver()->find($this->cssSelectToXpath($css_selector)), $message); - } - - /** * Clicks the element with the given CSS selector. * * @param string $css_selector @@ -1529,4 +1612,154 @@ protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descenda return (new CssSelectorConverter($html))->toXPath($selector, $prefix); } + /** + * Searches elements using a CSS selector in the raw content. + * + * The search is relative to the root element (HTML tag normally) of the page. + * + * @param string $selector + * CSS selector to use in the search. + * + * @return \Behat\Mink\Element\NodeElement[] + * The list of elements on the page that match the selector. + */ + protected function cssSelect($selector) { + return $this->getSession()->getPage()->findAll('css', $selector); + } + + /** + * Follows a link by complete name. + * + * Will click the first link found with this link text. + * + * If the link is discovered and clicked, the test passes. Fail otherwise. + * + * @param string|\Drupal\Component\Render\MarkupInterface $label + * Text between the anchor tags. + */ + protected function clickLink($label) { + $label = (string) $label; + $this->getSession()->getPage()->clickLink($label); + } + + /** + * Retrieves the plain-text content from the current page. + */ + protected function getTextContent() { + return $this->getSession()->getPage()->getContent(); + } + + /** + * Performs an xpath search on the contents of the internal browser. + * + * The search is relative to the root element (HTML tag normally) of the page. + * + * @param string $xpath + * The xpath string to use in the search. + * @param array $arguments + * An array of arguments with keys in the form ':name' matching the + * placeholders in the query. The values may be either strings or numeric + * values. + * + * @return \Behat\Mink\Element\NodeElement[] + * The list of elements matching the xpath expression. + */ + protected function xpath($xpath, array $arguments = []) { + $xpath = $this->buildXPathQuery($xpath, $arguments); + return $this->getSession()->getPage()->findAll('xpath', $xpath); + } + + /** + * Builds an XPath query. + * + * Builds an XPath query by replacing placeholders in the query by the value + * of the arguments. + * + * XPath 1.0 (the version supported by libxml2, the underlying XML library + * used by PHP) doesn't support any form of quotation. This function + * simplifies the building of XPath expression. + * + * @param string $xpath + * An XPath query, possibly with placeholders in the form ':name'. + * @param array $args + * An array of arguments with keys in the form ':name' matching the + * placeholders in the query. The values may be either strings or numeric + * values. + * + * @return string + * An XPath query with arguments replaced. + */ + protected function buildXPathQuery($xpath, array $args = array()) { + // Replace placeholders. + foreach ($args as $placeholder => $value) { + // Cast MarkupInterface objects to string. + if (is_object($value)) { + $value = (string) $value; + } + // XPath 1.0 doesn't support a way to escape single or double quotes in a + // string literal. We split double quotes out of the string, and encode + // them separately. + if (is_string($value)) { + // Explode the text at the quote characters. + $parts = explode('"', $value); + + // Quote the parts. + foreach ($parts as &$part) { + $part = '"' . $part . '"'; + } + + // Return the string. + $value = count($parts) > 1 ? 'concat(' . implode(', \'"\', ', $parts) . ')' : $parts[0]; + } + + // Use preg_replace_callback() instead of preg_replace() to prevent the + // regular expression engine from trying to substitute backreferences. + $replacement = function ($matches) use ($value) { + return $value; + }; + $xpath = preg_replace_callback('/' . preg_quote($placeholder) . '\b/', $replacement, $xpath); + } + return $xpath; + } + + /** + * Configuration accessor for tests. Returns non-overridden configuration. + * + * @param string $name + * Configuration name. + * + * @return \Drupal\Core\Config\Config + * The configuration object with original configuration data. + */ + protected function config($name) { + return \Drupal::configFactory()->getEditable($name); + } + + /** + * Gets the value of an HTTP response header. + * + * If multiple requests were required to retrieve the page, only the headers + * from the last request will be checked by default. + * + * @param string $name + * The name of the header to retrieve. Names are case-insensitive (see RFC + * 2616 section 4.2). + * + * @return string|null + * The HTTP header value or NULL if not found. + */ + protected function drupalGetHeader($name) { + return $this->getSession()->getResponseHeader($name); + } + + /** + * Get the current URL from the test browser. + * + * @return string + * The current URL. + */ + protected function getUrl() { + return $this->getSession()->getCurrentUrl(); + } + }