test_pasting_table_rows.html (19293B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Test pasting table rows</title> 6 <script src="/tests/SimpleTest/SimpleTest.js"></script> 7 <script src="/tests/SimpleTest/EventUtils.js"></script> 8 <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> 9 <style> 10 /** 11 * A small font-size, so that the loaded document fits on the screens of all 12 * test devices. 13 */ 14 * { font-size: 8px; } 15 16 /** 17 * Helps fitting the tables on the screens of all test devices. 18 */ 19 div[class="tableContainer"] { 20 display: inline-block; 21 } 22 </style> 23 <script> 24 const kEditabilityModeContenteditable = "contenteditable"; 25 const kEditabilityModeDesignMode = "designMode"; 26 27 // All column names of the test-tables used below. 28 const kColumns = ["c1", "c2", "c3"]; 29 30 // Ctrl+click on table cells to select them. 31 const kSelectionModeClickSelection = "click-selection"; 32 // Click and drag from the first given row to the end of the last given row. 33 const kSelectionModeDragSelection = "drag-selection"; 34 35 const kTableTagName = "TABLE"; 36 const kTbodyTagName = "TBODY"; 37 const kTheadTagName = "THEAD"; 38 const kTfootTagName = "TFOOT"; 39 40 const kInputEventType = "input"; 41 const kInputEventInputTypeInsertFromPaste = "insertFromPaste"; 42 43 // Where a table is pasted to in the test. 44 const kTargetElementId = "targetElement"; 45 46 /** 47 * @param aTableName see Test::constructor::aTableName. 48 * @param aRowsInTable see Test::constructor::aRowsInTable. 49 * @return an array of elements of aRowsInTable. 50 */ 51 function FilterRowsWithParentTag(aTableName, aRowsInTable, aTagName) { 52 return aRowsInTable.filter(rowName => document.getElementById(aTableName + 53 rowName).parentElement.tagName == aTagName); 54 } 55 56 /** 57 * Tables used with this class are required to: 58 * - have ids of the following form for each table cell: 59 <tableName><rowName><column>. Where <column> has to be one of 60 `kColumns`. 61 - have exactly `kColumns.length` columns per row. 62 - have an id of the form <tableName><rowName> for each table row. 63 */ 64 class Test { 65 /** 66 * @param aTableName indicates which table to operate on. 67 * @param aRowsInTable an array of row names. Ordered from top to bottom. 68 * @param aEditabilityMode `kEditabilityModeContenteditable` or 69 * `kEditabilityModeDesignMode`. 70 * @param aSelectionMode `kSelectionModeClickSelection` or 71 * `kSelectionModeDragSelection`. 72 */ 73 constructor(aTableName, aRowsInTable, aEditabilityMode, aSelectionMode) { 74 ok(aEditabilityMode == kEditabilityModeContenteditable || 75 aEditabilityMode == kEditabilityModeDesignMode, 76 "Editablity mode is valid."); 77 78 ok(aSelectionMode == kSelectionModeClickSelection || 79 aSelectionMode == kSelectionModeDragSelection, 80 "Selection mode is valid."); 81 82 this._tableName = aTableName; 83 this._rowsInTable = aRowsInTable; 84 this._editabilityMode = aEditabilityMode; 85 this._selectionMode = aSelectionMode; 86 this._innerHTMLOfTargetBeforeTestRun = 87 document.getElementById(kTargetElementId).innerHTML; 88 89 if (this._editabilityMode == kEditabilityModeDesignMode) { 90 this._removeContenteditableAttributeOfTarget(); 91 document.designMode = "on"; 92 } 93 94 SimpleTest.info("Constructed the test (" + this._toString() + ")."); 95 } 96 97 /** 98 * Call `_restoreStateOfDocumentBeforeRun` afterwards. 99 */ 100 async _run() { 101 // Generate the expected pasted HTML before pasting the clipboard's 102 // content, because that may duplicate ids, hence leading to creating 103 // a wrong expectation string. 104 const expectedPastedHTML = this._createExpectedOuterHTMLOfTable(); 105 106 if (this._selectionMode == kSelectionModeDragSelection) { 107 this._dragSelectAllCellsInRowsOfTable(); 108 } else { 109 this._clickSelectAllCellsInRowsOfTable(); 110 } 111 112 await this._copyToClipboard(expectedPastedHTML); 113 this._pasteToTargetElement(); 114 115 const targetElement = document.getElementById(kTargetElementId); 116 is(targetElement.children.length, 1, 117 "Target element has exactly one child."); 118 is(targetElement.children[0]?.tagName, kTableTagName, 119 "Target element has a table child."); 120 121 // Linebreaks and whitespace after tags are irrelevant, hence stripping 122 // them. 123 is(SimpleTest.stripLinebreaksAndWhitespaceAfterTags( 124 targetElement.children[0]?.outerHTML), expectedPastedHTML, 125 "Pasted table (" + this._toString() + ") has expected outerHTML."); 126 } 127 128 _restoreStateOfDocumentBeforeRun() { 129 if (this._editabilityMode == kEditabilityModeDesignMode) { 130 document.designMode = "off"; 131 this._setContenteditableAttributeOfTarget(); 132 } 133 134 const targetElement = document.getElementById(kTargetElementId); 135 targetElement.innerHTML = this._innerHTMLOfTargetBeforeTestRun; 136 targetElement.getBoundingClientRect(); 137 138 SimpleTest.info( 139 "Restored the state of the document before the test run."); 140 } 141 142 _toString() { 143 return "table: " + this._tableName + "; row(s): " + 144 this._rowsInTable.toString() + "; editability-mode: " + 145 this._editabilityMode + "; selection-mode: " + this._selectionMode; 146 } 147 148 _removeContenteditableAttributeOfTarget() { 149 const targetElement = document.getElementById(kTargetElementId); 150 SimpleTest.info("Removing target's 'contenteditable' attribute."); 151 targetElement.removeAttribute("contenteditable"); 152 } 153 154 _setContenteditableAttributeOfTarget() { 155 const targetElement = document.getElementById(kTargetElementId); 156 SimpleTest.info("Setting 'contenteditable' attribute of target."); 157 targetElement.setAttribute("contenteditable", ""); 158 } 159 160 _getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(aElementId) { 161 const outerHTML = document.getElementById(aElementId).outerHTML; 162 return SimpleTest.stripLinebreaksAndWhitespaceAfterTags(outerHTML); 163 } 164 165 _createExpectedOuterHTMLOfTable() { 166 const rowsInTableHead = FilterRowsWithParentTag(this._tableName, 167 this._rowsInTable, kTheadTagName); 168 169 const rowsInTableBody = FilterRowsWithParentTag(this._tableName, 170 this._rowsInTable, kTbodyTagName); 171 172 const rowsInTableFoot = FilterRowsWithParentTag(this._tableName, 173 this._rowsInTable, kTfootTagName); 174 175 let expectedTableOuterHTML = '\ 176 <table>'; 177 178 if (rowsInTableHead.length) { 179 expectedTableOuterHTML += '\ 180 <thead>'; 181 rowsInTableHead.forEach(rowName => 182 expectedTableOuterHTML += 183 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags( 184 this._tableName + rowName)); 185 expectedTableOuterHTML +='\ 186 </thead>'; 187 } 188 189 if (rowsInTableBody.length) { 190 expectedTableOuterHTML += '\ 191 <tbody>'; 192 193 rowsInTableBody.forEach(rowName => 194 expectedTableOuterHTML += 195 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags( 196 this._tableName + rowName)); 197 198 expectedTableOuterHTML +='\ 199 </tbody>'; 200 } 201 202 if (rowsInTableFoot.length) { 203 expectedTableOuterHTML += '\ 204 <tfoot>'; 205 rowsInTableFoot.forEach(rowName => 206 expectedTableOuterHTML += 207 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(this._tableName 208 + rowName)); 209 expectedTableOuterHTML += '\ 210 </tfoot>'; 211 } 212 213 expectedTableOuterHTML += '\ 214 </table>'; 215 216 return expectedTableOuterHTML; 217 } 218 219 _clickSelectAllCellsInRowsOfTable() { 220 function synthesizeAccelKeyAndClickAt(aElementId) { 221 const element = document.getElementById(aElementId); 222 synthesizeMouseAtCenter(element, { accelKey: true }); 223 } 224 225 this._rowsInTable.forEach(rowName => kColumns.forEach(column => 226 synthesizeAccelKeyAndClickAt(this._tableName + rowName + column))); 227 } 228 229 _dragSelectAllCellsInRowsOfTable() { 230 const firstColumnOfFirstRow = document.getElementById(this._tableName + 231 this._rowsInTable[0] + kColumns[0]); 232 const lastColumnOfLastRow = document.getElementById(this._tableName + 233 this._rowsInTable.slice(-1)[0] + kColumns.slice(-1)[0]); 234 235 synthesizeMouse(firstColumnOfFirstRow, 0 /* aOffsetX */, 236 0 /* aOffsetY */, { type: "mousedown" } /* aEvent */); 237 238 const rectOfLastColumnOfLastRow = 239 lastColumnOfLastRow.getBoundingClientRect(); 240 241 synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width 242 /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */, 243 { type: "mousemove" } /* aEvent */); 244 245 synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width 246 /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */, 247 { type: "mouseup" } /* aEvent */); 248 } 249 250 /** 251 * @return a promise. 252 */ 253 async _copyToClipboard(aExpectedPastedHTML) { 254 const flavor = "text/html"; 255 256 const expectedPastedHTML = (() => { 257 if (navigator.platform.includes(kPlatformWindows)) { 258 // TODO: ideally, this should be factored out, see bug 1669963. 259 260 // Windows wraps the pasted HTML, see 261 // https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842. 262 return kTextHtmlPrefixClipboardDataWindows + 263 aExpectedPastedHTML + kTextHtmlSuffixClipboardDataWindows; 264 } 265 return aExpectedPastedHTML; 266 })(); 267 268 function validatorFn(aData) { 269 // The data's format doesn't specify whether there should be line 270 // breaks or whitspace between tags. Hence, remove them. 271 if (SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) == 272 SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)) { 273 return true; 274 } 275 info(`Waiting clipboard data: expected:\n"${ 276 SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML) 277 }"\n, but got:\n"${ 278 SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) 279 }"`); 280 return false; 281 } 282 283 return SimpleTest.promiseClipboardChange(validatorFn, 284 () => synthesizeKey("c", { accelKey: true } /* aEvent*/), flavor); 285 } 286 287 _pasteToTargetElement() { 288 const editingHost = (this._editabilityMode == 289 kEditabilityModeContenteditable) ? 290 document.getElementById(kTargetElementId) : 291 document; 292 293 let inputEvent; 294 function handleInputEvent(aEvent) { 295 if (aEvent.inputType == kInputEventInputTypeInsertFromPaste) { 296 editingHost.removeEventListener(kInputEventType, handleInputEvent); 297 SimpleTest.info( 298 'Listened to an "' + kInputEventInputTypeInsertFromPaste + '" "' 299 + kInputEventType + ' event.'); 300 inputEvent = aEvent; 301 } 302 } 303 editingHost.addEventListener(kInputEventType, handleInputEvent); 304 305 const targetElement = document.getElementById(kTargetElementId); 306 synthesizeMouseAtCenter(targetElement, {}); 307 synthesizeKey("v", { accelKey: true } /* aEvent */); 308 309 ok( 310 inputEvent != undefined, 311 `An ${kInputEventType} whose "inputType" is ${ 312 kInputEventInputTypeInsertFromPaste 313 } should've been fired on ${editingHost.localName}` 314 ); 315 } 316 } 317 318 function ContainsRowWithParentTag(aTableName, aRowsInTable, aTagName) { 319 return !!FilterRowsWithParentTag(aTableName, aRowsInTable, 320 aTagName).length; 321 } 322 323 function DoesContainRowInTheadAndTbody(aTableName, aRowsInTable) { 324 return ContainsRowWithParentTag(aTableName, aRowsInTable, kTheadTagName) && 325 ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName); 326 } 327 328 function DoesContainRowInTbodyAndTfoot(aTableName, aRowsInTable) { 329 return ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName) 330 && ContainsRowWithParentTag(aTableName, aRowsInTable, kTfootTagName); 331 } 332 333 async function runTests() { 334 const kClickSelectionTests = { 335 selectionMode : kSelectionModeClickSelection, 336 tablesToTest : ["t1", "t2", "t3", "t4", "t5"], 337 rowsToSelect : [ 338 ["r1", "r2", "r3", "r4"], 339 ["r1"], 340 ["r2", "r3"], 341 ["r1", "r3"], 342 ["r3", "r4"], 343 ["r4"], 344 ], 345 }; 346 347 const kDragSelectionTests = { 348 selectionMode : kSelectionModeDragSelection, 349 tablesToTest : ["t1", "t2", "t3", "t4", "t5"], 350 // Only consecutive rows when drag-selecting. 351 rowsToSelect : [ 352 ["r1", "r2", "r3", "r4"], 353 ["r1"], 354 ["r2", "r3"], 355 ["r3", "r4"], 356 ["r4"], 357 ], 358 }; 359 360 const kTestGroups = [kClickSelectionTests, kDragSelectionTests]; 361 362 const kEditabilityModes = [ 363 kEditabilityModeContenteditable, 364 kEditabilityModeDesignMode, 365 ]; 366 367 for (const editabilityMode of kEditabilityModes) { 368 for (const testGroup of kTestGroups) { 369 for (const tableName of testGroup.tablesToTest) { 370 for (const rowsToSelect of testGroup.rowsToSelect) { 371 if (DoesContainRowInTheadAndTbody(tableName, rowsToSelect) || 372 DoesContainRowInTbodyAndTfoot(tableName, rowsToSelect)) { 373 todo(false, 374 'Rows to select (' + rowsToSelect.toString() + ') contains ' + 375 ' row in <tbody> and <thead> or <tfoot> of table "' + 376 tableName + '", see bug 1667786.'); 377 continue; 378 } 379 380 const test = new Test(tableName, rowsToSelect, editabilityMode, 381 testGroup.selectionMode); 382 try { 383 await test._run(); 384 } catch (ex) { 385 ok(false, `Aborting the following tests due to unexpected error: ${ex.message}`); 386 SimpleTest.finish(); 387 return; 388 } 389 test._restoreStateOfDocumentBeforeRun(); 390 } 391 } 392 } 393 } 394 395 SimpleTest.finish(); 396 } 397 398 function onLoad() { 399 SimpleTest.waitForExplicitFinish(); 400 SimpleTest.waitForFocus(runTests); 401 } 402 </script> 403 </head> 404 <body onload="onLoad()"> 405 <p id="display"></p> 406 <h4>Test for <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1639972">bug 1639972</a></h4> 407 <div id="content"> 408 <div class="tableContainer">Table with <code>tbody</code> and <code>td</code>: 409 <table> 410 <tbody> 411 <tr id="t1r1"> 412 <td id="t1r1c1">r1c1</td> 413 <td id="t1r1c2">r1c2</td> 414 <td id="t1r1c3">r1c3</td> 415 </tr> 416 <tr id="t1r2"> 417 <td id="t1r2c1">r2c1</td> 418 <td id="t1r2c2">r2c2</td> 419 <td id="t1r2c3">r2c3</td> 420 </tr> 421 <tr id="t1r3"> 422 <td id="t1r3c1">r3c1</td> 423 <td id="t1r3c2">r3c2</td> 424 <td id="t1r3c3">r3c3</td> 425 </tr> 426 <tr id="t1r4"> 427 <td id="t1r4c1">r4c1</td> 428 <td id="t1r4c2">r4c2</td> 429 <td id="t1r4c3">r4c3</td> 430 </tr> 431 </tbody> 432 </table> 433 </div> 434 435 <div class="tableContainer">Table with <code>tbody</code>, <code>td</code> and <code>th</code>: 436 <table> 437 <tbody> 438 <tr id="t2r1"> 439 <th id="t2r1c1">r1c1</th> 440 <th id="t2r1c2">r1c2</th> 441 <th id="t2r1c3">r1c3</th> 442 </tr> 443 <tr id="t2r2"> 444 <td id="t2r2c1">r2c1</td> 445 <td id="t2r2c2">r2c2</td> 446 <td id="t2r2c3">r2c3</td> 447 </tr> 448 <tr id="t2r3"> 449 <td id="t2r3c1">r3c1</td> 450 <td id="t2r3c2">r3c2</td> 451 <td id="t2r3c3">r3c3</td> 452 </tr> 453 <tr id="t2r4"> 454 <td id="t2r4c1">r4c1</td> 455 <td id="t2r4c2">r4c2</td> 456 <td id="t2r4c3">r4c3</td> 457 </tr> 458 </tbody> 459 </table> 460 </div> 461 462 <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code>: 463 <table> 464 <thead> 465 <tr id="t3r1"> 466 <td id="t3r1c1">r1c1</td> 467 <td id="t3r1c2">r1c2</td> 468 <td id="t3r1c3">r1c3</td> 469 </tr> 470 </thead> 471 <tbody> 472 <tr id="t3r2"> 473 <td id="t3r2c1">r2c1</td> 474 <td id="t3r2c2">r2c2</td> 475 <td id="t3r2c3">r2c3</td> 476 </tr> 477 <tr id="t3r3"> 478 <td id="t3r3c1">r3c1</td> 479 <td id="t3r3c2">r3c2</td> 480 <td id="t3r3c3">r3c3</td> 481 </tr> 482 <tr id="t3r4"> 483 <td id="t3r4c1">r4c1</td> 484 <td id="t3r4c2">r4c2</td> 485 <td id="t3r4c3">r4c3</td> 486 </tr> 487 </tbody> 488 </table> 489 </div> 490 491 <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code> and <code>th</code>: 492 <table> 493 <thead> 494 <tr id="t4r1"> 495 <th id="t4r1c1">r1c1</th> 496 <th id="t4r1c2">r1c2</th> 497 <th id="t4r1c3">r1c3</th> 498 </tr> 499 </thead> 500 <tbody> 501 <tr id="t4r2"> 502 <td id="t4r2c1">r2c1</td> 503 <td id="t4r2c2">r2c2</td> 504 <td id="t4r2c3">r2c3</td> 505 </tr> 506 <tr id="t4r3"> 507 <td id="t4r3c1">r3c1</td> 508 <td id="t4r3c2">r3c2</td> 509 <td id="t4r3c3">r3c3</td> 510 </tr> 511 <tr id="t4r4"> 512 <td id="t4r4c1">r4c1</td> 513 <td id="t4r4c2">r4c2</td> 514 <td id="t4r4c3">r4c3</td> 515 </tr> 516 </tbody> 517 </table> 518 </div> 519 <div class="tableContainer">Table with <code>thead</code>, 520 <code>tbody</code>, <code>tfoot</code>, and <code>td</code>: 521 <table> 522 <thead> 523 <tr id="t5r1"> 524 <td id="t5r1c1">r1c1</td> 525 <td id="t5r1c2">r1c2</td> 526 <td id="t5r1c3">r1c3</td> 527 </tr> 528 </thead> 529 <tbody> 530 <tr id="t5r2"> 531 <td id="t5r2c1">r2c1</td> 532 <td id="t5r2c2">r2c2</td> 533 <td id="t5r2c3">r2c3</td> 534 </tr> 535 <tr id="t5r3"> 536 <td id="t5r3c1">r3c1</td> 537 <td id="t5r3c2">r3c2</td> 538 <td id="t5r3c3">r3c3</td> 539 </tr> 540 </tbody> 541 <tfoot> 542 <tr id="t5r4"> 543 <td id="t5r4c1">r4c1</td> 544 <td id="t5r4c2">r4c2</td> 545 <td id="t5r4c3">r4c3</td> 546 </tr> 547 </tfoot> 548 </table> 549 </div> 550 <p>Target for pasting: 551 <div id="targetElement" contenteditable><!-- Some content so that it can be clicked on. -->X</div> 552 </p> 553 </div> 554 </html>