diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 7204c5d06086db90a929e67f89d204d1a2eceee3..589d451c3338ad9b154089002a60183fd06c236d 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -621,9 +621,9 @@ function drupal_valid_test_ua($new_prefix = NULL) {
   // string.
   $http_user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : NULL;
   $user_agent = isset($_COOKIE['SIMPLETEST_USER_AGENT']) ? $_COOKIE['SIMPLETEST_USER_AGENT'] : $http_user_agent;
-  if (isset($user_agent) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $user_agent, $matches)) {
+  if (isset($user_agent) && preg_match("/^(simpletest\d+):(.+):(.+):(.+)$/", $user_agent, $matches)) {
     list(, $prefix, $time, $salt, $hmac) = $matches;
-    $check_string =  $prefix . ';' . $time . ';' . $salt;
+    $check_string =  $prefix . ':' . $time . ':' . $salt;
     // Read the hash salt prepared by drupal_generate_test_ua().
     // This function is called before settings.php is read and Drupal's error
     // handlers are set up. While Drupal's error handling may be properly
@@ -680,8 +680,8 @@ function drupal_generate_test_ua($prefix) {
   }
   // Generate a moderately secure HMAC based on the database credentials.
   $salt = uniqid('', TRUE);
-  $check_string = $prefix . ';' . time() . ';' . $salt;
-  return $check_string . ';' . Crypt::hmacBase64($check_string, $key);
+  $check_string = $prefix . ':' . time() . ':' . $salt;
+  return $check_string . ':' . Crypt::hmacBase64($check_string, $key);
 }
 
 /**
diff --git a/core/modules/simpletest/src/BrowserTestBase.php b/core/modules/simpletest/src/BrowserTestBase.php
index dc933ac60cdaffc86b857702284ba2f22e248a4b..049db12976e6cf9bb6720892ff2b45a48a33d02e 100644
--- a/core/modules/simpletest/src/BrowserTestBase.php
+++ b/core/modules/simpletest/src/BrowserTestBase.php
@@ -27,6 +27,7 @@
 use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
+use Symfony\Component\CssSelector\CssSelector;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -225,8 +226,18 @@ abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
    */
   protected $preserveGlobalState = FALSE;
 
+  /**
+   * The base URL.
+   *
+   * @var string
+   */
+   protected $baseUrl;
+
   /**
    * Initializes Mink sessions.
+   *
+   * @return \Behat\Mink\Session
+   *   The mink session.
    */
   protected function initMink() {
     $driver = $this->getDefaultDriverInstance();
@@ -241,6 +252,17 @@ protected function initMink() {
     $this->mink->setDefaultSessionName('default');
     $this->registerSessions();
 
+    // According to the W3C WebDriver specification a cookie can only be set if
+    // the cookie domain is equal to the domain of the active document. When the
+    // browser starts up the active document is not our domain but 'about:blank'
+    // or similar. To be able to set our User-Agent and Xdebug cookies at the
+    // start of the test we now do a request to the front page so the active
+    // document matches the domain.
+    // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
+    // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
+    $session = $this->getSession();
+    $session->visit($this->baseUrl);
+
     return $session;
   }
 
@@ -271,7 +293,7 @@ protected function getDefaultDriverInstance() {
 
     if (is_array($this->minkDefaultDriverArgs)) {
        // Use ReflectionClass to instantiate class with received params.
-      $reflector = new ReflectionClass($this->minkDefaultDriverClass);
+      $reflector = new \ReflectionClass($this->minkDefaultDriverClass);
       $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
     }
     else {
@@ -317,6 +339,8 @@ protected function setUp() {
     $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
     $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80;
 
+    $this->baseUrl = $base_url;
+
     // If the passed URL schema is 'https' then setup the $_SERVER variables
     // properly so that testing will run under HTTPS.
     if ($parsed_url['scheme'] === 'https') {
@@ -1335,6 +1359,40 @@ protected function drupalUserIsLoggedIn(UserInterface $account) {
     return $logged_in;
   }
 
+  /**
+   * Asserts that the element with the given CSS selector is present.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   * @param string $message
+   *   Optional message to show alongside the assertion.
+   */
+  protected function assertElementPresent($css_selector, $message = '') {
+    $this->assertNotEmpty($this->getSession()->getDriver()->find(CssSelector::toXPath($css_selector)), $message);
+  }
+
+  /**
+   * Asserts that the element with the given CSS selector is not present.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   * @param string $message
+   *   Optional message to show alongside the assertion.
+   */
+  protected function assertElementNotPresent($css_selector, $message = '') {
+    $this->assertEmpty($this->getSession()->getDriver()->find(CssSelector::toXPath($css_selector)), $message);
+  }
+
+  /**
+   * 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) {
+    $this->getSession()->getDriver()->click(CssSelector::toXPath($css_selector));
+  }
+
   /**
    * Prevents serializing any properties.
    *
diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php
index fab88337c199800d019bd112c50be3b93d5fd1aa..4cecfd62526853cbd5d067d00f077d88c8908ddc 100644
--- a/core/modules/simpletest/src/TestDiscovery.php
+++ b/core/modules/simpletest/src/TestDiscovery.php
@@ -123,6 +123,7 @@ public function registerTestNamespaces() {
       $this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit";
       $this->testNamespaces["Drupal\\Tests\\$name\\Kernel\\"][] = "$base_path/tests/src/Kernel";
       $this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional";
+      $this->testNamespaces["Drupal\\Tests\\$name\\FunctionalJavascript\\"][] = "$base_path/tests/src/FunctionalJavascript";
     }
 
     foreach ($this->testNamespaces as $prefix => $paths) {
diff --git a/core/modules/simpletest/tests/src/FunctionalJavascript/BrowserWithJavascriptTest.php b/core/modules/simpletest/tests/src/FunctionalJavascript/BrowserWithJavascriptTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c856203d391665314098cc9e1c8e117ccb18453e
--- /dev/null
+++ b/core/modules/simpletest/tests/src/FunctionalJavascript/BrowserWithJavascriptTest.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\simpletest\FunctionalJavascript\BrowserWithJavascriptTest.
+ */
+
+namespace Drupal\Tests\simpletest\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests if we can execute JavaScript in the browser.
+ *
+ * @group javascript
+ */
+class BrowserWithJavascriptTest extends JavascriptTestBase {
+
+  public function testJavascript() {
+    $this->drupalGet('<front>');
+    $session = $this->getSession();
+
+    $session->resizeWindow(400, 300);
+    $javascript = <<<JS
+    (function(){
+        var w = window,
+        d = document,
+        e = d.documentElement,
+        g = d.getElementsByTagName('body')[0],
+        x = w.innerWidth || e.clientWidth || g.clientWidth,
+        y = w.innerHeight || e.clientHeight|| g.clientHeight;
+        return x == 400 && y == 300;
+    }());
+JS;
+    $this->assertJsCondition($javascript);
+  }
+
+  public function testAssertJsCondition() {
+    $this->drupalGet('<front>');
+    $session = $this->getSession();
+
+    $session->resizeWindow(500, 300);
+    $javascript = <<<JS
+    (function(){
+        var w = window,
+        d = document,
+        e = d.documentElement,
+        g = d.getElementsByTagName('body')[0],
+        x = w.innerWidth || e.clientWidth || g.clientWidth,
+        y = w.innerHeight || e.clientHeight|| g.clientHeight;
+        return x == 400 && y == 300;
+    }());
+JS;
+
+    // We expected the following assertion to fail because the window has been
+    // re-sized to have a width of 500 not 400.
+    $this->setExpectedException(\PHPUnit_Framework_AssertionFailedError::class);
+    $this->assertJsCondition($javascript, 100);
+  }
+
+}
diff --git a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2354a00216bc58963b1d7deb404c294a1a3d009
--- /dev/null
+++ b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\toolbar\FunctionalJavascript\ToolbarIntegrationTest.
+ */
+
+namespace Drupal\Tests\toolbar\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the JavaScript functionality of the toolbar.
+ *
+ * @group toolbar
+ */
+class ToolbarIntegrationTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['toolbar', 'node'];
+
+  /**
+   * Tests if the toolbar can be toggled with JavaScript.
+   */
+  public function testToolbarToggling() {
+    $admin_user = $this->drupalCreateUser([
+      'access toolbar',
+      'administer site configuration',
+      'access content overview'
+    ]);
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('<front>');
+
+    // Test that it is possible to toggle the toolbar tray.
+    $this->assertElementVisible('#toolbar-link-system-admin_content', 'Toolbar tray is open by default.');
+    $this->click('#toolbar-item-administration');
+    $this->assertElementNotVisible('#toolbar-link-system-admin_content', 'Toolbar tray is closed after clicking the "Manage" button.');
+    $this->click('#toolbar-item-administration');
+    $this->assertElementVisible('#toolbar-link-system-admin_content', 'Toolbar tray is visible again after clicking the "Manage" button a second time.');
+
+    // Test toggling the toolbar tray between horizontal and vertical.
+    $this->assertElementVisible('#toolbar-item-administration-tray.toolbar-tray-horizontal', 'Toolbar tray is horizontally oriented by default.');
+    $this->assertElementNotPresent('#toolbar-item-administration-tray.toolbar-tray-vertical', 'Toolbar tray is not vertically oriented by default.');
+
+    $this->click('#toolbar-item-administration-tray button.toolbar-icon-toggle-vertical');
+    $this->assertJsCondition('jQuery("#toolbar-item-administration-tray").hasClass("toolbar-tray-vertical")');
+    $this->assertElementVisible('#toolbar-item-administration-tray.toolbar-tray-vertical', 'After toggling the orientation the toolbar tray is now displayed vertically.');
+
+    $this->click('#toolbar-item-administration-tray button.toolbar-icon-toggle-horizontal');
+    $this->assertJsCondition('jQuery("#toolbar-item-administration-tray").hasClass("toolbar-tray-horizontal")');
+    $this->assertElementVisible('#toolbar-item-administration-tray.toolbar-tray-horizontal', 'After toggling the orientation a second time the toolbar tray is displayed horizontally again.');
+  }
+
+}
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index 2620fe925a29c61ef2f2a931085c02e39f9db133..4b9150e1b5287374c1297459de01eb53feeb90a0 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -50,6 +50,17 @@
       <!-- Exclude Drush tests. -->
       <exclude>./drush/tests</exclude>
     </testsuite>
+    <testsuite name="functional-javascript">
+      <directory>./tests/Drupal/FunctionalJavascriptTests</directory>
+      <directory>./modules/*/tests/src/FunctionalJavascript</directory>
+      <directory>../modules/*/tests/src/FunctionalJavascript</directory>
+      <directory>../profiles/*/tests/src/FunctionalJavascript</directory>
+      <directory>../sites/*/modules/*/tests/src/FunctionalJavascript</directory>
+      <!-- Exclude Composer's vendor directory so we don't run tests there. -->
+      <exclude>./vendor</exclude>
+      <!-- Exclude Drush tests. -->
+      <exclude>./drush/tests</exclude>
+    </testsuite>
   </testsuites>
   <listeners>
     <listener class="\Drupal\Tests\Listeners\DrupalStandardsListener">
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab1c79dfa9e0209aa92bc9e5d3910a2434221fb6
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\FunctionalJavascriptTests\JavascriptTestBase.
+ */
+
+namespace Drupal\FunctionalJavascriptTests;
+
+use Drupal\simpletest\BrowserTestBase;
+use Symfony\Component\CssSelector\CssSelector;
+use Zumba\Mink\Driver\PhantomJSDriver;
+
+/**
+ * Runs a browser test using PhantomJS.
+ *
+ * Base class for testing browser interaction implemented in JavaScript.
+ */
+abstract class JavascriptTestBase extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $minkDefaultDriverClass = PhantomJSDriver::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initMink() {
+    // Set up the template cache used by the PhantomJS mink driver.
+    $path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache';
+    $this->minkDefaultDriverArgs = [
+      'http://127.0.0.1:8510',
+      $path,
+    ];
+    if (!file_exists($path)) {
+      mkdir($path);
+    }
+    return parent::initMink();
+  }
+
+  /**
+   * Asserts that the element with the given CSS selector is visible.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   * @param string $message
+   *   Optional message to show alongside the assertion.
+   */
+  protected function assertElementVisible($css_selector, $message = '') {
+    $this->assertTrue($this->getSession()->getDriver()->isVisible(CssSelector::toXPath($css_selector)), $message);
+  }
+
+  /**
+   * Asserts that the element with the given CSS selector is not visible.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   * @param string $message
+   *   Optional message to show alongside the assertion.
+   */
+  protected function assertElementNotVisible($css_selector, $message = '') {
+    $this->assertFalse($this->getSession()->getDriver()->isVisible(CssSelector::toXPath($css_selector)), $message);
+  }
+
+  /**
+   * Waits for the given time or until the given JS condition becomes TRUE.
+   *
+   * @param string $condition
+   *   JS condition to wait until it becomes TRUE.
+   * @param int $timeout
+   *   (Optional) Timeout in milliseconds, defaults to 1000.
+   * @param string $message
+   *   (optional) A message to display with the assertion. If left blank, a
+   *   default message will be displayed.
+   *
+   * @throws \PHPUnit_Framework_AssertionFailedError
+   *
+   * @see \Behat\Mink\Driver\DriverInterface::evaluateScript()
+   */
+  protected function assertJsCondition($condition, $timeout = 1000, $message = '') {
+    $message = $message ?: "Javascript condition met:\n" . $condition;
+    $result = $this->getSession()->getDriver()->wait($timeout, $condition);
+    $this->assertTrue($result, $message);
+  }
+
+}
diff --git a/core/tests/README.md b/core/tests/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..dfd1154a753c43458f578a4d8da45b2c22e7c954
--- /dev/null
+++ b/core/tests/README.md
@@ -0,0 +1,15 @@
+# Running tests
+
+## Functional tests
+
+* Start PhantomJS:
+  ```
+  phantomjs --ssl-protocol=any --ignore-ssl-errors=true ./vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null &
+  ```
+* Run the functional tests:
+  ```
+  export SIMPLETEST_DB='mysql://root@localhost/dev_d8'
+  export SIMPLETEST_BASE_URL='http://d8.dev'
+  ./vendor/bin/phpunit -c core --testsuite functional
+  ./vendor/bin/phpunit -c core --testsuite functional-javascript
+  ```
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index f1f786255cc035472b182ddb550a4692bfc3847d..12c4debcbb01383582eb17f91e35a658442cb18d 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -104,6 +104,8 @@ function drupal_phpunit_populate_class_loader() {
   // Start with classes in known locations.
   $loader->add('Drupal\\Tests', __DIR__);
   $loader->add('Drupal\\KernelTests', __DIR__);
+  $loader->add('Drupal\\FunctionalTests', __DIR__);
+  $loader->add('Drupal\\FunctionalJavascriptTests', __DIR__);
 
   if (!isset($GLOBALS['namespaces'])) {
     // Scan for arbitrary extension namespaces from core and contrib.