annotate core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@17 1 <?php
Chris@17 2
Chris@17 3 namespace Drupal\FunctionalJavascriptTests\TableDrag;
Chris@17 4
Chris@17 5 use Behat\Mink\Element\NodeElement;
Chris@17 6 use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
Chris@17 7
Chris@17 8 /**
Chris@17 9 * Tests draggable table.
Chris@17 10 *
Chris@17 11 * @group javascript
Chris@17 12 */
Chris@17 13 class TableDragTest extends WebDriverTestBase {
Chris@17 14
Chris@17 15 /**
Chris@17 16 * Class used to verify that dragging operations are in execution.
Chris@17 17 */
Chris@17 18 const DRAGGING_CSS_CLASS = 'tabledrag-test-dragging';
Chris@17 19
Chris@17 20 /**
Chris@17 21 * {@inheritdoc}
Chris@17 22 */
Chris@17 23 protected static $modules = ['tabledrag_test'];
Chris@17 24
Chris@17 25 /**
Chris@17 26 * The state service.
Chris@17 27 *
Chris@17 28 * @var \Drupal\Core\State\StateInterface
Chris@17 29 */
Chris@17 30 protected $state;
Chris@17 31
Chris@17 32 /**
Chris@17 33 * {@inheritdoc}
Chris@17 34 */
Chris@17 35 protected function setUp() {
Chris@17 36 parent::setUp();
Chris@17 37
Chris@17 38 $this->state = $this->container->get('state');
Chris@17 39 }
Chris@17 40
Chris@17 41 /**
Chris@17 42 * Tests accessibility through keyboard of the tabledrag functionality.
Chris@17 43 */
Chris@17 44 public function testKeyboardAccessibility() {
Chris@17 45 $this->state->set('tabledrag_test_table', array_flip(range(1, 5)));
Chris@17 46
Chris@17 47 $expected_table = [
Chris@17 48 ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 49 ['id' => 2, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 50 ['id' => 3, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 51 ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 52 ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 53 ];
Chris@17 54 $this->drupalGet('tabledrag_test');
Chris@17 55 $this->assertDraggableTable($expected_table);
Chris@17 56
Chris@17 57 // Nest the row with id 2 as child of row 1.
Chris@17 58 $this->moveRowWithKeyboard($this->findRowById(2), 'right');
Chris@17 59 $expected_table[1] = ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
Chris@17 60 $this->assertDraggableTable($expected_table);
Chris@17 61
Chris@17 62 // Nest the row with id 3 as child of row 1.
Chris@17 63 $this->moveRowWithKeyboard($this->findRowById(3), 'right');
Chris@17 64 $expected_table[2] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
Chris@17 65 $this->assertDraggableTable($expected_table);
Chris@17 66
Chris@17 67 // Nest the row with id 3 as child of row 2.
Chris@17 68 $this->moveRowWithKeyboard($this->findRowById(3), 'right');
Chris@17 69 $expected_table[2] = ['id' => 3, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
Chris@17 70 $this->assertDraggableTable($expected_table);
Chris@17 71
Chris@17 72 // Nesting should be allowed to maximum level 2.
Chris@17 73 $this->moveRowWithKeyboard($this->findRowById(4), 'right', 4);
Chris@17 74 $expected_table[3] = ['id' => 4, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
Chris@17 75 $this->assertDraggableTable($expected_table);
Chris@17 76
Chris@17 77 // Re-order children of row 1.
Chris@17 78 $this->moveRowWithKeyboard($this->findRowById(4), 'up');
Chris@17 79 $expected_table[2] = ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
Chris@17 80 $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE];
Chris@17 81 $this->assertDraggableTable($expected_table);
Chris@17 82
Chris@17 83 // Move back the row 3 to the 1st level.
Chris@17 84 $this->moveRowWithKeyboard($this->findRowById(3), 'left');
Chris@17 85 $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE];
Chris@17 86 $this->assertDraggableTable($expected_table);
Chris@17 87
Chris@17 88 $this->moveRowWithKeyboard($this->findRowById(3), 'left');
Chris@17 89 $expected_table[0] = ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
Chris@17 90 $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
Chris@17 91 $expected_table[4] = ['id' => 5, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
Chris@17 92 $this->assertDraggableTable($expected_table);
Chris@17 93
Chris@17 94 // Move row 3 to the last position.
Chris@17 95 $this->moveRowWithKeyboard($this->findRowById(3), 'down');
Chris@17 96 $expected_table[3] = ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
Chris@17 97 $expected_table[4] = ['id' => 3, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
Chris@17 98 $this->assertDraggableTable($expected_table);
Chris@17 99
Chris@17 100 // Nothing happens when trying to move the last row further down.
Chris@17 101 $this->moveRowWithKeyboard($this->findRowById(3), 'down');
Chris@17 102 $this->assertDraggableTable($expected_table);
Chris@17 103
Chris@17 104 // Nest row 3 under 5. The max depth allowed should be 1.
Chris@17 105 $this->moveRowWithKeyboard($this->findRowById(3), 'right', 3);
Chris@17 106 $expected_table[4] = ['id' => 3, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE];
Chris@17 107 $this->assertDraggableTable($expected_table);
Chris@17 108
Chris@17 109 // The first row of the table cannot be nested.
Chris@17 110 $this->moveRowWithKeyboard($this->findRowById(1), 'right');
Chris@17 111 $this->assertDraggableTable($expected_table);
Chris@17 112
Chris@17 113 // Move a row which has nested children. The children should move with it,
Chris@17 114 // with nesting preserved. Swap the order of the top-level rows by moving
Chris@17 115 // row 1 to after row 3.
Chris@17 116 $this->moveRowWithKeyboard($this->findRowById(1), 'down', 2);
Chris@17 117 $expected_table[0] = ['id' => 5, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE];
Chris@17 118 $expected_table[3] = $expected_table[1];
Chris@17 119 $expected_table[1] = $expected_table[4];
Chris@17 120 $expected_table[4] = $expected_table[2];
Chris@17 121 $expected_table[2] = ['id' => 1, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
Chris@17 122 $this->assertDraggableTable($expected_table);
Chris@17 123 }
Chris@17 124
Chris@17 125 /**
Chris@17 126 * Tests the root and leaf behaviors for rows.
Chris@17 127 */
Chris@17 128 public function testRootLeafDraggableRowsWithKeyboard() {
Chris@17 129 $this->state->set('tabledrag_test_table', [
Chris@17 130 1 => [],
Chris@17 131 2 => ['parent' => 1, 'depth' => 1, 'classes' => ['tabledrag-leaf']],
Chris@17 132 3 => ['parent' => 1, 'depth' => 1],
Chris@17 133 4 => [],
Chris@17 134 5 => ['classes' => ['tabledrag-root']],
Chris@17 135 ]);
Chris@17 136
Chris@17 137 $this->drupalGet('tabledrag_test');
Chris@17 138 $expected_table = [
Chris@17 139 ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 140 ['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE],
Chris@17 141 ['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE],
Chris@17 142 ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 143 ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE],
Chris@17 144 ];
Chris@17 145 $this->assertDraggableTable($expected_table);
Chris@17 146
Chris@17 147 // Rows marked as root cannot be moved as children of another row.
Chris@17 148 $this->moveRowWithKeyboard($this->findRowById(5), 'right');
Chris@17 149 $this->assertDraggableTable($expected_table);
Chris@17 150
Chris@17 151 // Rows marked as leaf cannot have children. Trying to move the row #3
Chris@17 152 // as child of #2 should have no results.
Chris@17 153 $this->moveRowWithKeyboard($this->findRowById(3), 'right');
Chris@17 154 $this->assertDraggableTable($expected_table);
Chris@17 155
Chris@17 156 // Leaf can be still swapped and moved to first level.
Chris@17 157 $this->moveRowWithKeyboard($this->findRowById(2), 'down');
Chris@17 158 $this->moveRowWithKeyboard($this->findRowById(2), 'left');
Chris@17 159 $expected_table[0]['weight'] = -10;
Chris@17 160 $expected_table[1]['id'] = 3;
Chris@17 161 $expected_table[1]['weight'] = -10;
Chris@17 162 $expected_table[2] = ['id' => 2, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE];
Chris@17 163 $expected_table[3]['weight'] = -8;
Chris@17 164 $expected_table[4]['weight'] = -7;
Chris@17 165 $this->assertDraggableTable($expected_table);
Chris@17 166
Chris@17 167 // Root rows can have children.
Chris@17 168 $this->moveRowWithKeyboard($this->findRowById(4), 'down');
Chris@17 169 $this->moveRowWithKeyboard($this->findRowById(4), 'right');
Chris@17 170 $expected_table[3]['id'] = 5;
Chris@17 171 $expected_table[4] = ['id' => 4, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE];
Chris@17 172 $this->assertDraggableTable($expected_table);
Chris@17 173 }
Chris@17 174
Chris@17 175 /**
Chris@17 176 * Tests the warning that appears upon making changes to a tabledrag table.
Chris@17 177 */
Chris@17 178 public function testTableDragChangedWarning() {
Chris@17 179 $this->drupalGet('tabledrag_test');
Chris@17 180
Chris@17 181 // By default no text is visible.
Chris@17 182 $this->assertSession()->pageTextNotContains('You have unsaved changes.');
Chris@17 183 // Try to make a non-allowed action, like moving further down the last row.
Chris@17 184 // No changes happen, so no message should be shown.
Chris@17 185 $this->moveRowWithKeyboard($this->findRowById(5), 'down');
Chris@17 186 $this->assertSession()->pageTextNotContains('You have unsaved changes.');
Chris@17 187
Chris@17 188 // Make a change. The message will appear.
Chris@17 189 $this->moveRowWithKeyboard($this->findRowById(5), 'right');
Chris@17 190 $this->assertSession()->pageTextContainsOnce('You have unsaved changes.');
Chris@17 191
Chris@17 192 // Make another change, the text will stay visible and appear only once.
Chris@17 193 $this->moveRowWithKeyboard($this->findRowById(2), 'up');
Chris@17 194 $this->assertSession()->pageTextContainsOnce('You have unsaved changes.');
Chris@17 195 }
Chris@17 196
Chris@17 197 /**
Chris@17 198 * Asserts the whole structure of the draggable test table.
Chris@17 199 *
Chris@17 200 * @param array $structure
Chris@17 201 * The table structure. Each entry represents a row and consists of:
Chris@17 202 * - id: the expected value for the ID hidden field.
Chris@17 203 * - weight: the expected row weight.
Chris@17 204 * - parent: the expected parent ID for the row.
Chris@17 205 * - indentation: how many indents the row should have.
Chris@17 206 * - changed: whether or not the row should have been marked as changed.
Chris@17 207 */
Chris@17 208 protected function assertDraggableTable(array $structure) {
Chris@17 209 $rows = $this->getSession()->getPage()->findAll('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr');
Chris@17 210 $this->assertSession()->elementsCount('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr', count($structure));
Chris@17 211
Chris@17 212 foreach ($structure as $delta => $expected) {
Chris@17 213 $this->assertTableRow($rows[$delta], $expected['id'], $expected['weight'], $expected['parent'], $expected['indentation'], $expected['changed']);
Chris@17 214 }
Chris@17 215 }
Chris@17 216
Chris@17 217 /**
Chris@17 218 * Asserts the values of a draggable row.
Chris@17 219 *
Chris@17 220 * @param \Behat\Mink\Element\NodeElement $row
Chris@17 221 * The row element to assert.
Chris@17 222 * @param string $id
Chris@17 223 * The expected value for the ID hidden input of the row.
Chris@17 224 * @param int $weight
Chris@17 225 * The expected weight of the row.
Chris@17 226 * @param string $parent
Chris@17 227 * The expected parent ID.
Chris@17 228 * @param int $indentation
Chris@17 229 * The expected indentation of the row.
Chris@17 230 * @param bool $changed
Chris@17 231 * Whether or not the row should have been marked as changed.
Chris@17 232 */
Chris@17 233 protected function assertTableRow(NodeElement $row, $id, $weight, $parent = '', $indentation = 0, $changed = FALSE) {
Chris@17 234 // Assert that the row position is correct by checking that the id
Chris@17 235 // corresponds.
Chris@17 236 $this->assertSession()->hiddenFieldValueEquals("table[$id][id]", $id, $row);
Chris@17 237 $this->assertSession()->hiddenFieldValueEquals("table[$id][parent]", $parent, $row);
Chris@17 238 $this->assertSession()->fieldValueEquals("table[$id][weight]", $weight, $row);
Chris@17 239 $this->assertSession()->elementsCount('css', '.js-indentation.indentation', $indentation, $row);
Chris@17 240 // A row is marked as changed when the related markup is present.
Chris@17 241 $this->assertSession()->elementsCount('css', 'abbr.tabledrag-changed', (int) $changed, $row);
Chris@17 242 }
Chris@17 243
Chris@17 244 /**
Chris@17 245 * Finds a row in the test table by the row ID.
Chris@17 246 *
Chris@17 247 * @param string $id
Chris@17 248 * The ID of the row.
Chris@17 249 *
Chris@17 250 * @return \Behat\Mink\Element\NodeElement
Chris@17 251 * The row element.
Chris@17 252 */
Chris@17 253 protected function findRowById($id) {
Chris@17 254 $xpath = "//table[@id='tabledrag-test-table']/tbody/tr[.//input[@name='table[$id][id]']]";
Chris@17 255 $row = $this->getSession()->getPage()->find('xpath', $xpath);
Chris@17 256 $this->assertNotEmpty($row);
Chris@17 257 return $row;
Chris@17 258 }
Chris@17 259
Chris@17 260 /**
Chris@17 261 * Moves a row through the keyboard.
Chris@17 262 *
Chris@17 263 * @param \Behat\Mink\Element\NodeElement $row
Chris@17 264 * The row to move.
Chris@17 265 * @param string $arrow
Chris@17 266 * The arrow button to use to move the row. Either one of 'left', 'right',
Chris@17 267 * 'up' or 'down'.
Chris@17 268 * @param int $repeat
Chris@17 269 * (optional) How many times to press the arrow button. Defaults to 1.
Chris@17 270 */
Chris@17 271 protected function moveRowWithKeyboard(NodeElement $row, $arrow, $repeat = 1) {
Chris@17 272 $keys = [
Chris@17 273 'left' => 37,
Chris@17 274 'right' => 39,
Chris@17 275 'up' => 38,
Chris@17 276 'down' => 40,
Chris@17 277 ];
Chris@17 278 if (!isset($keys[$arrow])) {
Chris@17 279 throw new \InvalidArgumentException('The arrow parameter must be one of "left", "right", "up" or "down".');
Chris@17 280 }
Chris@17 281
Chris@17 282 $key = $keys[$arrow];
Chris@17 283
Chris@17 284 $handle = $row->find('css', 'a.tabledrag-handle');
Chris@17 285 $handle->focus();
Chris@17 286
Chris@17 287 for ($i = 0; $i < $repeat; $i++) {
Chris@17 288 $this->markRowHandleForDragging($handle);
Chris@17 289 $handle->keyDown($key);
Chris@17 290 $handle->keyUp($key);
Chris@17 291 $this->waitUntilDraggingCompleted($handle);
Chris@17 292 }
Chris@17 293
Chris@17 294 $handle->blur();
Chris@17 295 }
Chris@17 296
Chris@17 297 /**
Chris@17 298 * Marks a row handle for dragging.
Chris@17 299 *
Chris@17 300 * The handle is marked by adding a css class that is removed by an helper
Chris@17 301 * js library once the dragging is over.
Chris@17 302 *
Chris@17 303 * @param \Behat\Mink\Element\NodeElement $handle
Chris@17 304 * The draggable row handle element.
Chris@17 305 *
Chris@17 306 * @throws \Exception
Chris@17 307 * Thrown when the class is not added successfully to the handle.
Chris@17 308 */
Chris@17 309 protected function markRowHandleForDragging(NodeElement $handle) {
Chris@17 310 $class = self::DRAGGING_CSS_CLASS;
Chris@17 311 $script = <<<JS
Chris@17 312 document.evaluate("{$handle->getXpath()}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
Chris@17 313 .singleNodeValue.classList.add('{$class}');
Chris@17 314 JS;
Chris@17 315
Chris@17 316 $this->getSession()->executeScript($script);
Chris@17 317 $has_class = $this->getSession()->getPage()->waitFor(1, function () use ($handle, $class) {
Chris@17 318 return $handle->hasClass($class);
Chris@17 319 });
Chris@17 320
Chris@17 321 if (!$has_class) {
Chris@17 322 throw new \Exception(sprintf('Dragging css class was not added on handle "%s".', $handle->getXpath()));
Chris@17 323 }
Chris@17 324 }
Chris@17 325
Chris@17 326 /**
Chris@17 327 * Waits until the dragging operations are finished on a row handle.
Chris@17 328 *
Chris@17 329 * @param \Behat\Mink\Element\NodeElement $handle
Chris@17 330 * The draggable row handle element.
Chris@17 331 *
Chris@17 332 * @throws \Exception
Chris@17 333 * Thrown when the dragging operations are not completed on time.
Chris@17 334 */
Chris@17 335 protected function waitUntilDraggingCompleted(NodeElement $handle) {
Chris@17 336 $class_removed = $this->getSession()->getPage()->waitFor(1, function () use ($handle) {
Chris@17 337 return !$handle->hasClass($this::DRAGGING_CSS_CLASS);
Chris@17 338 });
Chris@17 339
Chris@17 340 if (!$class_removed) {
Chris@17 341 throw new \Exception(sprintf('Dragging operations did not complete on time on handle %s', $handle->getXpath()));
Chris@17 342 }
Chris@17 343 }
Chris@17 344
Chris@17 345 }