only in patch2: unchanged: --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php @@ -0,0 +1,154 @@ +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' => [ + [ + 'data' => $this->t('Text'), + 'colspan' => 4, + ], + $this->t('Weight'), + ], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'tabledrag-test-weight', + ], + [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'tabledrag-test-parent', + 'subgroup' => 'tabledrag-test-parent', + 'source' => 'tabledrag-test-id', + 'hidden' => TRUE, + 'limit' => 2, + ], + [ + 'action' => 'depth', + 'relationship' => 'group', + 'group' => 'tabledrag-test-depth', + 'hidden' => TRUE, + ], + ], + '#attributes' => ['id' => 'tabledrag-test-table'], + ]; + + foreach ($this->state->get('tabledrag_test_table') as $id => $row) { + if (!is_array($row)) { + $row = []; + } + + $row += [ + 'parent' => '', + 'weight' => 0, + 'depth' => 0, + 'classes' => [], + 'draggable' => TRUE, + ]; + + if (!empty($row['draggable'])) { + $row['classes'][] = 'draggable'; + } + + $form['table'][$id] = [ + 'title' => [ + 'indentation' => [ + '#theme' => 'indentation', + '#size' => $row['depth'], + ], + '#plain_text' => "Row with id $id", + ], + 'id' => [ + '#type' => 'hidden', + '#value' => $id, + '#attributes' => ['class' => ['tabledrag-test-id']], + ], + 'parent' => [ + '#type' => 'hidden', + '#default_value' => $row['parent'], + '#parents' => ['table', $id, 'parent'], + '#attributes' => ['class' => ['tabledrag-test-parent']], + ], + 'depth' => [ + '#type' => 'hidden', + '#default_value' => $row['depth'], + '#attributes' => ['class' => ['tabledrag-test-depth']], + ], + 'weight' => [ + '#type' => 'weight', + '#default_value' => $row['weight'], + '#attributes' => ['class' => ['tabledrag-test-weight']], + ], + '#attributes' => ['class' => $row['classes']], + ]; + } + + $form['save'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $test_table = []; + foreach ($form_state->getValue('table') as $row) { + $test_table[$row['id']] = $row; + } + + $this->state->set('tabledrag_test_table', $test_table); + } + +} only in patch2: unchanged: --- /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 only in patch2: unchanged: --- /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' only in patch2: unchanged: --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php @@ -0,0 +1,343 @@ +state = $this->container->get('state'); + } + + /** + * Tests accessibility through keyboard of the tabledrag functionality. + */ + public function testKeyboardAccessibility() { + $this->state->set('tabledrag_test_table', array_flip(range(1, 5))); + + $this->drupalGet('tabledrag_test'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Nest the row with id 2 as child of row 1. + $this->moveRowWithKeyboard($this->findRowById(2), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 3, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Nest the row with id 3 as child of row 1. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Nest the row with id 3 as child of row 2. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 3, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Nesting should be allowed to maximum level 3. + $this->moveRowWithKeyboard($this->findRowById(4), 'right', 4); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 3, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 4, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Re-order children of row 1. + $this->moveRowWithKeyboard($this->findRowById(4), 'up'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 3, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Move back the row 3 to the 1st level. + $this->moveRowWithKeyboard($this->findRowById(3), 'left'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + $this->moveRowWithKeyboard($this->findRowById(3), 'left'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 3, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE], + ['id' => 5, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Move row 3 to the last position. + $this->moveRowWithKeyboard($this->findRowById(3), 'down'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => TRUE], + ]); + + // Nothing happens when trying to move the last row further down. + $this->moveRowWithKeyboard($this->findRowById(3), 'down'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => TRUE], + ]); + + // Nest row 3 under 5. The max depth allowed should be 1. + $this->moveRowWithKeyboard($this->findRowById(3), 'right', 3); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE], + ]); + + // The first row of the table cannot be nested. + $this->moveRowWithKeyboard($this->findRowById(1), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE], + ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE], + ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE], + ]); + } + + /** + * Tests the root and leaf behaviors for rows. + */ + public function testRootLeafDraggableRowsWithKeyboard() { + $this->state->set('tabledrag_test_table', [ + 1 => [], + 2 => ['parent' => 1, 'depth' => 1, 'classes' => ['tabledrag-leaf']], + 3 => ['parent' => 1, 'depth' => 1], + 4 => [], + 5 => ['classes' => ['tabledrag-root']], + ]); + + $this->drupalGet('tabledrag_test'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Rows marked as root cannot be moved as children of another row. + $this->moveRowWithKeyboard($this->findRowById(5), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Rows marked as leaf cannot have children. Trying to move the row #3 + // as child of #2 should have no results. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Leaf can be still swapped and moved to first level. + $this->moveRowWithKeyboard($this->findRowById(2), 'down'); + $this->moveRowWithKeyboard($this->findRowById(2), 'left'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 2, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE ], + ['id' => 4, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => -7, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]); + + // Root rows can have children. + $this->moveRowWithKeyboard($this->findRowById(4), 'down'); + $this->moveRowWithKeyboard($this->findRowById(4), 'right'); + $this->assertDraggableTable([ + ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 2, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE], + ['id' => 5, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 4, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE], + ]); + } + + /** + * Asserts the whole structure of the draggable test table. + * + * @param array $structure + * The table structure. Each entry represents a row and consists of: + * - id: the expected value for the ID hidden field. + * - weight: the expected row weight. + * - parent: the expected parent ID for the row. + * - indentation: how many indents the row should have. + * - changed: whether or not the row should have been marked as changed. + */ + protected function assertDraggableTable(array $structure) { + $rows = $this->getSession()->getPage()->findAll('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr'); + $this->assertSameSize($structure, $rows); + + foreach ($structure as $delta => $expected) { + $this->assertTableRow($rows[$delta], $expected['id'], $expected['weight'], $expected['parent'], $expected['indentation'], $expected['changed']); + } + } + + /** + * Asserts the values of a draggable row. + * + * @param \Behat\Mink\Element\NodeElement $row + * The row element to assert. + * @param string $id + * The expected value for the ID hidden input of the row. + * @param int $weight + * The expected weight of the row. + * @param string $parent + * The expected parent ID. + * @param int $indentation + * The expected indentation of the row. + * @param bool $changed + * Whether or not the row should have been marked as changed. + */ + protected function assertTableRow(NodeElement $row, $id, $weight, $parent = '', $indentation = 0, $changed = FALSE) { + $this->assertEquals($id, $row->find('hidden_field_selector', ['hidden_field', "table[$id][id]"])->getValue()); + $this->assertEquals($parent, $row->find('hidden_field_selector', ['hidden_field', "table[$id][parent]"])->getValue()); + $this->assertEquals($weight, $row->findField("table[$id][weight]")->getValue()); + $this->assertCount($indentation, $row->findAll('css', '.js-indentation.indentation')); + // A row is marked as changed when the related markup is present. + $this->assertEquals($changed, !empty($row->find('css', 'abbr.tabledrag-changed'))); + } + + /** + * Finds a row in the test table by the row ID. + * + * @param string $id + * The ID of the row. + * + * @return \Behat\Mink\Element\NodeElement|mixed|null + * The row element. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * Thrown when the row is not found. + */ + protected function findRowById($id) { + $xpath = "//table[@id='tabledrag-test-table']/tbody/tr[.//input[@name='table[$id][id]']]"; + $row = $this->getSession()->getPage()->find('xpath', $xpath); + + if (!$row) { + throw new ElementNotFoundException($this->getSession(), 'row'); + } + + return $row; + } + + /** + * Moves a row through the keyboard. + * + * @param \Behat\Mink\Element\NodeElement $row + * The row to move. + * @param string $arrow + * The arrow button to use to move the row. Either one of 'left', 'right', + * 'up' or 'down'. + * @param int $repeat + * How many times to press the arrow button specified. + */ + protected function moveRowWithKeyboard(NodeElement $row, $arrow, $repeat = 1) { + // Convert the arrow name to the browser key event code. + switch ($arrow) { + case 'left': + $key = 37; + break; + + case 'right': + $key = 39; + break; + + case 'up': + $key = 38; + break; + + case 'down': + $key = 40; + } + + if (is_null($key)) { + throw new \InvalidArgumentException('The direction parameter must be one of "left", "right", "up" or "down".'); + } + + $handle = $row->find('xpath', '//a[@class="tabledrag-handle"]'); + $handle->focus(); + + for ($i = 0; $i < $repeat; $i++) { + $handle->keyDown($key); + $handle->keyUp($key); + } + + $handle->blur(); + + // Give some time for the DOM manipulation. + $this->getSession()->wait(500); + } + +}