diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index 5670b060b7e17b741bc4b0a4c8569e2d13e65888..9e8307a5571790dbf70b89edebfb4ed9f37cd8af 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -4,7 +4,6 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Development\ConfigSchemaChecker; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; @@ -13,6 +12,7 @@ use Drupal\Core\Session\UserSession; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\StreamWrapperInterface; +use Drupal\Tests\SessionTestTrait; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Yaml\Yaml as SymfonyYaml; @@ -22,6 +22,9 @@ */ trait FunctionalTestSetupTrait { + use SessionTestTrait; + use RefreshVariablesTrait; + /** * The "#1" admin user. * @@ -219,32 +222,6 @@ protected function resetAll() { $this->refreshVariables(); } - /** - * Refreshes in-memory configuration and state information. - * - * Useful after a page request is made that changes configuration or state in - * a different thread. - * - * In other words calling a settings page with $this->drupalPostForm() with a - * changed value would update configuration to reflect that change, but in the - * thread that made the call (thread running the test) the changed values - * would not be picked up. - * - * This method clears the cache and loads a fresh copy. - */ - protected function refreshVariables() { - // Clear the tag cache. - \Drupal::service('cache_tags.invalidator')->resetChecksums(); - foreach (Cache::getBins() as $backend) { - if (is_callable([$backend, 'reset'])) { - $backend->reset(); - } - } - - $this->container->get('config.factory')->reset(); - $this->container->get('state')->resetCache(); - } - /** * Creates a mock request and sets it on the generator. * diff --git a/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..974d96ed66b8fca806ccb1eaa9ac81360c5c2fee --- /dev/null +++ b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php @@ -0,0 +1,38 @@ +drupalPostForm() with a + * changed value would update configuration to reflect that change, but in the + * thread that made the call (thread running the test) the changed values + * would not be picked up. + * + * This method clears the cache and loads a fresh copy. + */ + protected function refreshVariables() { + // Clear the tag cache. + \Drupal::service('cache_tags.invalidator')->resetChecksums(); + foreach (Cache::getBins() as $backend) { + if (is_callable([$backend, 'reset'])) { + $backend->reset(); + } + } + + \Drupal::service('config.factory')->reset(); + \Drupal::service('state')->resetCache(); + } + +} diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index 314e02aaaf142ddde0644d9883b641171ee7f05d..12a061fd00e3f6772497856a8fb7d12b602edb11 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -15,7 +15,6 @@ use Drupal\Tests\AssertHelperTrait as BaseAssertHelperTrait; use Drupal\Tests\ConfigTestTrait; use Drupal\Tests\RandomGeneratorTrait; -use Drupal\Tests\SessionTestTrait; use Drupal\Tests\Traits\Core\GeneratePermutationsTrait; /** @@ -27,7 +26,6 @@ abstract class TestBase { use BaseAssertHelperTrait; use TestSetupTrait; - use SessionTestTrait; use RandomGeneratorTrait; use GeneratePermutationsTrait; // For backwards compatibility switch the visbility of the methods to public. diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php index 0bc057e7e7aec6a2556efaf0bb5db2325756d775..50167bff3b2a25d887fbcae06dadc3c2f2ec969d 100644 --- a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php +++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php @@ -8,7 +8,6 @@ use Drupal\Core\Test\TestSetupTrait; use Drupal\TestSite\TestSetupInterface; use Drupal\Tests\RandomGeneratorTrait; -use Drupal\Tests\SessionTestTrait; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,7 +25,6 @@ class TestSiteInstallCommand extends Command { installParameters as protected installParametersTrait; } use RandomGeneratorTrait; - use SessionTestTrait; use TestSetupTrait { changeDatabasePrefix as protected changeDatabasePrefixTrait; } diff --git a/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php b/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..f06a5442f45ef77f0fce70c18a475837c2900559 --- /dev/null +++ b/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php @@ -0,0 +1,222 @@ +Headers:
' . Html::escape(var_export($flattened_headers, TRUE)) . '
'; + } + + /** + * Returns headers in HTML output format. + * + * @return string + * HTML output headers. + */ + protected function getHtmlOutputHeaders() { + return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders()); + } + + /** + * Logs a HTML output message in a text file. + * + * The link to the HTML output message will be printed by the results printer. + * + * @param string|null $message + * (optional) The HTML output message to be stored. If not supplied the + * current page content is used. + * + * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult() + */ + protected function htmlOutput($message = NULL) { + if (!$this->htmlOutputEnabled) { + return; + } + $message = $message ?: $this->getSession()->getPage()->getContent(); + $message = '
ID #' . $this->htmlOutputCounter . ' (Previous | Next)
' . $message; + $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html'; + file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message); + file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++); + // Do not use file_create_url() as the module_handler service might not be + // available. + $uri = $GLOBALS['base_url'] . '/sites/simpletest/browser_output/' . $html_output_filename; + file_put_contents($this->htmlOutputFile, $uri . "\n", FILE_APPEND); + } + + /** + * Creates the directory to store browser output. + * + * Creates the directory to store browser output in if a file to write + * URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter. + */ + protected function initBrowserOutputFile() { + $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE'); + $this->htmlOutputEnabled = is_file($browser_output_file); + if ($this->htmlOutputEnabled) { + $this->htmlOutputFile = $browser_output_file; + $this->htmlOutputClassName = str_replace("\\", "_", get_called_class()); + $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output'; + // Do not use the file_system service so this method can be called before + // it is available. + if (!is_dir($this->htmlOutputDirectory)) { + mkdir($this->htmlOutputDirectory, 0775, TRUE); + } + if (!file_exists($this->htmlOutputDirectory . '/.htaccess')) { + file_put_contents($this->htmlOutputDirectory . '/.htaccess', "\nExpiresActive Off\n\n"); + } + $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter'; + $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory); + if (is_file($this->htmlOutputCounterStorage)) { + $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1; + } + } + } + + /** + * Provides a Guzzle middleware handler to log every response received. + * + * @return callable + * The callable handler that will do the logging. + */ + protected function getResponseLogHandler() { + return function (callable $handler) { + return function (RequestInterface $request, array $options) use ($handler) { + return $handler($request, $options) + ->then(function (ResponseInterface $response) use ($request) { + if ($this->htmlOutputEnabled) { + + $caller = $this->getTestMethodCaller(); + $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line']; + $html_output .= '
' . $request->getMethod() . ' request to: ' . $request->getUri(); + + // On redirect responses (status code starting with '3') we need + // to remove the meta tag that would do a browser refresh. We + // don't want to redirect developers away when they look at the + // debug output file in their browser. + $body = $response->getBody(); + $status_code = (string) $response->getStatusCode(); + if ($status_code[0] === '3') { + $body = preg_replace('##', '', $body, 1); + } + $html_output .= '
' . $body; + $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders()); + + $this->htmlOutput($html_output); + } + return $response; + }); + }; + }; + } + + /** + * Retrieves the current calling line in the class under test. + * + * @return array + * An associative array with keys 'file', 'line' and 'function'. + */ + protected function getTestMethodCaller() { + $backtrace = debug_backtrace(); + // Find the test class that has the test method. + while ($caller = Error::getLastCaller($backtrace)) { + if (isset($caller['class']) && $caller['class'] === get_class($this)) { + break; + } + // If the test method is implemented by a test class's parent then the + // class name of $this will not be part of the backtrace. + // In that case we process the backtrace until the caller is not a + // subclass of $this and return the previous caller. + if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) { + // Return the last caller since that has to be the test class. + $caller = $last_caller; + break; + } + // Otherwise we have not reached our test class yet: save the last caller + // and remove an element from to backtrace to process the next call. + $last_caller = $caller; + array_shift($backtrace); + } + + return $caller; + } + +} diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 2119b0153953612c609df84e11185d948397eae0..74df1e0a3abc615b818efde78a46a3c001c6222a 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -7,16 +7,10 @@ use Behat\Mink\Mink; use Behat\Mink\Selector\SelectorsHandler; use Behat\Mink\Session; -use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Serialization\Json; -use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Database\Database; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\Test\FunctionalTestSetupTrait; use Drupal\Core\Test\TestSetupTrait; -use Drupal\Core\Url; use Drupal\Core\Utility\Error; use Drupal\FunctionalTests\AssertLegacyTrait; use Drupal\Tests\block\Traits\BlockCreationTrait; @@ -44,14 +38,15 @@ abstract class BrowserTestBase extends TestCase { use FunctionalTestSetupTrait; + use UiHelperTrait { + FunctionalTestSetupTrait::refreshVariables insteadof UiHelperTrait; + } use TestSetupTrait; - use AssertHelperTrait; use BlockCreationTrait { placeBlock as drupalPlaceBlock; } use AssertLegacyTrait; use RandomGeneratorTrait; - use SessionTestTrait; use NodeCreationTrait { getNodeByTitle as drupalGetNodeByTitle; createNode as drupalCreateNode; @@ -118,13 +113,6 @@ abstract class BrowserTestBase extends TestCase { */ protected $profile = 'testing'; - /** - * The current user logged in using the Mink controlled browser. - * - * @var \Drupal\user\UserInterface - */ - protected $loggedInUser = FALSE; - /** * An array of custom translations suitable for drupal_rewrite_settings(). * @@ -181,59 +169,6 @@ abstract class BrowserTestBase extends TestCase { */ protected $preserveGlobalState = FALSE; - /** - * Class name for HTML output logging. - * - * @var string - */ - protected $htmlOutputClassName; - - /** - * Directory name for HTML output logging. - * - * @var string - */ - protected $htmlOutputDirectory; - - /** - * Counter storage for HTML output logging. - * - * @var string - */ - protected $htmlOutputCounterStorage; - - /** - * Counter for HTML output logging. - * - * @var int - */ - protected $htmlOutputCounter = 1; - - /** - * HTML output output enabled. - * - * @var bool - */ - protected $htmlOutputEnabled = FALSE; - - /** - * The file name to write the list of URLs to. - * - * This file is read by the PHPUnit result printer. - * - * @var string - * - * @see \Drupal\Tests\Listeners\HtmlOutputPrinter - */ - protected $htmlOutputFile; - - /** - * HTML output test ID. - * - * @var int - */ - protected $htmlOutputTestId; - /** * The base URL. * @@ -248,20 +183,6 @@ abstract class BrowserTestBase extends TestCase { */ protected $originalShutdownCallbacks = []; - /** - * The number of meta refresh redirects to follow, or NULL if unlimited. - * - * @var null|int - */ - protected $maximumMetaRefreshCount = NULL; - - /** - * The number of meta refresh redirects followed during ::drupalGet(). - * - * @var int - */ - protected $metaRefreshCount = 0; - /** * The app root. * @@ -380,35 +301,6 @@ protected function getDefaultDriverInstance() { return $driver; } - /** - * Creates the directory to store browser output. - * - * Creates the directory to store browser output in if a file to write - * URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter. - */ - protected function initBrowserOutputFile() { - $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE'); - $this->htmlOutputEnabled = is_file($browser_output_file); - if ($this->htmlOutputEnabled) { - $this->htmlOutputFile = $browser_output_file; - $this->htmlOutputClassName = str_replace("\\", "_", get_called_class()); - $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output'; - // Do not use the file_system service so this method can be called before - // it is available. - if (!is_dir($this->htmlOutputDirectory)) { - mkdir($this->htmlOutputDirectory, 0775, TRUE); - } - if (!file_exists($this->htmlOutputDirectory . '/.htaccess')) { - file_put_contents($this->htmlOutputDirectory . '/.htaccess', "\nExpiresActive Off\n\n"); - } - $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter'; - $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory); - if (is_file($this->htmlOutputCounterStorage)) { - $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1; - } - } - } - /** * Get the Mink driver args from an environment variable, if it is set. Can * be overridden in a derived class so it is possible to use a different @@ -624,411 +516,6 @@ protected function getHttpClient() { throw new \RuntimeException('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().'); } - /** - * 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) { - $this->addToAssertionCount(1); - return new WebAssert($this->getSession($name), $this->baseUrl); - } - - /** - * Prepare for a request to testing site. - * - * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is - * checked by drupal_valid_test_ua(). - * - * @see drupal_valid_test_ua() - */ - protected function prepareRequest() { - $session = $this->getSession(); - $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); - } - - /** - * Builds an a absolute URL from a system path or a URL object. - * - * @param string|\Drupal\Core\Url $path - * A system path or a URL. - * @param array $options - * Options to be passed to Url::fromUri(). - * - * @return string - * An absolute URL stsring. - */ - protected function buildUrl($path, array $options = []) { - if ($path instanceof Url) { - $url_options = $path->getOptions(); - $options = $url_options + $options; - $path->setOptions($options); - return $path->setAbsolute()->toString(); - } - // The URL generator service is not necessarily available yet; e.g., in - // interactive installer tests. - elseif ($this->container->has('url_generator')) { - $force_internal = isset($options['external']) && $options['external'] == FALSE; - if (!$force_internal && UrlHelper::isExternal($path)) { - return Url::fromUri($path, $options)->toString(); - } - else { - $uri = $path === '' ? 'base:/' : 'base:/' . $path; - // Path processing is needed for language prefixing. Skip it when a - // path that may look like an external URL is being used as internal. - $options['path_processing'] = !$force_internal; - return Url::fromUri($uri, $options) - ->setAbsolute() - ->toString(); - } - } - else { - return $this->getAbsoluteUrl($path); - } - } - - /** - * Retrieves a Drupal path or an absolute path. - * - * @param string|\Drupal\Core\Url $path - * Drupal path or URL to load into Mink controlled browser. - * @param array $options - * (optional) Options to be forwarded to the url generator. - * @param string[] $headers - * An array containing additional HTTP request headers, the array keys are - * the header names and the array values the header values. This is useful - * to set for example the "Accept-Language" header for requesting the page - * in a different language. Note that not all headers are supported, for - * example the "Accept" header is always overridden by the browser. For - * testing REST APIs it is recommended to obtain a separate HTTP client - * using getHttpClient() and performing requests that way. - * - * @return string - * The retrieved HTML string, also available as $this->getRawContent() - * - * @see \Drupal\Tests\BrowserTestBase::getHttpClient() - */ - protected function drupalGet($path, array $options = [], array $headers = []) { - $options['absolute'] = TRUE; - $url = $this->buildUrl($path, $options); - - $session = $this->getSession(); - - $this->prepareRequest(); - foreach ($headers as $header_name => $header_value) { - $session->setRequestHeader($header_name, $header_value); - } - - $session->visit($url); - $out = $session->getPage()->getContent(); - - // Ensure that any changes to variables in the other thread are picked up. - $this->refreshVariables(); - - // Replace original page output with new output from redirected page(s). - if ($new = $this->checkForMetaRefresh()) { - $out = $new; - // We are finished with all meta refresh redirects, so reset the counter. - $this->metaRefreshCount = 0; - } - - // Log only for JavascriptTestBase tests because for Goutte we log with - // ::getResponseLogHandler. - if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { - $html_output = 'GET request to: ' . $url . - '
Ending URL: ' . $this->getSession()->getCurrentUrl(); - $html_output .= '
' . $out; - $html_output .= $this->getHtmlOutputHeaders(); - $this->htmlOutput($html_output); - } - - return $out; - } - - /** - * Takes a path and returns an absolute path. - * - * @param string $path - * A path from the Mink controlled browser content. - * - * @return string - * The $path with $base_url prepended, if necessary. - */ - protected function getAbsoluteUrl($path) { - global $base_url, $base_path; - - $parts = parse_url($path); - if (empty($parts['host'])) { - // Ensure that we have a string (and no xpath object). - $path = (string) $path; - // Strip $base_path, if existent. - $length = strlen($base_path); - if (substr($path, 0, $length) === $base_path) { - $path = substr($path, $length); - } - // Ensure that we have an absolute path. - if (empty($path) || $path[0] !== '/') { - $path = '/' . $path; - } - // Finally, prepend the $base_url. - $path = $base_url . $path; - } - return $path; - } - - /** - * Logs in a user using the Mink controlled browser. - * - * If a user is already logged in, then the current user is logged out before - * logging in the specified user. - * - * Please note that neither the current user nor the passed-in user object is - * populated with data of the logged in user. If you need full access to the - * user object after logging in, it must be updated manually. If you also need - * access to the plain-text password of the user (set by drupalCreateUser()), - * e.g. to log in the same user again, then it must be re-assigned manually. - * For example: - * @code - * // Create a user. - * $account = $this->drupalCreateUser(array()); - * $this->drupalLogin($account); - * // Load real user object. - * $pass_raw = $account->passRaw; - * $account = User::load($account->id()); - * $account->passRaw = $pass_raw; - * @endcode - * - * @param \Drupal\Core\Session\AccountInterface $account - * User object representing the user to log in. - * - * @see drupalCreateUser() - */ - protected function drupalLogin(AccountInterface $account) { - if ($this->loggedInUser) { - $this->drupalLogout(); - } - - $this->drupalGet('user/login'); - $this->submitForm([ - 'name' => $account->getUsername(), - 'pass' => $account->passRaw, - ], t('Log in')); - - // @see BrowserTestBase::drupalUserIsLoggedIn() - $account->sessionId = $this->getSession()->getCookie($this->getSessionName()); - $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()])); - - $this->loggedInUser = $account; - $this->container->get('current_user')->setAccount($account); - } - - /** - * Logs a user out of the Mink controlled browser and confirms. - * - * Confirms logout by checking the login page. - */ - protected function drupalLogout() { - // Make a request to the logout page, and redirect to the user page, the - // idea being if you were properly logged out you should be seeing a login - // screen. - $assert_session = $this->assertSession(); - $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); - $assert_session->fieldExists('name'); - $assert_session->fieldExists('pass'); - - // @see BrowserTestBase::drupalUserIsLoggedIn() - unset($this->loggedInUser->sessionId); - $this->loggedInUser = FALSE; - $this->container->get('current_user')->setAccount(new AnonymousUserSession()); - } - - /** - * Fills and submits a form. - * - * @param array $edit - * Field data in an associative array. Changes the current input fields - * (where possible) to the values indicated. - * - * A checkbox can be set to TRUE to be checked and should be set to FALSE to - * be unchecked. - * @param string $submit - * Value of the submit button whose click is to be emulated. For example, - * 'Save'. The processing of the request depends on this value. For example, - * a form may have one button with the value 'Save' and another button with - * the value 'Delete', and execute different code depending on which one is - * clicked. - * @param string $form_html_id - * (optional) HTML ID of the form to be submitted. On some pages - * there are many identical forms, so just using the value of the submit - * button is not enough. For example: 'trigger-node-presave-assign-form'. - * Note that this is not the Drupal $form_id, but rather the HTML ID of the - * form, which is typically the same thing but with hyphens replacing the - * underscores. - */ - protected function submitForm(array $edit, $submit, $form_html_id = NULL) { - $assert_session = $this->assertSession(); - - // Get the form. - if (isset($form_html_id)) { - $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']"); - $submit_button = $assert_session->buttonExists($submit, $form); - $action = $form->getAttribute('action'); - } - else { - $submit_button = $assert_session->buttonExists($submit); - $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); - $action = $form->getAttribute('action'); - } - - // Edit the form values. - foreach ($edit as $name => $value) { - $field = $assert_session->fieldExists($name, $form); - - // Provide support for the values '1' and '0' for checkboxes instead of - // TRUE and FALSE. - // @todo Get rid of supporting 1/0 by converting all tests cases using - // this to boolean values. - $field_type = $field->getAttribute('type'); - if ($field_type === 'checkbox') { - $value = (bool) $value; - } - - $field->setValue($value); - } - - // Submit form. - $this->prepareRequest(); - $submit_button->press(); - - // Ensure that any changes to variables in the other thread are picked up. - $this->refreshVariables(); - - // Check if there are any meta refresh redirects (like Batch API pages). - if ($this->checkForMetaRefresh()) { - // We are finished with all meta refresh redirects, so reset the counter. - $this->metaRefreshCount = 0; - } - - // Log only for JavascriptTestBase tests because for Goutte we log with - // ::getResponseLogHandler. - if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { - $out = $this->getSession()->getPage()->getContent(); - $html_output = 'POST request to: ' . $action . - '
Ending URL: ' . $this->getSession()->getCurrentUrl(); - $html_output .= '
' . $out; - $html_output .= $this->getHtmlOutputHeaders(); - $this->htmlOutput($html_output); - } - - } - - /** - * Executes a form submission. - * - * It will be done as usual submit form 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, 'Save'); - * - * // Second step in form. - * $edit = array(...); - * $this->drupalPostForm(NULL, $edit, '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 - * @todo change $edit to disallow NULL as a value for Drupal 9. - * https://www.drupal.org/node/2802401 - * @param string $submit - * Value of the submit button whose click is to be emulated. For example, - * 'Save'. The processing of the request depends on this value. For example, - * a form may have one button with the value 'Save' and another button with - * the value '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. - * @param string|null $form_html_id - * (optional) HTML ID of the form to be submitted. On some pages - * there are many identical forms, so just using the value of the submit - * button is not enough. For example: 'trigger-node-presave-assign-form'. - * Note that this is not the Drupal $form_id, but rather the HTML ID of the - * form, which is typically the same thing but with hyphens replacing the - * underscores. - * - * @return string - * (deprecated) The response content after submit form. It is necessary for - * backwards compatibility and will be removed before Drupal 9.0. You should - * just use the webAssert object for your assertions. - */ - protected function drupalPostForm($path, $edit, $submit, array $options = [], $form_html_id = NULL) { - if (is_object($submit)) { - // Cast MarkupInterface objects to string. - $submit = (string) $submit; - } - if ($edit === NULL) { - $edit = []; - } - if (is_array($edit)) { - $edit = $this->castSafeStrings($edit); - } - - if (isset($path)) { - $this->drupalGet($path, $options); - } - - $this->submitForm($edit, $submit, $form_html_id); - - return $this->getSession()->getPage()->getContent(); - } - /** * Helper function to get the options of select field. * @@ -1068,49 +555,6 @@ public function installDrupal() { $this->rebuildAll(); } - /** - * Returns whether a given user account is logged in. - * - * @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(AccountInterface $account) { - $logged_in = FALSE; - - if (isset($account->sessionId)) { - $session_handler = $this->container->get('session_handler.storage'); - $logged_in = (bool) $session_handler->read($account->sessionId); - } - - return $logged_in; - } - - /** - * Clicks the element with the given CSS selector. - * - * @param string $css_selector - * The CSS selector identifying the element to click. - */ - protected function click($css_selector) { - $starting_url = $this->getSession()->getCurrentUrl(); - $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector)); - // Log only for JavascriptTestBase tests because for Goutte we log with - // ::getResponseLogHandler. - if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { - $out = $this->getSession()->getPage()->getContent(); - $html_output = - 'Clicked element with CSS selector: ' . $css_selector . - '
Starting URL: ' . $starting_url . - '
Ending URL: ' . $this->getSession()->getCurrentUrl(); - $html_output .= '
' . $out; - $html_output .= $this->getHtmlOutputHeaders(); - $this->htmlOutput($html_output); - } - } - /** * Prevents serializing any properties. * @@ -1129,63 +573,6 @@ public function __sleep() { return []; } - /** - * Logs a HTML output message in a text file. - * - * The link to the HTML output message will be printed by the results printer. - * - * @param string|null $message - * (optional) The HTML output message to be stored. If not supplied the - * current page content is used. - * - * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult() - */ - protected function htmlOutput($message) { - if (!$this->htmlOutputEnabled) { - return; - } - $message = $message ?: $this->getSession()->getPage()->getContent(); - $message = '
ID #' . $this->htmlOutputCounter . ' (Previous | Next)
' . $message; - $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html'; - file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message); - file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++); - // Do not use file_create_url() as the module_handler service might not be - // available. - $uri = $GLOBALS['base_url'] . '/sites/simpletest/browser_output/' . $html_output_filename; - file_put_contents($this->htmlOutputFile, $uri . "\n", FILE_APPEND); - } - - /** - * Returns headers in HTML output format. - * - * @return string - * HTML output headers. - */ - protected function getHtmlOutputHeaders() { - return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders()); - } - - /** - * Formats HTTP headers as string for HTML output logging. - * - * @param array[] $headers - * Headers that should be formatted. - * - * @return string - * The formatted HTML string. - */ - protected function formatHtmlOutputHeaders(array $headers) { - $flattened_headers = array_map(function ($header) { - if (is_array($header)) { - return implode(';', array_map('trim', $header)); - } - else { - return $header; - } - }, $headers); - return '
Headers:
' . Html::escape(var_export($flattened_headers, TRUE)) . '
'; - } - /** * Translates a CSS expression to its XPath equivalent. * @@ -1205,48 +592,6 @@ 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. - * @param int $index - * (optional) The index number for cases where multiple links have the same - * text. Defaults to 0. - */ - protected function clickLink($label, $index = 0) { - $label = (string) $label; - $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); - $this->assertArrayHasKey($index, $links, 'The link ' . $label . ' was not found on the page.'); - $links[$index]->click(); - } - - /** - * Retrieves the plain-text content from the current page. - */ - protected function getTextContent() { - return $this->getSession()->getPage()->getText(); - } - /** * Performs an xpath search on the contents of the internal browser. * @@ -1310,16 +655,6 @@ protected function drupalGetHeader($name) { return $this->getSession()->getResponseHeader($name); } - /** - * Get the current URL from the browser. - * - * @return string - * The current URL. - */ - protected function getUrl() { - return $this->getSession()->getCurrentUrl(); - } - /** * Gets the JavaScript drupalSettings variable for the currently-loaded page. * @@ -1399,26 +734,4 @@ protected function translatePostValues(array $values) { return $edit; } - /** - * Checks for meta refresh tag and if found call drupalGet() recursively. - * - * This function looks for the http-equiv attribute to be set to "Refresh" and - * is case-insensitive. - * - * @return string|false - * Either the new page content or FALSE. - */ - protected function checkForMetaRefresh() { - $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); - if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { - // Parse the content attribute of the meta tag for the format: - // "[delay]: URL=[page_to_redirect_to]". - if (preg_match('/\d+;\s*URL=(?.*)/i', $refresh[0]->getAttribute('content'), $match)) { - $this->metaRefreshCount++; - return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); - } - } - return FALSE; - } - } diff --git a/core/tests/Drupal/Tests/UiHelperTrait.php b/core/tests/Drupal/Tests/UiHelperTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..cc5a324adf07e4dec292955245ee064777a406b7 --- /dev/null +++ b/core/tests/Drupal/Tests/UiHelperTrait.php @@ -0,0 +1,566 @@ +assertSession(); + + // Get the form. + if (isset($form_html_id)) { + $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']"); + $submit_button = $assert_session->buttonExists($submit, $form); + $action = $form->getAttribute('action'); + } + else { + $submit_button = $assert_session->buttonExists($submit); + $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button); + $action = $form->getAttribute('action'); + } + + // Edit the form values. + foreach ($edit as $name => $value) { + $field = $assert_session->fieldExists($name, $form); + + // Provide support for the values '1' and '0' for checkboxes instead of + // TRUE and FALSE. + // @todo Get rid of supporting 1/0 by converting all tests cases using + // this to boolean values. + $field_type = $field->getAttribute('type'); + if ($field_type === 'checkbox') { + $value = (bool) $value; + } + + $field->setValue($value); + } + + // Submit form. + $this->prepareRequest(); + $submit_button->press(); + + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + + // Check if there are any meta refresh redirects (like Batch API pages). + if ($this->checkForMetaRefresh()) { + // We are finished with all meta refresh redirects, so reset the counter. + $this->metaRefreshCount = 0; + } + + // Log only for JavascriptTestBase tests because for Goutte we log with + // ::getResponseLogHandler. + if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { + $out = $this->getSession()->getPage()->getContent(); + $html_output = 'POST request to: ' . $action . + '
Ending URL: ' . $this->getSession()->getCurrentUrl(); + $html_output .= '
' . $out; + $html_output .= $this->getHtmlOutputHeaders(); + $this->htmlOutput($html_output); + } + + } + + /** + * Executes a form submission. + * + * It will be done as usual submit form 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, 'Save'); + * + * // Second step in form. + * $edit = array(...); + * $this->drupalPostForm(NULL, $edit, '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 + * @todo change $edit to disallow NULL as a value for Drupal 9. + * https://www.drupal.org/node/2802401 + * @param string $submit + * Value of the submit button whose click is to be emulated. For example, + * 'Save'. The processing of the request depends on this value. For example, + * a form may have one button with the value 'Save' and another button with + * the value '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. + * @param string|null $form_html_id + * (optional) HTML ID of the form to be submitted. On some pages + * there are many identical forms, so just using the value of the submit + * button is not enough. For example: 'trigger-node-presave-assign-form'. + * Note that this is not the Drupal $form_id, but rather the HTML ID of the + * form, which is typically the same thing but with hyphens replacing the + * underscores. + * + * @return string + * (deprecated) The response content after submit form. It is necessary for + * backwards compatibility and will be removed before Drupal 9.0. You should + * just use the webAssert object for your assertions. + */ + protected function drupalPostForm($path, $edit, $submit, array $options = [], $form_html_id = NULL) { + if (is_object($submit)) { + // Cast MarkupInterface objects to string. + $submit = (string) $submit; + } + if ($edit === NULL) { + $edit = []; + } + if (is_array($edit)) { + $edit = $this->castSafeStrings($edit); + } + + if (isset($path)) { + $this->drupalGet($path, $options); + } + + $this->submitForm($edit, $submit, $form_html_id); + + return $this->getSession()->getPage()->getContent(); + } + + /** + * Logs in a user using the Mink controlled browser. + * + * If a user is already logged in, then the current user is logged out before + * logging in the specified user. + * + * Please note that neither the current user nor the passed-in user object is + * populated with data of the logged in user. If you need full access to the + * user object after logging in, it must be updated manually. If you also need + * access to the plain-text password of the user (set by drupalCreateUser()), + * e.g. to log in the same user again, then it must be re-assigned manually. + * For example: + * @code + * // Create a user. + * $account = $this->drupalCreateUser(array()); + * $this->drupalLogin($account); + * // Load real user object. + * $pass_raw = $account->passRaw; + * $account = User::load($account->id()); + * $account->passRaw = $pass_raw; + * @endcode + * + * @param \Drupal\Core\Session\AccountInterface $account + * User object representing the user to log in. + * + * @see drupalCreateUser() + */ + protected function drupalLogin(AccountInterface $account) { + if ($this->loggedInUser) { + $this->drupalLogout(); + } + + $this->drupalGet('user/login'); + $this->submitForm([ + 'name' => $account->getUsername(), + 'pass' => $account->passRaw, + ], t('Log in')); + + // @see ::drupalUserIsLoggedIn() + $account->sessionId = $this->getSession()->getCookie(\Drupal::service('session_configuration')->getOptions(\Drupal::request())['name']); + $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()])); + + $this->loggedInUser = $account; + $this->container->get('current_user')->setAccount($account); + } + + /** + * Logs a user out of the Mink controlled browser and confirms. + * + * Confirms logout by checking the login page. + */ + protected function drupalLogout() { + // Make a request to the logout page, and redirect to the user page, the + // idea being if you were properly logged out you should be seeing a login + // screen. + $assert_session = $this->assertSession(); + $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); + $assert_session->fieldExists('name'); + $assert_session->fieldExists('pass'); + + // @see BrowserTestBase::drupalUserIsLoggedIn() + unset($this->loggedInUser->sessionId); + $this->loggedInUser = FALSE; + \Drupal::currentUser()->setAccount(new AnonymousUserSession()); + } + + /** + * 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) { + $this->addToAssertionCount(1); + return new WebAssert($this->getSession($name), $this->baseUrl); + } + + /** + * Retrieves a Drupal path or an absolute path. + * + * @param string|\Drupal\Core\Url $path + * Drupal path or URL to load into Mink controlled browser. + * @param array $options + * (optional) Options to be forwarded to the url generator. + * @param string[] $headers + * An array containing additional HTTP request headers, the array keys are + * the header names and the array values the header values. This is useful + * to set for example the "Accept-Language" header for requesting the page + * in a different language. Note that not all headers are supported, for + * example the "Accept" header is always overridden by the browser. For + * testing REST APIs it is recommended to obtain a separate HTTP client + * using getHttpClient() and performing requests that way. + * + * @return string + * The retrieved HTML string, also available as $this->getRawContent() + * + * @see \Drupal\Tests\BrowserTestBase::getHttpClient() + */ + protected function drupalGet($path, array $options = [], array $headers = []) { + $options['absolute'] = TRUE; + $url = $this->buildUrl($path, $options); + + $session = $this->getSession(); + + $this->prepareRequest(); + foreach ($headers as $header_name => $header_value) { + $session->setRequestHeader($header_name, $header_value); + } + + $session->visit($url); + $out = $session->getPage()->getContent(); + + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + + // Replace original page output with new output from redirected page(s). + if ($new = $this->checkForMetaRefresh()) { + $out = $new; + // We are finished with all meta refresh redirects, so reset the counter. + $this->metaRefreshCount = 0; + } + + // Log only for JavascriptTestBase tests because for Goutte we log with + // ::getResponseLogHandler. + if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { + $html_output = 'GET request to: ' . $url . + '
Ending URL: ' . $this->getSession()->getCurrentUrl(); + $html_output .= '
' . $out; + $html_output .= $this->getHtmlOutputHeaders(); + $this->htmlOutput($html_output); + } + + return $out; + } + + /** + * Builds an a absolute URL from a system path or a URL object. + * + * @param string|\Drupal\Core\Url $path + * A system path or a URL. + * @param array $options + * Options to be passed to Url::fromUri(). + * + * @return string + * An absolute URL stsring. + */ + protected function buildUrl($path, array $options = []) { + if ($path instanceof Url) { + $url_options = $path->getOptions(); + $options = $url_options + $options; + $path->setOptions($options); + return $path->setAbsolute()->toString(); + } + // The URL generator service is not necessarily available yet; e.g., in + // interactive installer tests. + elseif (\Drupal::hasService('url_generator')) { + $force_internal = isset($options['external']) && $options['external'] == FALSE; + if (!$force_internal && UrlHelper::isExternal($path)) { + return Url::fromUri($path, $options)->toString(); + } + else { + $uri = $path === '' ? 'base:/' : 'base:/' . $path; + // Path processing is needed for language prefixing. Skip it when a + // path that may look like an external URL is being used as internal. + $options['path_processing'] = !$force_internal; + return Url::fromUri($uri, $options) + ->setAbsolute() + ->toString(); + } + } + else { + return $this->getAbsoluteUrl($path); + } + } + + /** + * Takes a path and returns an absolute path. + * + * @param string $path + * A path from the Mink controlled browser content. + * + * @return string + * The $path with $base_url prepended, if necessary. + */ + protected function getAbsoluteUrl($path) { + global $base_url, $base_path; + + $parts = parse_url($path); + if (empty($parts['host'])) { + // Ensure that we have a string (and no xpath object). + $path = (string) $path; + // Strip $base_path, if existent. + $length = strlen($base_path); + if (substr($path, 0, $length) === $base_path) { + $path = substr($path, $length); + } + // Ensure that we have an absolute path. + if (empty($path) || $path[0] !== '/') { + $path = '/' . $path; + } + // Finally, prepend the $base_url. + $path = $base_url . $path; + } + return $path; + } + + /** + * Prepare for a request to testing site. + * + * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is + * checked by drupal_valid_test_ua(). + * + * @see drupal_valid_test_ua() + */ + protected function prepareRequest() { + $session = $this->getSession(); + $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix)); + } + + /** + * Returns whether a given user account is logged in. + * + * @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(AccountInterface $account) { + $logged_in = FALSE; + + if (isset($account->sessionId)) { + $session_handler = \Drupal::service('session_handler.storage'); + $logged_in = (bool) $session_handler->read($account->sessionId); + } + + return $logged_in; + } + + /** + * Clicks the element with the given CSS selector. + * + * @param string $css_selector + * The CSS selector identifying the element to click. + */ + protected function click($css_selector) { + $starting_url = $this->getSession()->getCurrentUrl(); + $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector)); + // Log only for JavascriptTestBase tests because for Goutte we log with + // ::getResponseLogHandler. + if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) { + $out = $this->getSession()->getPage()->getContent(); + $html_output = + 'Clicked element with CSS selector: ' . $css_selector . + '
Starting URL: ' . $starting_url . + '
Ending URL: ' . $this->getSession()->getCurrentUrl(); + $html_output .= '
' . $out; + $html_output .= $this->getHtmlOutputHeaders(); + $this->htmlOutput($html_output); + } + } + + /** + * 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. + * @param int $index + * (optional) The index number for cases where multiple links have the same + * text. Defaults to 0. + */ + protected function clickLink($label, $index = 0) { + $label = (string) $label; + $links = $this->getSession()->getPage()->findAll('named', ['link', $label]); + $this->assertArrayHasKey($index, $links, 'The link ' . $label . ' was not found on the page.'); + $links[$index]->click(); + } + + /** + * Retrieves the plain-text content from the current page. + */ + protected function getTextContent() { + return $this->getSession()->getPage()->getText(); + } + + /** + * Get the current URL from the browser. + * + * @return string + * The current URL. + */ + protected function getUrl() { + return $this->getSession()->getCurrentUrl(); + } + + /** + * Checks for meta refresh tag and if found call drupalGet() recursively. + * + * This function looks for the http-equiv attribute to be set to "Refresh" and + * is case-insensitive. + * + * @return string|false + * Either the new page content or FALSE. + */ + protected function checkForMetaRefresh() { + $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]'); + if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { + // Parse the content attribute of the meta tag for the format: + // "[delay]: URL=[page_to_redirect_to]". + if (preg_match('/\d+;\s*URL=(?.*)/i', $refresh[0]->getAttribute('content'), $match)) { + $this->metaRefreshCount++; + return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); + } + } + return FALSE; + } + + /** + * 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); + } + +}