diff --git a/core/modules/book/src/Tests/BookBreadcrumbTest.php b/core/modules/book/tests/src/Functional/BookBreadcrumbTest.php
similarity index 95%
rename from core/modules/book/src/Tests/BookBreadcrumbTest.php
rename to core/modules/book/tests/src/Functional/BookBreadcrumbTest.php
index 57e1b3d..69c0d62 100644
--- a/core/modules/book/src/Tests/BookBreadcrumbTest.php
+++ b/core/modules/book/tests/src/Functional/BookBreadcrumbTest.php
@@ -1,15 +1,15 @@
 <?php
 
-namespace Drupal\book\Tests;
+namespace Drupal\Tests\book\Functional;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Create a book, add pages, and test book interface.
  *
  * @group book
  */
-class BookBreadcrumbTest extends WebTestBase {
+class BookBreadcrumbTest extends BrowserTestBase {
 
   /**
    * Modules to install.
@@ -148,7 +148,7 @@ public function testBreadcrumbTitleUpdates() {
     $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
-      $got_breadcrumb[] = (string) $link;
+      $got_breadcrumb[] = $link->getText();
     }
     // Home link and four parent book nodes should be in the breadcrumb.
     $this->assertEqual(5, count($got_breadcrumb));
@@ -162,7 +162,7 @@ public function testBreadcrumbTitleUpdates() {
     $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
-      $got_breadcrumb[] = (string) $link;
+      $got_breadcrumb[] = $link->getText();
     }
     $this->assertEqual(5, count($got_breadcrumb));
     $this->assertEqual($edit['title[0][value]'], end($got_breadcrumb));
@@ -183,7 +183,7 @@ public function testBreadcrumbAccessUpdates() {
     $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
-      $got_breadcrumb[] = (string) $link;
+      $got_breadcrumb[] = $link->getText();
     }
     $this->assertEqual(5, count($got_breadcrumb));
     $this->assertEqual($edit['title[0][value]'], end($got_breadcrumb));
@@ -193,7 +193,7 @@ public function testBreadcrumbAccessUpdates() {
     $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
-      $got_breadcrumb[] = (string) $link;
+      $got_breadcrumb[] = $link->getText();
     }
     $this->assertEqual(4, count($got_breadcrumb));
     $this->assertEqual($nodes[2]->getTitle(), end($got_breadcrumb));
diff --git a/core/modules/book/src/Tests/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php
similarity index 87%
rename from core/modules/book/src/Tests/BookTest.php
rename to core/modules/book/tests/src/Functional/BookTest.php
index 7ebec63..53afc1a 100644
--- a/core/modules/book/src/Tests/BookTest.php
+++ b/core/modules/book/tests/src/Functional/BookTest.php
@@ -1,11 +1,11 @@
 <?php
 
-namespace Drupal\book\Tests;
+namespace Drupal\Tests\book\Functional;
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 use Drupal\user\RoleInterface;
 
 /**
@@ -13,7 +13,7 @@
  *
  * @group book
  */
-class BookTest extends WebTestBase {
+class BookTest extends BrowserTestBase {
 
   /**
    * Modules to install.
@@ -255,7 +255,7 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F
 
     // Check outline structure.
     if ($nodes !== NULL) {
-      $this->assertPattern($this->generateOutlinePattern($nodes), format_string('Node @number outline confirmed.', array('@number' => $number)));
+      $this->assertPattern($this->generateOutlinePattern($nodes));
     }
     else {
       $this->pass(format_string('Node %number does not have outline.', array('%number' => $number)));
@@ -267,14 +267,14 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F
       $url = $previous->urlInfo();
       $url->setOptions(array('attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page'))));
       $text = SafeMarkup::format('<b>‹</b> @label', array('@label' => $previous->label()));
-      $this->assertRaw(\Drupal::l($text, $url), 'Previous page link found.');
+      $this->assertRaw(\Drupal::l($text, $url));
     }
 
     if ($up) {
       /** @var \Drupal\Core\Url $url */
       $url = $up->urlInfo();
       $url->setOptions(array('attributes' => array('title' => t('Go to parent page'))));
-      $this->assertRaw(\Drupal::l('Up', $url), 'Up page link found.');
+      $this->assertRaw(\Drupal::l('Up', $url));
     }
 
     if ($next) {
@@ -282,7 +282,7 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F
       $url = $next->urlInfo();
       $url->setOptions(array('attributes' => array('rel' => array('next'), 'title' => t('Go to next page'))));
       $text = SafeMarkup::format('@label <b>›</b>', array('@label' => $next->label()));
-      $this->assertRaw(\Drupal::l($text, $url), 'Next page link found.');
+      $this->assertRaw(\Drupal::l($text, $url));
     }
 
     // Compute the expected breadcrumb.
@@ -296,7 +296,7 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F
     $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
-      $got_breadcrumb[] = (string) $link['href'];
+      $got_breadcrumb[] = $link->getAttribute('href');
     }
 
     // Compare expected and got breadcrumbs.
@@ -304,8 +304,8 @@ function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = F
 
     // Check printer friendly version.
     $this->drupalGet('book/export/html/' . $node->id());
-    $this->assertText($node->label(), 'Printer friendly title found.');
-    $this->assertRaw($node->body->processed, 'Printer friendly body found.');
+    $this->assertText($node->label());
+    $this->assertRaw($node->body->processed);
 
     $number++;
   }
@@ -384,35 +384,35 @@ function testBookExport() {
 
     // Make sure each part of the book is there.
     foreach ($nodes as $node) {
-      $this->assertText($node->label(), 'Node title found in printer friendly version.');
-      $this->assertRaw($node->body->processed, 'Node body found in printer friendly version.');
+      $this->assertText($node->label());
+      $this->assertRaw($node->body->processed);
     }
 
     // Make sure we can't export an unsupported format.
     $this->drupalGet('book/export/foobar/' . $this->book->id());
-    $this->assertResponse('404', 'Unsupported export format returned "not found".');
+    $this->assertResponse('404');
 
     // Make sure we get a 404 on a not existing book node.
     $this->drupalGet('book/export/html/123');
-    $this->assertResponse('404', 'Not existing book node returned "not found".');
+    $this->assertResponse('404');
 
     // Make sure an anonymous user cannot view printer-friendly version.
     $this->drupalLogout();
 
     // Load the book and verify there is no printer-friendly version link.
     $this->drupalGet('node/' . $this->book->id());
-    $this->assertNoLink(t('Printer-friendly version'), 'Anonymous user is not shown link to printer-friendly version.');
+    $this->assertNoLink(t('Printer-friendly version'));
 
     // Try getting the URL directly, and verify it fails.
     $this->drupalGet('book/export/html/' . $this->book->id());
-    $this->assertResponse('403', 'Anonymous user properly forbidden.');
+    $this->assertResponse('403');
 
     // Now grant anonymous users permission to view the printer-friendly
     // version and verify that node access restrictions still prevent them from
     // seeing it.
     user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array('access printer-friendly version'));
     $this->drupalGet('book/export/html/' . $this->book->id());
-    $this->assertResponse('403', 'Anonymous user properly forbidden from seeing the printer-friendly version when denied by node access.');
+    $this->assertResponse('403');
   }
 
   /**
@@ -428,14 +428,14 @@ function testBookNavigationBlock() {
     $edit = array();
     $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
     $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
-    $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
+    $this->assertText(t('The changes have been saved.'));
 
     // Test correct display of the block.
     $nodes = $this->createBook();
     $this->drupalGet('<front>');
-    $this->assertText($block->label(), 'Book navigation block is displayed.');
-    $this->assertText($this->book->label(), format_string('Link to book root (@title) is displayed.', array('@title' => $nodes[0]->label())));
-    $this->assertNoText($nodes[0]->label(), 'No links to individual book pages are displayed.');
+    $this->assertText($block->label());
+    $this->assertText($this->book->label());
+    $this->assertNoText($nodes[0]->label());
   }
 
   /**
@@ -503,7 +503,7 @@ function testNavigationBlockOnAccessModuleInstalled() {
     $edit = array();
     $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
     $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
-    $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
+    $this->assertText(t('The changes have been saved.'));
 
     // Create a book.
     $this->createBook();
@@ -511,16 +511,16 @@ function testNavigationBlockOnAccessModuleInstalled() {
     // Test correct display of the block to registered users.
     $this->drupalLogin($this->webUser);
     $this->drupalGet('node/' . $this->book->id());
-    $this->assertText($block->label(), 'Book navigation block is displayed to registered users.');
+    $this->assertText($block->label());
     $this->drupalLogout();
 
     // Test correct display of the block to anonymous users.
     $this->drupalGet('node/' . $this->book->id());
-    $this->assertText($block->label(), 'Book navigation block is displayed to anonymous users.');
+    $this->assertText($block->label());
 
     // Test the 'book pages' block_mode setting.
     $this->drupalGet('<front>');
-    $this->assertNoText($block->label(), 'Book navigation block is not shown on non-book pages.');
+    $this->assertNoText($block->label());
   }
 
   /**
@@ -534,7 +534,7 @@ function testBookDelete() {
 
      // Test access to delete top-level and child book nodes.
      $this->drupalGet('node/' . $this->book->id() . '/outline/remove');
-     $this->assertResponse('403', 'Deleting top-level book node properly forbidden.');
+     $this->assertResponse('403');
      $this->drupalPostForm('node/' . $nodes[4]->id() . '/outline/remove', $edit, t('Remove'));
      $node_storage->resetCache(array($nodes[4]->id()));
      $node4 = $node_storage->load($nodes[4]->id());
@@ -571,34 +571,6 @@ function testBookDelete() {
    }
 
   /**
-   * Tests re-ordering of books.
-   */
-  public function testBookOrdering() {
-    // Create new book.
-    $this->createBook();
-    $book = $this->book;
-
-    $this->drupalLogin($this->adminUser);
-    $node1 = $this->createBookNode($book->id());
-    $node2 = $this->createBookNode($book->id());
-    $pid = $node1->book['nid'];
-
-    // Head to admin screen and attempt to re-order.
-    $this->drupalGet('admin/structure/book/' . $book->id());
-    $edit = array(
-      "table[book-admin-{$node1->id()}][weight]" => 1,
-      "table[book-admin-{$node2->id()}][weight]" => 2,
-      // Put node 2 under node 1.
-      "table[book-admin-{$node2->id()}][pid]" => $pid,
-    );
-    $this->drupalPostForm(NULL, $edit, t('Save book pages'));
-    // Verify weight was updated.
-    $this->assertFieldByName("table[book-admin-{$node1->id()}][weight]", 1);
-    $this->assertFieldByName("table[book-admin-{$node2->id()}][weight]", 2);
-    $this->assertFieldByName("table[book-admin-{$node2->id()}][pid]", $pid);
-  }
-
-  /**
    * Tests outline of a book.
    */
   public function testBookOutline() {
@@ -612,7 +584,7 @@ public function testBookOutline() {
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('node/' . $empty_book->id() . '/outline');
     $this->assertRaw(t('Book outline'));
-    $this->assertOptionSelected('edit-book-bid', 0, 'Node does not belong to a book');
+    $this->assertOptionSelected('edit-book-bid', 0);
     $this->assertNoLink(t('Remove from book outline'));
 
     $edit = array();
@@ -701,7 +673,7 @@ public function testAdminBookListing() {
     // Load the book page and assert the created book title is displayed.
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/structure/book');
-    $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
+    $this->assertText($this->book->label());
   }
 
   /**
@@ -718,7 +690,7 @@ public function testAdminBookNodeListing() {
     $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
 
     $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a');
-    $this->assertEqual((string) $elements[0], 'View', 'View link is found from the list.');
+    $this->assertEqual((string) $elements[0]->getText(), 'View', 'View link is found from the list.');
   }
 
   /**
diff --git a/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php b/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php
new file mode 100644
index 0000000..2ab7d3a
--- /dev/null
+++ b/core/modules/book/tests/src/FunctionalJavascript/BookJavascriptTest.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\Tests\book\FunctionalJavascript;
+
+use Behat\Mink\Exception\ExpectationException;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\node\Entity\Node;
+
+/**
+ * Tests Book javascript functionality.
+ *
+ * @group book
+ */
+class BookJavascriptTest extends JavascriptTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = ['book'];
+
+  /**
+   * Tests re-ordering of books.
+   */
+  public function testBookOrdering() {
+    $book = Node::create([
+      'type' => 'book',
+      'title' => 'Book',
+      'book' => ['bid' => 'new'],
+    ]);
+    $book->save();
+    $page1 = Node::create([
+      'type' => 'book',
+      'title' => '1st page',
+      'book' => ['bid' => $book->id(), 'pid' => $book->id(), 'weight' => 0],
+    ]);
+    $page1->save();
+    $page2 = Node::create([
+      'type' => 'book',
+      'title' => '2nd page',
+      'book' => ['bid' => $book->id(), 'pid' => $book->id(), 'weight' => 1],
+    ]);
+    $page2->save();
+
+    // Head to admin screen and attempt to re-order.
+    $this->drupalLogin($this->drupalCreateUser(['administer book outlines']));
+    $this->drupalGet('admin/structure/book/' . $book->id());
+
+    $session = $this->getSession();
+    $page = $session->getPage();
+
+    $weight_select1 = $page->findField("table[book-admin-{$page1->id()}][weight]");
+    $weight_select2 = $page->findField("table[book-admin-{$page2->id()}][weight]");
+
+    // Check that rows weight selects are hidden.
+    $this->assertFalse($weight_select1->isVisible());
+    $this->assertFalse($weight_select2->isVisible());
+
+    // Check that '2nd page' row is heavier than '1st page' row.
+    $this->assertTrue($weight_select2->getValue() > $weight_select1->getValue());
+
+    // Check that '1st page' precedes the '2nd page'.
+    $this->assertFirstPrecedesSecond('1st page', '2nd');
+
+    // Check that the 'unsaved changes' text is not present in the message area.
+    $this->assertSession()->pageTextNotContains('You have unsaved changes.');
+
+    // Drag and drop the '1st page' row over the '2nd page' row.
+    // @todo: Investigate why if trying the reverse (2nd over 1st), the 2nd is
+    //   indented instead of moved upward. This might be a bug in tabledrag.js.
+    $dragged = $this->xpath("//tr[@data-drupal-selector='edit-table-book-admin-{$page1->id()}']//a[@class='tabledrag-handle']")[0];
+    $target = $this->xpath("//tr[@data-drupal-selector='edit-table-book-admin-{$page2->id()}']//a[@class='tabledrag-handle']")[0];
+    $dragged->dragTo($target);
+
+    // Give javascript some time to manipulate the DOM.
+    $this->getSession()->wait(500);
+
+    // Check that the 'unsaved changes' text appeared in the message area.
+    $this->assertSession()->pageTextContains('You have unsaved changes.');
+
+    // Check that '2nd page' page precedes the '1st page'.
+    $this->assertFirstPrecedesSecond('2nd page', '1st page');
+
+    // Toggle row weight selects as visible.
+    $page->findButton(t('Show row weights'))->click();
+
+    // Check that rows weight selects are visible.
+    $this->assertTrue($weight_select1->isVisible());
+    $this->assertTrue($weight_select2->isVisible());
+
+    // Check that '1st page' row became heavier than '1st page' row.
+    $this->assertTrue($weight_select1->getValue() > $weight_select2->getValue());
+
+    // Toggle row weight selects back to hidden.
+    $page->findButton(t('Hide row weights'))->click();
+
+    // Check that rows weight selects are hidden again.
+    $this->assertFalse($weight_select1->isVisible());
+    $this->assertFalse($weight_select2->isVisible());
+
+    $this->submitForm([], t('Save book pages'));
+    $this->assertSession()->pageTextContains(t('Updated book @book.', ['@book' => $book->getTitle()]));
+
+    // Check again that '2nd page' is on top after form submit.
+    $this->assertFirstPrecedesSecond('2nd page', '1st page');
+
+    // Check that page reordering was done in the backend.
+    $page1 = Node::load($page1->id());
+    $page2 = Node::load($page2->id());
+    $this->assertTrue($page1->book['weight'] > $page2->book['weight']);
+  }
+
+  /**
+   * Asserts that the first piece of markup precedes the second.
+   *
+   * @param string $first
+   *   The first string to be checked.
+   * @param string $second
+   *   The second string to be checked.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   *   When any of the strings is not found.
+   */
+  protected function assertFirstPrecedesSecond($first, $second) {
+    $session = $this->getSession();
+    $text = $session->getPage()->getHtml();
+    if (($pos1 = strpos($text, $first)) === FALSE) {
+      throw new ExpectationException("Cannot find '$first'", $session->getDriver());
+    }
+    if (($pos2 = strpos($text, $second)) === FALSE) {
+      throw new ExpectationException("Cannot find '$second'", $session->getDriver());
+    }
+    $this->assertTrue($pos2 > $pos1, "'$first' precedes '$second'");
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
index 6ca9f85..ad42806 100644
--- a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
+++ b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
@@ -204,6 +204,39 @@ protected function assertNoRaw($raw) {
   }
 
   /**
+   * Triggers a pass if the Perl regex pattern is found in the raw content.
+   *
+   * @param string $pattern
+   *   Perl regex to look for including the regex delimiters.
+   *
+   * @deprecated Scheduled for removal in Drupal 9.0.0.
+   *   Use $this->assertSession()->responseMatches() instead.
+   */
+  protected function assertPattern($pattern) {
+    $this->assertSession()->responseMatches($pattern);
+  }
+
+  /**
+   * Asserts that a select option in the current page is checked.
+   *
+   * @param string $select
+   *   The locator of the select field to assert.
+   * @param string $option
+   *   Option to assert.
+   *
+   * @deprecated Scheduled for removal in Drupal 9.0.0. Use methods provided by
+   *   \Drupal\Tests\WebAssert instead, for example:
+   * @code
+   *   $option = $this->assertSession()->optionExists($select, $value);
+   *   $this->assertTrue($option->getAttribute('selected'));
+   * @endcode
+   */
+  protected function assertOptionSelected($select, $option) {
+    $node = $this->assertSession()->optionExists($select, $option);
+    $this->assertTrue($node->getAttribute('selected'));
+  }
+
+  /**
    * Pass if the page title is the given string.
    *
    * @param string $expected_title
