diff --git a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php b/core/modules/contact/tests/src/Functional/ContactAuthenticatedUserTest.php similarity index 84% rename from core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php rename to core/modules/contact/tests/src/Functional/ContactAuthenticatedUserTest.php index eb7c527..4b4a520 100644 --- a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php +++ b/core/modules/contact/tests/src/Functional/ContactAuthenticatedUserTest.php @@ -1,15 +1,15 @@ contactUser->getEmail(); $this->contactUser->setEmail(FALSE)->save(); $this->drupalGet('user/' . $this->contactUser->id() . '/contact'); - $this->assertResponse(404, 'Not found (404) returned when visiting a personal contact form for a user with no email address'); + $this->assertResponse(404); // Test that the 'contact tab' does not appear on the user profiles // for users without an email address configured. $this->drupalGet('user/' . $this->contactUser->id()); $contact_link = '/user/' . $this->contactUser->id() . '/contact'; $this->assertResponse(200); - $this->assertNoLinkByHref ($contact_link, 'The "contact" tab is hidden on profiles for users with no email address'); + $this->assertNoLinkByHref ($contact_link); // Restore original email address. $this->contactUser->setEmail($original_email)->save(); @@ -170,7 +175,7 @@ function testPersonalContactAccess() { $this->drupalLogin($this->adminUser); $edit = array('contact_default_status' => FALSE); $this->drupalPostForm('admin/config/people/accounts', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), 'Setting successfully saved.'); + $this->assertText(t('The configuration options have been saved.')); $this->drupalLogout(); // Re-create our contacted user with personal contact forms disabled by @@ -232,17 +237,17 @@ function testPersonalContactFlood() { // Submit contact form with correct values and check flood interval. for ($i = 0; $i < $flood_limit; $i++) { $this->submitPersonalContact($this->contactUser); - $this->assertText(t('Your message has been sent.'), 'Message sent.'); + $this->assertText(t('Your message has been sent.')); } // Submit contact form one over limit. $this->submitPersonalContact($this->contactUser); - $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => \Drupal::service('date.formatter')->formatInterval($this->config('contact.settings')->get('flood.interval')))), 'Normal user denied access to flooded contact form.'); + $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => \Drupal::service('date.formatter')->formatInterval($this->config('contact.settings')->get('flood.interval'))))); // Test that the admin user can still access the contact form even though // the flood limit was reached. $this->drupalLogin($this->adminUser); - $this->assertNoText('Try again later.', 'Admin user not denied access to flooded contact form.'); + $this->assertNoText('Try again later.Admin user not denied access to flooded contact form.'); } /** diff --git a/core/modules/contact/src/Tests/ContactSitewideTest.php b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php similarity index 99% rename from core/modules/contact/src/Tests/ContactSitewideTest.php rename to core/modules/contact/tests/src/Functional/ContactSitewideTest.php index b049f25..92dee6b 100644 --- a/core/modules/contact/src/Tests/ContactSitewideTest.php +++ b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php @@ -1,12 +1,12 @@ deleteContactForms(); $this->drupalGet('admin/structure/contact'); - $this->assertText('Personal', 'Personal form was not deleted'); + $this->assertText('Personal'); $this->assertNoLinkByHref('admin/structure/contact/manage/feedback'); // Ensure that the contact form won't be shown without forms. diff --git a/core/modules/contact/src/Tests/ContactStorageTest.php b/core/modules/contact/tests/src/Functional/ContactStorageTest.php similarity index 98% rename from core/modules/contact/src/Tests/ContactStorageTest.php rename to core/modules/contact/tests/src/Functional/ContactStorageTest.php index 6fd3edd..069cbcf 100644 --- a/core/modules/contact/src/Tests/ContactStorageTest.php +++ b/core/modules/contact/tests/src/Functional/ContactStorageTest.php @@ -1,6 +1,6 @@ assertSession()->fieldExists($name); if ($value !== NULL) { - $this->assertSession()->fieldValueEquals($name, $value); + $this->assertSession()->fieldValueEquals($name, (string) $value); } } @@ -148,6 +149,32 @@ protected function assertFieldById($id, $value = NULL) { } /** + * Asserts that a field exists with the given name or ID. + * + * @param string $field + * Name or ID of field to assert. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->fieldExists() instead. + */ + protected function assertField($field) { + $this->assertSession()->fieldExists($field); + } + + /** + * Asserts that a field exists with the given name or ID does NOT exist. + * + * @param string $field + * Name or ID of field to assert. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->fieldNotExists() instead. + */ + protected function assertNoField($field) { + $this->assertSession()->fieldNotExists($field); + } + + /** * Passes if the raw text IS found on the loaded page, fail otherwise. * * Raw text refers to the raw HTML that the page generated. @@ -163,12 +190,32 @@ protected function assertRaw($raw) { } /** + * Passes if the raw text IS not 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. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->responseNotContains() instead. + */ + protected function assertNoRaw($raw) { + $this->assertSession()->responseNotContains($raw); + } + + /** * Pass if the page title is the given string. * * @param string $expected_title * The string the page title should be. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->titleEquals() instead. */ protected function assertTitle($expected_title) { + // Cast MarkupInterface to string. + $expected_title = (string) $expected_title; return $this->assertSession()->titleEquals($expected_title); } @@ -181,12 +228,93 @@ protected function assertTitle($expected_title) { * Text between the anchor tags. * @param int $index * Link position counting from zero. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->linkExists() instead. */ protected function assertLink($label, $index = 0) { return $this->assertSession()->linkExists($label, $index); } /** + * Passes if a link with the specified label is not found. + * + * @param string|\Drupal\Component\Render\MarkupInterface $label + * Text between the anchor tags. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->linkNotExists() instead. + */ + protected function assertNoLink($label) { + return $this->assertSession()->linkNotExists($label); + } + + /** + * Passes if a link containing a given href (part) is found. + * + * @param string $href + * The full or partial value of the 'href' attribute of the anchor tag. + * @param int $index + * Link position counting from zero. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->linkByHref() instead. + */ + protected function assertLinkByHref($href, $index = 0) { + $this->assertSession()->linkByHrefExists($href, $index); + } + + /** + * Passes if a link containing a given href (part) is not found. + * + * @param string $href + * The full or partial value of the 'href' attribute of the anchor tag. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->linkByHrefNotExists() instead. + */ + protected function assertNoLinkByHref($href) { + $this->assertSession()->linkByHrefNotExists($href); + } + + /** + * Asserts that a field does not exist with the given ID and value. + * + * @param string $id + * ID of field to assert. + * @param string $value + * (optional) Value for the field, to assert that the field's value on the + * page doesn't match it. You may pass in NULL to skip checking the value, + * while still checking that the field doesn't exist. However, the default + * value ('') asserts that the field value is not an empty string. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->fieldNotExists() or + * $this->assertSession()->fieldValueNotEquals() instead. + */ + protected function assertNoFieldById($id, $value = '') { + if ($this->getSession()->getPage()->findField($id)) { + $this->assertSession()->fieldValueNotEquals($id, (string) $value); + } + else { + $this->assertSession()->fieldNotExists($id); + } + } + + /** + * Passes if the internal browser's URL matches the given path. + * + * @param \Drupal\Core\Url|string $path + * The expected system path or URL. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->addressEquals() instead. + */ + protected function assertUrl($path) { + $this->assertSession()->addressEquals($path); + } + + /** * Asserts that a select option in the current page exists. * * @param string $id @@ -217,20 +345,22 @@ protected function assertNoOption($id, $option) { } /** - * Passes if the internal browser's URL matches the given path. + * Passes if the raw text IS found escaped on the loaded page, fail otherwise. + * + * Raw text refers to the raw HTML that the page generated. * - * @param string $path - * The expected system path. + * @param string $raw + * Raw (HTML) string to look for. * * @deprecated Scheduled for removal in Drupal 9.0.0. - * Use $this->assertSession()->addressEquals() instead. + * Use $this->assertSession()->assertEscaped() instead. */ - protected function assertUrl($path) { - $this->assertSession()->addressEquals($path); + protected function assertEscaped($raw) { + $this->assertSession()->assertEscaped($raw); } /** - * Passes if the raw text IS NOT found escaped on the loaded page. + * Passes if the raw text is not found escaped on the loaded page. * * Raw text refers to the raw HTML that the page generated. * @@ -244,4 +374,125 @@ protected function assertNoEscaped($raw) { $this->assertSession()->assertNoEscaped($raw); } + /** + * Asserts whether an expected cache tag was present in the last response. + * + * @param string $expected_cache_tag + * The expected cache tag. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->responseHeaderContains() instead. + */ + protected function assertCacheTag($expected_cache_tag) { + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tag); + } + + /** + * 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. + */ + abstract public function assertSession($name = NULL); + + /** + * 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. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->buildXPathQuery() instead. + */ + public function buildXPathQuery($xpath, array $args = array()) { + return $this->assertSession()->buildXPathQuery($xpath, $args); + } + + /** + /** + * Asserts whether an expected cache context was present in the last response. + * + * @param string $expected_cache_context + * The expected cache context. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->assertSession()->responseHeaderContains() instead. + */ + protected function assertCacheContext($expected_cache_context) { + $cache_contexts = explode(' ', $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_context)); + $this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header."); + } + + /** + * Asserts that a checkbox field in the current page is checked. + * + * @param string $id + * ID of field 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. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Browser'; most tests do not override + * this default. + * + * @return bool + * TRUE on pass, FALSE on fail. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->getSession()->getPage()->findField instead. + */ + protected function assertFieldChecked($id, $message = '', $group = 'Browser') { + $elements = $this->getSession()->getPage()->findField($id); + $checked = $elements->hasCheckedField($id); + return $this->assertTrue(isset($checked) , $message ? $message : SafeMarkup::format('Checkbox field @id is checked.', array('@id' => $id)), $group); + } + + /** + * Asserts that a checkbox field in the current page is not checked. + * + * @param string $id + * ID of field 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. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Browser'; most tests do not override + * this default. + * + * @return bool + * TRUE on pass, FALSE on fail. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->getSession()->getPage()->findField instead. + */ + protected function assertNoFieldChecked($id, $message = '', $group = 'Browser') { + $elements = $this->getSession()->getPage()->findField($id); + $checked = $elements->hasCheckedField($id); + return $this->assertTrue(isset($checked), $message ? $message : SafeMarkup::format('Checkbox field @id is not checked.', array('@id' => $id)), $group); + } } diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 70a1840..3beb32f 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -43,11 +43,14 @@ */ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase { use AssertHelperTrait; - use BlockCreationTrait; + use BlockCreationTrait { + placeBlock as drupalPlaceBlock; + } use AssertLegacyTrait; use RandomGeneratorTrait; use SessionTestTrait; use NodeCreationTrait { + getNodeByTitle as drupalGetNodeByTitle; createNode as drupalCreateNode; } use ContentTypeCreationTrait { @@ -575,40 +578,60 @@ protected function prepareRequest() { } /** - * Retrieves a Drupal path or an absolute path. + * Builds an a absolute URL from a system path or a URL object. * * @param string|\Drupal\Core\Url $path - * Drupal path or URL to load into Mink controlled browser. + * A system path or a URL. * @param array $options - * (optional) Options to be forwarded to the url generator. + * Options to be passed to Url::fromUri(). * * @return string - * The retrieved HTML string, also available as $this->getRawContent() + * An absolute URL stsring. */ - protected function drupalGet($path, array $options = array()) { - $options['absolute'] = TRUE; - + protected function buildUrl($path, array $options = array()) { if ($path instanceof Url) { $url_options = $path->getOptions(); $options = $url_options + $options; $path->setOptions($options); - $url = $path->setAbsolute()->toString(); + 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')) { - if (UrlHelper::isExternal($path)) { - $url = Url::fromUri($path, $options)->toString(); + $force_internal = isset($options['external']) && $options['external'] == FALSE; + if (!$force_internal && UrlHelper::isExternal($path)) { + return Url::fromUri($path, $options)->toString(); } else { - // This is needed for language prefixing. - $options['path_processing'] = TRUE; - $url = Url::fromUri('base:/' . $path, $options)->toString(); + $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 { - $url = $this->getAbsoluteUrl($path); + 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. + * + * @return string + * The retrieved HTML string, also available as $this->getRawContent() + */ + protected function drupalGet($path, array $options = array()) { + $options['absolute'] = TRUE; + $url = $this->buildUrl($path, $options); + $session = $this->getSession(); $this->prepareRequest(); @@ -902,6 +925,16 @@ protected function submitForm(array $edit, $submit, $form_html_id = NULL) { // 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); } @@ -1690,64 +1723,11 @@ protected function getTextContent() { * The list of elements matching the xpath expression. */ protected function xpath($xpath, array $arguments = []) { - $xpath = $this->buildXPathQuery($xpath, $arguments); + $xpath = $this->assertSession()->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 diff --git a/core/tests/Drupal/Tests/WebAssert.php b/core/tests/Drupal/Tests/WebAssert.php index ec31fdf..6156dda 100644 --- a/core/tests/Drupal/Tests/WebAssert.php +++ b/core/tests/Drupal/Tests/WebAssert.php @@ -8,6 +8,7 @@ use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Session; use Drupal\Component\Utility\Html; +use Drupal\Core\Url; /** * Defines a class with methods for asserting presence of elements during tests. @@ -38,6 +39,9 @@ public function __construct(Session $session, $base_url = '') { * {@inheritdoc} */ protected function cleanUrl($url) { + if ($url instanceof Url) { + $url = $url->setAbsolute()->toString(); + } // Strip the base URL from the beginning for absolute URLs. if ($this->baseUrl !== '' && strpos($url, $this->baseUrl) === 0) { $url = substr($url, strlen($this->baseUrl)); @@ -76,6 +80,24 @@ public function buttonExists($button, TraversableElement $container = NULL) { } /** + * Checks that the specific button does NOT exist on the current page. + * + * @param string $button + * One of id|name|label|value for the button. + * @param \Behat\Mink\Element\TraversableElement $container + * (optional) The document to check against. Defaults to the current page. + * + * @throws \Behat\Mink\Exception\ExpectationException + * When the button exists. + */ + public function buttonNotExists($button, TraversableElement $container = NULL) { + $container = $container ?: $this->session->getPage(); + $node = $container->findButton($button); + + $this->assert(NULL === $node, sprintf('A button "%s" appears on this page, but it should not.', $button)); + } + + /** * Checks that specific select field exists on the current page. * * @param string $select @@ -191,7 +213,7 @@ public function titleEquals($expected_title) { * * An optional link index may be passed. * - * @param string|\Drupal\Component\Render\MarkupInterface $label + * @param string $label * Text between the anchor tags. * @param int $index * Link position counting from zero. @@ -204,14 +226,126 @@ public function titleEquals($expected_title) { * Thrown when element doesn't exist, or the link label is a different one. */ public function linkExists($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->session->getPage()->findAll('named', ['link', $label]); - if (empty($links[$index])) { - throw new ExpectationException($message); + $this->assert(!empty($links[$index]), $message); + } + + /** + * Passes if a link with the specified label is not found. + * + * An optional link index may be passed. + * + * @param string $label + * Text between the anchor tags. + * @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. + * + * @throws \Behat\Mink\Exception\ExpectationException + * Thrown when element doesn't exist, or the link label is a different one. + */ + public function linkNotExists($label, $message = '') { + $message = ($message ? $message : strtr('Link with label %label found.', ['%label' => $label])); + $links = $this->session->getPage()->findAll('named', ['link', $label]); + $this->assert(empty($links), $message); + } + + /** + * Passes if a link containing a given href (part) is found. + * + * @param string $href + * The full or partial value of the 'href' attribute of the anchor tag. + * @param int $index + * Link position counting from zero. + * @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. + * + * @throws \Behat\Mink\Exception\ExpectationException + * Thrown when element doesn't exist, or the link label is a different one. + */ + public function linkByHrefExists($href, $index = 0, $message = '') { + $xpath = $this->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $href]); + $message = ($message ? $message : strtr('Link containing href %href found.', ['%href' => $href])); + $links = $this->session->getPage()->findAll('xpath', $xpath); + $this->assert(!empty($links[$index]), $message); + } + + /** + * Passes if a link containing a given href (part) is not found. + * + * @param string $href + * The full or partial value of the 'href' attribute of the anchor tag. + * @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. + * + * @throws \Behat\Mink\Exception\ExpectationException + * Thrown when element doesn't exist, or the link label is a different one. + */ + public function linkByHrefNotExists($href, $message = '') { + $xpath = $this->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $href]); + $message = ($message ? $message : strtr('Link containing href %href found.', ['%href' => $href])); + $links = $this->session->getPage()->findAll('xpath', $xpath); + $this->assert(empty($links), $message); + } + + /** + * 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. + */ + public function buildXPathQuery($xpath, array $args = array()) { + // Replace placeholders. + foreach ($args as $placeholder => $value) { + if (is_object($value)) { + throw new \InvalidArgumentException('Just pass in scalar values.'); + } + // 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); } - $this->assert($links[$index] !== NULL, $message); + return $xpath; } /** @@ -223,7 +357,19 @@ public function linkExists($label, $index = 0, $message = '') { * Raw (HTML) string to look for. */ public function assertNoEscaped($raw) { - $this->pageTextNotContains(Html::escape($raw)); + $this->responseNotContains(Html::escape($raw)); + } + + /** + * Passes if the raw text IS 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. + */ + public function assertEscaped($raw) { + $this->responseContains(Html::escape($raw)); } /** @@ -247,4 +393,33 @@ public function assert($condition, $message) { throw new ExpectationException($message, $this->session->getDriver()); } + /** + * Checks that a given form field element is disabled. + * + * @param string $field + * One of id|name|label|value for the field. + * @param \Behat\Mink\Element\TraversableElement $container + * (optional) The document to check against. Defaults to the current page. + * + * @return \Behat\Mink\Element\NodeElement + * The matching element. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function fieldDisabled($field, TraversableElement $container = NULL) { + $container = $container ?: $this->session->getPage(); + $node = $container->findField($field); + + if ($node === NULL) { + throw new ElementNotFoundException($this->session->getDriver(), 'field', 'id|name|label|value', $field); + } + + if (!$node->hasAttribute('disabled')) { + throw new ExpectationException("Field $field is disabled", $this->session->getDriver()); + } + + return $node; + } + }