diff --git a/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php b/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php
new file mode 100644
index 0000000..44b2c7e
--- /dev/null
+++ b/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\tabledrag_test\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\State\StateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for draggable table testing.
+ */
+class TableDragTestForm extends FormBase {
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a TableDragTestForm object.
+   *
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
+   */
+  public function __construct(StateInterface $state) {
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('state'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'tabledrag_test_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['table'] = [
+      '#type' => 'table',
+      '#header' => [$this->t('Text'), $this->t('Weight')],
+      '#tabledrag' => [[
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'tabledrag-test-weight',
+      ]],
+    ];
+
+    foreach ($this->state->get('tabledrag_test_table') as $delta => $row) {
+      $form['table'][$delta] = [
+        'label' => [
+          '#type' => 'textfield',
+          '#default_value' => $row,
+        ],
+        'weight' => [
+          '#type' => 'weight',
+          '#default_value' => $delta,
+          '#attributes' => ['class' => ['tabledrag-test-weight']],
+        ],
+        '#attributes' => ['class' => ['draggable']],
+      ];
+    }
+
+    $form['save'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $table = array_map(function (array $row) {
+      return $row['label'];
+    }, $form_state->getValue('table'));
+    $this->state->set('tabledrag_test_table', $table);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml
new file mode 100644
index 0000000..98d6bb4
--- /dev/null
+++ b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml
@@ -0,0 +1,6 @@
+type: module
+name: 'TableDrag test'
+description: 'Draggable table test module.'
+core: 8.x
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml
new file mode 100644
index 0000000..1bb88ff
--- /dev/null
+++ b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml
@@ -0,0 +1,7 @@
+tabledrag_test.test_form:
+  path: '/tabledrag_test'
+  defaults:
+    _form: '\Drupal\tabledrag_test\Form\TableDragTestForm'
+    _title: 'Draggable table test'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php
new file mode 100644
index 0000000..a213888
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\TableDrag;
+
+use Behat\Mink\Exception\ExpectationException;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests draggable table.
+ *
+ * @group javascript
+ */
+class TableDragTest extends JavascriptTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['tabledrag_test'];
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->state = $this->container->get('state');
+    // Add a sample table structure as state.
+    $this->state->set('tabledrag_test_table', ['row0', 'row1', 'row2']);
+  }
+
+  /**
+   * Tests row weight switch.
+   */
+  public function testRowWeightSwitch() {
+    $this->drupalGet('tabledrag_test');
+
+    $session = $this->getSession();
+    $page = $session->getPage();
+
+    $weight_select0 = $page->findField("table[0][weight]");
+    $weight_select1 = $page->findField("table[1][weight]");
+    $weight_select2 = $page->findField("table[2][weight]");
+
+    // Check that rows weight selects are hidden.
+    $this->assertFalse($weight_select0->isVisible());
+    $this->assertFalse($weight_select1->isVisible());
+    $this->assertFalse($weight_select2->isVisible());
+
+    // Toggle row weight selects as visible.
+    $page->findButton(t('Show row weights'))->click();
+
+    // Check that rows weight selects are visible.
+    $this->assertTrue($weight_select0->isVisible());
+    $this->assertTrue($weight_select1->isVisible());
+    $this->assertTrue($weight_select2->isVisible());
+
+    // 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_select0->isVisible());
+    $this->assertFalse($weight_select1->isVisible());
+    $this->assertFalse($weight_select2->isVisible());
+  }
+
+  /**
+   * Tests draggable table drag'n'drop.
+   */
+  public function testDragAndDrop() {
+    $this->drupalGet('tabledrag_test');
+
+    $session = $this->getSession();
+    $page = $session->getPage();
+
+    $weight_select0 = $page->findField("table[0][weight]");
+    $weight_select1 = $page->findField("table[1][weight]");
+    $weight_select2 = $page->findField("table[2][weight]");
+
+    // Check weights.
+    $this->assertGreaterThan($weight_select0->getValue(), $weight_select1->getValue());
+    $this->assertGreaterThan($weight_select0->getValue(), $weight_select2->getValue());
+    $this->assertGreaterThan($weight_select1->getValue(), $weight_select2->getValue());
+
+    // Check that initially the rows are in the correct order.
+    $this->assertOrder(['row0', 'row1', 'row2']);
+
+    // Check that the 'unsaved changes' text is not present in the message area.
+    $this->assertSession()->pageTextNotContains('You have unsaved changes.');
+
+    $row0 = $this->xpath('//tr[@data-drupal-selector="edit-table-0"]//a[@class="tabledrag-handle"]')[0];
+    $row1 = $this->xpath('//tr[@data-drupal-selector="edit-table-1"]//a[@class="tabledrag-handle"]')[0];
+    $row2 = $this->xpath('//tr[@data-drupal-selector="edit-table-2"]//a[@class="tabledrag-handle"]')[0];
+
+    // Drag row0 over row1.
+    $row0->dragTo($row1);
+
+    // Give javascript some time to manipulate the DOM.
+    $session->wait(500);
+
+    // Check that the 'unsaved changes' text was added in the message area.
+    $this->assertSession()->pageTextContains('You have unsaved changes.');
+
+    // Check that row0 and row1 were swapped.
+    $this->assertOrder(['row1', 'row0', 'row2']);
+
+    // Check that weights were changed.
+    $this->assertGreaterThan($weight_select1->getValue(), $weight_select0->getValue());
+    $this->assertGreaterThan($weight_select1->getValue(), $weight_select2->getValue());
+    $this->assertGreaterThan($weight_select0->getValue(), $weight_select2->getValue());
+
+    // Now move the last row (row2) in the second position. row0 should go last.
+    $row2->dragTo($row0);
+
+    // Check that the order is: row1, row2 and row0.
+    $this->assertOrder(['row1', 'row2', 'row0']);
+  }
+
+  /**
+   * Asserts that several pieces of markup are in a given order in the page.
+   *
+   * @param string[] $items
+   *   An ordered list of strings.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   *   When any of the given string is not found.
+   */
+  protected function assertOrder(array $items) {
+    $session = $this->getSession();
+    $text = $session->getPage()->getHtml();
+    $strings = [];
+    foreach ($items as $item) {
+      if (($pos = strpos($text, $item)) === FALSE) {
+        throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver());
+      }
+      $strings[$pos] = $item;
+    }
+    ksort($strings);
+    $ordered = implode(', ', array_map(function ($item) {
+      return "'$item'";
+    }, $items));
+    $this->assertSame($items, array_values($strings), "Found strings, ordered as: $ordered");
+  }
+
+}
