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"/>
11 * A small font-size, so that the loaded document fits on the screens of all
17 * Helps fitting the tables on the screens of all test devices.
19 div[
class=
"tableContainer"] {
20 display: inline-block;
24 const kEditabilityModeContenteditable
= "contenteditable";
25 const kEditabilityModeDesignMode
= "designMode";
27 // All column names of the test-tables used below.
28 const kColumns
= ["c1", "c2", "c3"];
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";
35 const kTableTagName
= "TABLE";
36 const kTbodyTagName
= "TBODY";
37 const kTheadTagName
= "THEAD";
38 const kTfootTagName
= "TFOOT";
40 const kInputEventType
= "input";
41 const kInputEventInputTypeInsertFromPaste
= "insertFromPaste";
43 // Where a table is pasted to in the test.
44 const kTargetElementId
= "targetElement";
47 * @param aTableName see Test::constructor::aTableName.
48 * @param aRowsInTable see Test::constructor::aRowsInTable.
49 * @return an array of elements of aRowsInTable.
51 function FilterRowsWithParentTag(aTableName
, aRowsInTable
, aTagName
) {
52 return aRowsInTable
.filter(rowName
=> document
.getElementById(aTableName
+
53 rowName
).parentElement
.tagName
== aTagName
);
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
61 - have exactly `kColumns.length` columns per row.
62 - have an id of the form <tableName><rowName> for each table row.
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`.
73 constructor(aTableName
, aRowsInTable
, aEditabilityMode
, aSelectionMode
) {
74 ok(aEditabilityMode
== kEditabilityModeContenteditable
||
75 aEditabilityMode
== kEditabilityModeDesignMode
,
76 "Editablity mode is valid.");
78 ok(aSelectionMode
== kSelectionModeClickSelection
||
79 aSelectionMode
== kSelectionModeDragSelection
,
80 "Selection mode is valid.");
82 this._tableName
= aTableName
;
83 this._rowsInTable
= aRowsInTable
;
84 this._editabilityMode
= aEditabilityMode
;
85 this._selectionMode
= aSelectionMode
;
86 this._innerHTMLOfTargetBeforeTestRun
=
87 document
.getElementById(kTargetElementId
).innerHTML
;
89 if (this._editabilityMode
== kEditabilityModeDesignMode
) {
90 this._removeContenteditableAttributeOfTarget();
91 document
.designMode
= "on";
94 SimpleTest
.info("Constructed the test (" + this._toString() + ").");
98 * Call `_restoreStateOfDocumentBeforeRun` afterwards.
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();
106 if (this._selectionMode
== kSelectionModeDragSelection
) {
107 this._dragSelectAllCellsInRowsOfTable();
109 this._clickSelectAllCellsInRowsOfTable();
112 await
this._copyToClipboard(expectedPastedHTML
);
113 this._pasteToTargetElement();
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.");
121 // Linebreaks and whitespace after tags are irrelevant, hence stripping
123 is(SimpleTest
.stripLinebreaksAndWhitespaceAfterTags(
124 targetElement
.children
[0]?.outerHTML
), expectedPastedHTML
,
125 "Pasted table (" + this._toString() + ") has expected outerHTML.");
128 _restoreStateOfDocumentBeforeRun() {
129 if (this._editabilityMode
== kEditabilityModeDesignMode
) {
130 document
.designMode
= "off";
131 this._setContenteditableAttributeOfTarget();
134 const targetElement
= document
.getElementById(kTargetElementId
);
135 targetElement
.innerHTML
= this._innerHTMLOfTargetBeforeTestRun
;
136 targetElement
.getBoundingClientRect();
139 "Restored the state of the document before the test run.");
143 return "table: " + this._tableName
+ "; row(s): " +
144 this._rowsInTable
.toString() + "; editability-mode: " +
145 this._editabilityMode
+ "; selection-mode: " + this._selectionMode
;
148 _removeContenteditableAttributeOfTarget() {
149 const targetElement
= document
.getElementById(kTargetElementId
);
150 SimpleTest
.info("Removing target's 'contenteditable' attribute.");
151 targetElement
.removeAttribute("contenteditable");
154 _setContenteditableAttributeOfTarget() {
155 const targetElement
= document
.getElementById(kTargetElementId
);
156 SimpleTest
.info("Setting 'contenteditable' attribute of target.");
157 targetElement
.setAttribute("contenteditable", "");
160 _getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(aElementId
) {
161 const outerHTML
= document
.getElementById(aElementId
).outerHTML
;
162 return SimpleTest
.stripLinebreaksAndWhitespaceAfterTags(outerHTML
);
165 _createExpectedOuterHTMLOfTable() {
166 const rowsInTableHead
= FilterRowsWithParentTag(this._tableName
,
167 this._rowsInTable
, kTheadTagName
);
169 const rowsInTableBody
= FilterRowsWithParentTag(this._tableName
,
170 this._rowsInTable
, kTbodyTagName
);
172 const rowsInTableFoot
= FilterRowsWithParentTag(this._tableName
,
173 this._rowsInTable
, kTfootTagName
);
175 let expectedTableOuterHTML
= '\
178 if (rowsInTableHead
.length
) {
179 expectedTableOuterHTML
+= '\
181 rowsInTableHead
.forEach(rowName
=>
182 expectedTableOuterHTML
+=
183 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
184 this._tableName
+ rowName
));
185 expectedTableOuterHTML
+='\
189 if (rowsInTableBody
.length
) {
190 expectedTableOuterHTML
+= '\
193 rowsInTableBody
.forEach(rowName
=>
194 expectedTableOuterHTML
+=
195 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
196 this._tableName
+ rowName
));
198 expectedTableOuterHTML
+='\
202 if (rowsInTableFoot
.length
) {
203 expectedTableOuterHTML
+= '\
205 rowsInTableFoot
.forEach(rowName
=>
206 expectedTableOuterHTML
+=
207 this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(this._tableName
209 expectedTableOuterHTML
+= '\
213 expectedTableOuterHTML
+= '\
216 return expectedTableOuterHTML
;
219 _clickSelectAllCellsInRowsOfTable() {
220 function synthesizeAccelKeyAndClickAt(aElementId
) {
221 const element
= document
.getElementById(aElementId
);
222 synthesizeMouseAtCenter(element
, { accelKey
: true });
225 this._rowsInTable
.forEach(rowName
=> kColumns
.forEach(column
=>
226 synthesizeAccelKeyAndClickAt(this._tableName
+ rowName
+ column
)));
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]);
235 synthesizeMouse(firstColumnOfFirstRow
, 0 /* aOffsetX */,
236 0 /* aOffsetY */, { type
: "mousedown" } /* aEvent */);
238 const rectOfLastColumnOfLastRow
=
239 lastColumnOfLastRow
.getBoundingClientRect();
241 synthesizeMouse(lastColumnOfLastRow
, rectOfLastColumnOfLastRow
.width
242 /* aOffsetX */, rectOfLastColumnOfLastRow
.height
/* aOffsetY */,
243 { type
: "mousemove" } /* aEvent */);
245 synthesizeMouse(lastColumnOfLastRow
, rectOfLastColumnOfLastRow
.width
246 /* aOffsetX */, rectOfLastColumnOfLastRow
.height
/* aOffsetY */,
247 { type
: "mouseup" } /* aEvent */);
253 async
_copyToClipboard(aExpectedPastedHTML
) {
254 const flavor
= "text/html";
256 const expectedPastedHTML
= (() => {
257 if (navigator
.platform
.includes(kPlatformWindows
)) {
258 // TODO: ideally, this should be factored out, see bug 1669963.
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
;
265 return aExpectedPastedHTML
;
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
)) {
275 info(`Waiting clipboard data: expected:\n"${
276 SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)
278 SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData)
283 return SimpleTest
.promiseClipboardChange(validatorFn
,
284 () => synthesizeKey("c", { accelKey
: true } /* aEvent*/), flavor
);
287 _pasteToTargetElement() {
288 const editingHost
= (this._editabilityMode
==
289 kEditabilityModeContenteditable
) ?
290 document
.getElementById(kTargetElementId
) :
294 function handleInputEvent(aEvent
) {
295 if (aEvent
.inputType
== kInputEventInputTypeInsertFromPaste
) {
296 editingHost
.removeEventListener(kInputEventType
, handleInputEvent
);
298 'Listened to an "' + kInputEventInputTypeInsertFromPaste
+ '" "'
299 + kInputEventType
+ ' event.');
303 editingHost
.addEventListener(kInputEventType
, handleInputEvent
);
305 const targetElement
= document
.getElementById(kTargetElementId
);
306 synthesizeMouseAtCenter(targetElement
, {});
307 synthesizeKey("v", { accelKey
: true } /* aEvent */);
310 inputEvent
!= undefined,
311 `An ${kInputEventType} whose "inputType" is ${
312 kInputEventInputTypeInsertFromPaste
313 } should've been fired on ${editingHost.localName}`
318 function ContainsRowWithParentTag(aTableName
, aRowsInTable
, aTagName
) {
319 return !!FilterRowsWithParentTag(aTableName
, aRowsInTable
,
323 function DoesContainRowInTheadAndTbody(aTableName
, aRowsInTable
) {
324 return ContainsRowWithParentTag(aTableName
, aRowsInTable
, kTheadTagName
) &&
325 ContainsRowWithParentTag(aTableName
, aRowsInTable
, kTbodyTagName
);
328 function DoesContainRowInTbodyAndTfoot(aTableName
, aRowsInTable
) {
329 return ContainsRowWithParentTag(aTableName
, aRowsInTable
, kTbodyTagName
)
330 && ContainsRowWithParentTag(aTableName
, aRowsInTable
, kTfootTagName
);
333 async
function runTests() {
334 const kClickSelectionTests
= {
335 selectionMode
: kSelectionModeClickSelection
,
336 tablesToTest
: ["t1", "t2", "t3", "t4", "t5"],
338 ["r1", "r2", "r3", "r4"],
347 const kDragSelectionTests
= {
348 selectionMode
: kSelectionModeDragSelection
,
349 tablesToTest
: ["t1", "t2", "t3", "t4", "t5"],
350 // Only consecutive rows when drag-selecting.
352 ["r1", "r2", "r3", "r4"],
360 const kTestGroups
= [kClickSelectionTests
, kDragSelectionTests
];
362 const kEditabilityModes
= [
363 kEditabilityModeContenteditable
,
364 kEditabilityModeDesignMode
,
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
)) {
374 'Rows to select (' + rowsToSelect
.toString() + ') contains ' +
375 ' row in <tbody> and <thead> or <tfoot> of table "' +
376 tableName
+ '", see bug 1667786.');
380 const test
= new Test(tableName
, rowsToSelect
, editabilityMode
,
381 testGroup
.selectionMode
);
385 ok(false, `Aborting the following tests due to unexpected error: ${ex.message}`);
389 test
._restoreStateOfDocumentBeforeRun();
399 SimpleTest
.waitForExplicitFinish();
400 SimpleTest
.waitForFocus(runTests
);
404 <body onload=
"onLoad()">
406 <h4>Test for
<a href=
"https://bugzilla.mozilla.org/show_bug.cgi?id=1639972">bug
1639972</a></h4>
408 <div class=
"tableContainer">Table with
<code>tbody
</code> and
<code>td
</code>:
412 <td id=
"t1r1c1">r1c1
</td>
413 <td id=
"t1r1c2">r1c2
</td>
414 <td id=
"t1r1c3">r1c3
</td>
417 <td id=
"t1r2c1">r2c1
</td>
418 <td id=
"t1r2c2">r2c2
</td>
419 <td id=
"t1r2c3">r2c3
</td>
422 <td id=
"t1r3c1">r3c1
</td>
423 <td id=
"t1r3c2">r3c2
</td>
424 <td id=
"t1r3c3">r3c3
</td>
427 <td id=
"t1r4c1">r4c1
</td>
428 <td id=
"t1r4c2">r4c2
</td>
429 <td id=
"t1r4c3">r4c3
</td>
435 <div class=
"tableContainer">Table with
<code>tbody
</code>,
<code>td
</code> and
<code>th
</code>:
439 <th id=
"t2r1c1">r1c1
</th>
440 <th id=
"t2r1c2">r1c2
</th>
441 <th id=
"t2r1c3">r1c3
</th>
444 <td id=
"t2r2c1">r2c1
</td>
445 <td id=
"t2r2c2">r2c2
</td>
446 <td id=
"t2r2c3">r2c3
</td>
449 <td id=
"t2r3c1">r3c1
</td>
450 <td id=
"t2r3c2">r3c2
</td>
451 <td id=
"t2r3c3">r3c3
</td>
454 <td id=
"t2r4c1">r4c1
</td>
455 <td id=
"t2r4c2">r4c2
</td>
456 <td id=
"t2r4c3">r4c3
</td>
462 <div class=
"tableContainer">Table with
<code>thead
</code>,
<code>tbody
</code>,
<code>td
</code>:
466 <td id=
"t3r1c1">r1c1
</td>
467 <td id=
"t3r1c2">r1c2
</td>
468 <td id=
"t3r1c3">r1c3
</td>
473 <td id=
"t3r2c1">r2c1
</td>
474 <td id=
"t3r2c2">r2c2
</td>
475 <td id=
"t3r2c3">r2c3
</td>
478 <td id=
"t3r3c1">r3c1
</td>
479 <td id=
"t3r3c2">r3c2
</td>
480 <td id=
"t3r3c3">r3c3
</td>
483 <td id=
"t3r4c1">r4c1
</td>
484 <td id=
"t3r4c2">r4c2
</td>
485 <td id=
"t3r4c3">r4c3
</td>
491 <div class=
"tableContainer">Table with
<code>thead
</code>,
<code>tbody
</code>,
<code>td
</code> and
<code>th
</code>:
495 <th id=
"t4r1c1">r1c1
</th>
496 <th id=
"t4r1c2">r1c2
</th>
497 <th id=
"t4r1c3">r1c3
</th>
502 <td id=
"t4r2c1">r2c1
</td>
503 <td id=
"t4r2c2">r2c2
</td>
504 <td id=
"t4r2c3">r2c3
</td>
507 <td id=
"t4r3c1">r3c1
</td>
508 <td id=
"t4r3c2">r3c2
</td>
509 <td id=
"t4r3c3">r3c3
</td>
512 <td id=
"t4r4c1">r4c1
</td>
513 <td id=
"t4r4c2">r4c2
</td>
514 <td id=
"t4r4c3">r4c3
</td>
519 <div class=
"tableContainer">Table with
<code>thead
</code>,
520 <code>tbody
</code>,
<code>tfoot
</code>, and
<code>td
</code>:
524 <td id=
"t5r1c1">r1c1
</td>
525 <td id=
"t5r1c2">r1c2
</td>
526 <td id=
"t5r1c3">r1c3
</td>
531 <td id=
"t5r2c1">r2c1
</td>
532 <td id=
"t5r2c2">r2c2
</td>
533 <td id=
"t5r2c3">r2c3
</td>
536 <td id=
"t5r3c1">r3c1
</td>
537 <td id=
"t5r3c2">r3c2
</td>
538 <td id=
"t5r3c3">r3c3
</td>
543 <td id=
"t5r4c1">r4c1
</td>
544 <td id=
"t5r4c2">r4c2
</td>
545 <td id=
"t5r4c3">r4c3
</td>
550 <p>Target for pasting:
551 <div id=
"targetElement" contenteditable
><!-- Some content so that it can be clicked on. -->X
</div>