diff --git a/core/modules/views/tests/src/Functional/ViewTestBase.php b/core/modules/views/tests/src/Functional/ViewTestBase.php
new file mode 100644
index 0000000..3de16b5
--- /dev/null
+++ b/core/modules/views/tests/src/Functional/ViewTestBase.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\Tests\views\Functional;
+
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\views\Tests\ViewResultAssertionTrait;
+use Drupal\views\Tests\ViewTestData;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Defines a base class for Views testing in the full web test environment.
+ *
+ * Use this base test class if you need to emulate a full Drupal installation.
+ * When possible, ViewsKernelTestBase should be used instead. Both base classes
+ * include the same methods.
+ *
+ * @see \Drupal\Tests\views\Kernel\ViewsKernelTestBase
+ * @see \Drupal\simpletest\WebTestBase
+ */
+abstract class ViewTestBase extends BrowserTestBase {
+
+  use ViewResultAssertionTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('views', 'views_test_config');
+
+  protected function setUp($import_test_views = TRUE) {
+    parent::setUp();
+    if ($import_test_views) {
+      ViewTestData::createTestViews(get_class($this), array('views_test_config'));
+    }
+  }
+
+  /**
+   * Sets up the views_test_data.module.
+   *
+   * Because the schema of views_test_data.module is dependent on the test
+   * using it, it cannot be enabled normally.
+   */
+  protected function enableViewsTestModule() {
+    // Define the schema and views data variable before enabling the test module.
+    \Drupal::state()->set('views_test_data_schema', $this->schemaDefinition());
+    \Drupal::state()->set('views_test_data_views_data', $this->viewsData());
+
+    \Drupal::service('module_installer')->install(array('views_test_data'));
+    $this->resetAll();
+    $this->rebuildContainer();
+    $this->container->get('module_handler')->reload();
+
+    // Load the test dataset.
+    $data_set = $this->dataSet();
+    $query = db_insert('views_test_data')
+      ->fields(array_keys($data_set[0]));
+    foreach ($data_set as $record) {
+      $query->values($record);
+    }
+    $query->execute();
+  }
+
+  /**
+   * Orders a nested array containing a result set based on a given column.
+   *
+   * @param array $result_set
+   *   An array of rows from a result set, with each row as an associative
+   *   array keyed by column name.
+   * @param string $column
+   *   The column name by which to sort the result set.
+   * @param bool $reverse
+   *   (optional) Boolean indicating whether to sort the result set in reverse
+   *   order. Defaults to FALSE.
+   *
+   * @return array
+   *   The sorted result set.
+   */
+  protected function orderResultSet($result_set, $column, $reverse = FALSE) {
+    $order = $reverse ? -1 : 1;
+    usort($result_set, function ($a, $b) use ($column, $order) {
+      if ($a[$column] == $b[$column]) {
+        return 0;
+      }
+      return $order * (($a[$column] < $b[$column]) ? -1 : 1);
+    });
+    return $result_set;
+  }
+
+  /**
+   * Asserts the existence of a button with a certain ID and label.
+   *
+   * @param string $id
+   *   The HTML ID of the button
+   * @param string $label.
+   *   The expected label for the button.
+   * @param string $message
+   *   (optional) A custom message to display with the assertion. If no custom
+   *   message is provided, the message will indicate the button label.
+   */
+  protected function helperButtonHasLabel($id, $expected_label, $message = 'Label has the expected value: %label.') {
+    $this->assertSession()->fieldValueEquals($id, $expected_label, t($message, array('%label' => $expected_label)));
+  }
+
+  /**
+   * Executes a view with debugging.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The view object.
+   * @param array $args
+   *   (optional) An array of the view arguments to use for the view.
+   */
+  protected function executeView(ViewExecutable $view, $args = array()) {
+    // A view does not really work outside of a request scope, due to many
+    // dependencies like the current user.
+    $view->setDisplay();
+    $view->preExecute($args);
+    $view->execute();
+    $verbose_message = '<pre>Executed view: ' . ((string) $view->build_info['query']) . '</pre>';
+    if ($view->build_info['query'] instanceof SelectInterface) {
+      $verbose_message .= '<pre>Arguments: ' . print_r($view->build_info['query']->getArguments(), TRUE) . '</pre>';
+    }
+    $this->verbose($verbose_message);
+  }
+
+  /**
+   * Returns the schema definition.
+   */
+  protected function schemaDefinition() {
+    return ViewTestData::schemaDefinition();
+  }
+
+  /**
+   * Returns the views data definition.
+   */
+  protected function viewsData() {
+    return ViewTestData::viewsData();
+  }
+
+  /**
+   * Returns a very simple test dataset.
+   */
+  protected function dataSet() {
+    return ViewTestData::dataSet();
+  }
+
+}
diff --git a/core/modules/views_ui/src/Tests/CachedDataUITest.php b/core/modules/views_ui/tests/src/Functional/CachedDataUITest.php
similarity index 96%
rename from core/modules/views_ui/src/Tests/CachedDataUITest.php
rename to core/modules/views_ui/tests/src/Functional/CachedDataUITest.php
index 1f69b19..87768ee 100644
--- a/core/modules/views_ui/src/Tests/CachedDataUITest.php
+++ b/core/modules/views_ui/tests/src/Functional/CachedDataUITest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\views_ui\Tests;
+namespace Drupal\Tests\views_ui\Functional;
 
 /**
  * Tests the user tempstore cache in the UI.
@@ -58,6 +58,7 @@ public function testCacheData() {
     $this->clickLink(t('break this lock'));
     $this->drupalPostForm(NULL, array(), t('Break lock'));
     // Test that save and cancel buttons are shown.
+    file_put_contents('/tmp/foo.html', $this->getSession()->getPage()->getHtml());
     $this->assertFieldById('edit-actions-submit', t('Save'));
     $this->assertFieldById('edit-actions-cancel', t('Cancel'));
     // Test we can save the view.
diff --git a/core/modules/views_ui/tests/src/Functional/UITestBase.php b/core/modules/views_ui/tests/src/Functional/UITestBase.php
new file mode 100644
index 0000000..303fb1d
--- /dev/null
+++ b/core/modules/views_ui/tests/src/Functional/UITestBase.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\Tests\views_ui\Functional;
+
+use Drupal\Tests\views\Functional\ViewTestBase;
+
+/**
+ * Provides a base class for testing the Views UI.
+ */
+abstract class UITestBase extends ViewTestBase {
+
+  /**
+   * An admin user with the 'administer views' permission.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * An admin user with administrative permissions for views, blocks, and nodes.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $fullAdminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node', 'views_ui', 'block', 'taxonomy');
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp($import_test_views = TRUE) {
+    parent::setUp();
+
+    $this->enableViewsTestModule();
+
+    $this->adminUser = $this->drupalCreateUser(array('administer views'));
+
+    $this->fullAdminUser = $this->drupalCreateUser(array('administer views',
+      'administer blocks',
+      'bypass node access',
+      'access user profiles',
+      'view all revisions',
+      'administer permissions',
+    ));
+    $this->drupalLogin($this->fullAdminUser);
+  }
+
+  /**
+   * A helper method which creates a random view.
+   */
+  public function randomView(array $view = array()) {
+    // Create a new view in the UI.
+    $default = array();
+    $default['label'] = $this->randomMachineName(16);
+    $default['id'] = strtolower($this->randomMachineName(16));
+    $default['description'] = $this->randomMachineName(16);
+    $default['page[create]'] = TRUE;
+    $default['page[path]'] = $default['id'];
+
+    $view += $default;
+
+    $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
+
+    return $default;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function drupalGet($path, array $options = array()) {
+    $url = $this->buildUrl($path, $options);
+
+    // Ensure that each nojs page is accessible via ajax as well.
+    if (strpos($url, 'nojs') !== FALSE) {
+      $url = str_replace('nojs', 'ajax', $url);
+      $result = $this->drupalGet($url, $options);
+      $this->assertSession()->statusCodeEquals(200);
+      $this->assertEquals('application/json', $this->getSession()->getResponseHeader('Content-Type'));
+      $this->assertTrue(json_decode($result), 'Ensure that the AJAX request returned valid content.');
+    }
+
+    return parent::drupalGet($path, $options);
+  }
+
+}
diff --git a/core/modules/views_ui/src/Tests/XssTest.php b/core/modules/views_ui/tests/src/Functional/XssTest.php
similarity index 78%
rename from core/modules/views_ui/src/Tests/XssTest.php
rename to core/modules/views_ui/tests/src/Functional/XssTest.php
index 8eac3a6..4a93f31 100644
--- a/core/modules/views_ui/src/Tests/XssTest.php
+++ b/core/modules/views_ui/tests/src/Functional/XssTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\views_ui\Tests;
+namespace Drupal\Tests\views_ui\Functional;
 
 /**
  * Tests the Xss vulnerability.
@@ -18,14 +18,18 @@ class XssTest extends UITestBase {
 
   public function testViewsUi() {
     $this->drupalGet('admin/structure/views');
-    $this->assertEscaped('<script>alert("foo");</script>, <marquee>test</marquee>', 'The view tag is properly escaped.');
+    // The view tag is properly escaped.
+    $this->assertEscaped('<script>alert("foo");</script>, <marquee>test</marquee>');
 
     $this->drupalGet('admin/structure/views/view/sa_contrib_2013_035');
-    $this->assertEscaped('<marquee>test</marquee>', 'Field admin label is properly escaped.');
+    // Field admin label is properly escaped.
+    $this->assertEscaped('<marquee>test</marquee>');
 
     $this->drupalGet('admin/structure/views/nojs/handler/sa_contrib_2013_035/page_1/header/area');
-    $this->assertEscaped('{{ title }} == <marquee>test</marquee>', 'Token label is properly escaped.');
-    $this->assertEscaped('{{ title_1 }} == <script>alert("XSS")</script>', 'Token label is properly escaped.');
+    // Token label is properly escaped.
+    $this->assertEscaped('{{ title }} == <marquee>test</marquee>');
+    // Token label is properly escaped.
+    $this->assertEscaped('{{ title_1 }} == <script>alert("XSS")</script>');
   }
 
   /**
diff --git a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
index 1f8bba6..098cbb3 100644
--- a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
+++ b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
@@ -52,11 +52,19 @@ protected function assertElementNotPresent($css_selector) {
    * @param string $text
    *   Plain text to look for.
    *
-   * @deprecated Scheduled for removal in Drupal 9.0.0.
-   *   Use $this->assertSession()->responseContains() instead.
+   *  Use $this->assertSession()->pageTextContains() or
+   *   $this->assertSession()->responseContains() instead.
    */
   protected function assertText($text) {
-    $this->assertSession()->responseContains($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);
+    }
   }
 
   /**
@@ -106,7 +114,7 @@ protected function assertResponse($code) {
   protected function assertFieldByName($name, $value = NULL) {
     $this->assertSession()->fieldExists($name);
     if ($value !== NULL) {
-      $this->assertSession()->fieldValueEquals($name, $value);
+      $this->assertSession()->fieldValueEquals($name, (string) $value);
     }
   }
 
@@ -149,4 +157,117 @@ protected function assertLink($label, $index = 0) {
     return $this->assertSession()->linkExists($label, $index);
   }
 
+  /**
+   * 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);
+  }
+
+  /**
+   * 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.
+   *
+   * @deprecated Scheduled for removal in Drupal 9.0.0.
+   *   Use $this->assertSession()->fieldExists() or
+   *   $this->assertSession()->fieldValueEquals() instead.
+   */
+  protected function assertFieldById($id, $value = NULL) {
+    $this->assertFieldByName($id, (string) $value);
+  }
+
+  /**
+   * 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()->fieldExists() or
+   *   $this->assertSession()->fieldValueEquals() 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) {
+    $path = "/$path";
+    $this->assertSession()->addressEquals($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 $raw
+   *   Raw (HTML) string to look for.
+   *
+   * @deprecated Scheduled for removal in Drupal 9.0.0.
+   *   Use $this->assertSession()->assertEscaped() instead.
+   */
+  protected function assertEscaped($raw) {
+    $this->assertSession()->assertEscaped($raw);
+  }
+
+  /**
+   * Passes if the raw text IS NOT found escaped 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()->assertNoEscaped() instead.
+   */
+  protected function assertNoEscaped($raw) {
+    $this->assertSession()->assertNoEscaped($raw);
+  }
+
+  /**
+   * 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);
+
 }
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 5a9b0c0..782f165 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -567,40 +567,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 === '<front>' ? '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();
@@ -1674,64 +1694,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 84fc744..963a6e0 100644
--- a/core/tests/Drupal/Tests/WebAssert.php
+++ b/core/tests/Drupal/Tests/WebAssert.php
@@ -113,6 +113,86 @@ public function linkExists($label, $index = 0, $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 = '') {
+    // Cast MarkupInterface objects to string.
+    $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);
+    if (empty($links[$index])) {
+      throw new ExpectationException($message);
+    }
+    $this->assert($links[$index] !== NULL, $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) {
+      // 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;
+  }
+
+  /**
    * Passes if the raw text IS NOT found escaped on the loaded page.
    *
    * Raw text refers to the raw HTML that the page generated.
@@ -121,7 +201,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));
   }
 
   /**
