summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/tests/test_pasting_table_rows.html
diff options
context:
space:
mode:
Diffstat (limited to 'editor/libeditor/tests/test_pasting_table_rows.html')
-rw-r--r--editor/libeditor/tests/test_pasting_table_rows.html554
1 files changed, 554 insertions, 0 deletions
diff --git a/editor/libeditor/tests/test_pasting_table_rows.html b/editor/libeditor/tests/test_pasting_table_rows.html
new file mode 100644
index 0000000000..328fb0b297
--- /dev/null
+++ b/editor/libeditor/tests/test_pasting_table_rows.html
@@ -0,0 +1,554 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test pasting table rows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <style>
+ /**
+ * A small font-size, so that the loaded document fits on the screens of all
+ * test devices.
+ */
+ * { font-size: 8px; }
+
+ /**
+ * Helps fitting the tables on the screens of all test devices.
+ */
+ div[class="tableContainer"] {
+ display: inline-block;
+ }
+ </style>
+ <script>
+ const kEditabilityModeContenteditable = "contenteditable";
+ const kEditabilityModeDesignMode = "designMode";
+
+ // All column names of the test-tables used below.
+ const kColumns = ["c1", "c2", "c3"];
+
+ // Ctrl+click on table cells to select them.
+ const kSelectionModeClickSelection = "click-selection";
+ // Click and drag from the first given row to the end of the last given row.
+ const kSelectionModeDragSelection = "drag-selection";
+
+ const kTableTagName = "TABLE";
+ const kTbodyTagName = "TBODY";
+ const kTheadTagName = "THEAD";
+ const kTfootTagName = "TFOOT";
+
+ const kInputEventType = "input";
+ const kInputEventInputTypeInsertFromPaste = "insertFromPaste";
+
+ // Where a table is pasted to in the test.
+ const kTargetElementId = "targetElement";
+
+ /**
+ * @param aTableName see Test::constructor::aTableName.
+ * @param aRowsInTable see Test::constructor::aRowsInTable.
+ * @return an array of elements of aRowsInTable.
+ */
+ function FilterRowsWithParentTag(aTableName, aRowsInTable, aTagName) {
+ return aRowsInTable.filter(rowName => document.getElementById(aTableName +
+ rowName).parentElement.tagName == aTagName);
+ }
+
+ /**
+ * Tables used with this class are required to:
+ * - have ids of the following form for each table cell:
+ <tableName><rowName><column>. Where <column> has to be one of
+ `kColumns`.
+ - have exactly `kColumns.length` columns per row.
+ - have an id of the form <tableName><rowName> for each table row.
+ */
+ class Test {
+ /**
+ * @param aTableName indicates which table to operate on.
+ * @param aRowsInTable an array of row names. Ordered from top to bottom.
+ * @param aEditabilityMode `kEditabilityModeContenteditable` or
+ * `kEditabilityModeDesignMode`.
+ * @param aSelectionMode `kSelectionModeClickSelection` or
+ * `kSelectionModeDragSelection`.
+ */
+ constructor(aTableName, aRowsInTable, aEditabilityMode, aSelectionMode) {
+ ok(aEditabilityMode == kEditabilityModeContenteditable ||
+ aEditabilityMode == kEditabilityModeDesignMode,
+ "Editablity mode is valid.");
+
+ ok(aSelectionMode == kSelectionModeClickSelection ||
+ aSelectionMode == kSelectionModeDragSelection,
+ "Selection mode is valid.");
+
+ this._tableName = aTableName;
+ this._rowsInTable = aRowsInTable;
+ this._editabilityMode = aEditabilityMode;
+ this._selectionMode = aSelectionMode;
+ this._innerHTMLOfTargetBeforeTestRun =
+ document.getElementById(kTargetElementId).innerHTML;
+
+ if (this._editabilityMode == kEditabilityModeDesignMode) {
+ this._removeContenteditableAttributeOfTarget();
+ document.designMode = "on";
+ }
+
+ SimpleTest.info("Constructed the test (" + this._toString() + ").");
+ }
+
+ /**
+ * Call `_restoreStateOfDocumentBeforeRun` afterwards.
+ */
+ async _run() {
+ // Generate the expected pasted HTML before pasting the clipboard's
+ // content, because that may duplicate ids, hence leading to creating
+ // a wrong expectation string.
+ const expectedPastedHTML = this._createExpectedOuterHTMLOfTable();
+
+ if (this._selectionMode == kSelectionModeDragSelection) {
+ this._dragSelectAllCellsInRowsOfTable();
+ } else {
+ this._clickSelectAllCellsInRowsOfTable();
+ }
+
+ await this._copyToClipboard(expectedPastedHTML);
+ this._pasteToTargetElement();
+
+ const targetElement = document.getElementById(kTargetElementId);
+ is(targetElement.children.length, 1,
+ "Target element has exactly one child.");
+ is(targetElement.children[0]?.tagName, kTableTagName,
+ "Target element has a table child.");
+
+ // Linebreaks and whitespace after tags are irrelevant, hence stripping
+ // them.
+ is(SimpleTest.stripLinebreaksAndWhitespaceAfterTags(
+ targetElement.children[0]?.outerHTML), expectedPastedHTML,
+ "Pasted table (" + this._toString() + ") has expected outerHTML.");
+ }
+
+ _restoreStateOfDocumentBeforeRun() {
+ if (this._editabilityMode == kEditabilityModeDesignMode) {
+ document.designMode = "off";
+ this._setContenteditableAttributeOfTarget();
+ }
+
+ const targetElement = document.getElementById(kTargetElementId);
+ targetElement.innerHTML = this._innerHTMLOfTargetBeforeTestRun;
+ targetElement.getBoundingClientRect();
+
+ SimpleTest.info(
+ "Restored the state of the document before the test run.");
+ }
+
+ _toString() {
+ return "table: " + this._tableName + "; row(s): " +
+ this._rowsInTable.toString() + "; editability-mode: " +
+ this._editabilityMode + "; selection-mode: " + this._selectionMode;
+ }
+
+ _removeContenteditableAttributeOfTarget() {
+ const targetElement = document.getElementById(kTargetElementId);
+ SimpleTest.info("Removing target's 'contenteditable' attribute.");
+ targetElement.removeAttribute("contenteditable");
+ }
+
+ _setContenteditableAttributeOfTarget() {
+ const targetElement = document.getElementById(kTargetElementId);
+ SimpleTest.info("Setting 'contenteditable' attribute of target.");
+ targetElement.setAttribute("contenteditable", "");
+ }
+
+ _getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(aElementId) {
+ const outerHTML = document.getElementById(aElementId).outerHTML;
+ return SimpleTest.stripLinebreaksAndWhitespaceAfterTags(outerHTML);
+ }
+
+ _createExpectedOuterHTMLOfTable() {
+ const rowsInTableHead = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTheadTagName);
+
+ const rowsInTableBody = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTbodyTagName);
+
+ const rowsInTableFoot = FilterRowsWithParentTag(this._tableName,
+ this._rowsInTable, kTfootTagName);
+
+ let expectedTableOuterHTML = '\
+<table>';
+
+ if (rowsInTableHead.length) {
+ expectedTableOuterHTML += '\
+<thead>';
+ rowsInTableHead.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
+ this._tableName + rowName));
+ expectedTableOuterHTML +='\
+</thead>';
+ }
+
+ if (rowsInTableBody.length) {
+ expectedTableOuterHTML += '\
+<tbody>';
+
+ rowsInTableBody.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(
+ this._tableName + rowName));
+
+ expectedTableOuterHTML +='\
+</tbody>';
+ }
+
+ if (rowsInTableFoot.length) {
+ expectedTableOuterHTML += '\
+<tfoot>';
+ rowsInTableFoot.forEach(rowName =>
+ expectedTableOuterHTML +=
+ this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(this._tableName
+ + rowName));
+ expectedTableOuterHTML += '\
+</tfoot>';
+ }
+
+ expectedTableOuterHTML += '\
+</table>';
+
+ return expectedTableOuterHTML;
+ }
+
+ _clickSelectAllCellsInRowsOfTable() {
+ function synthesizeAccelKeyAndClickAt(aElementId) {
+ const element = document.getElementById(aElementId);
+ synthesizeMouseAtCenter(element, { accelKey: true });
+ }
+
+ this._rowsInTable.forEach(rowName => kColumns.forEach(column =>
+ synthesizeAccelKeyAndClickAt(this._tableName + rowName + column)));
+ }
+
+ _dragSelectAllCellsInRowsOfTable() {
+ const firstColumnOfFirstRow = document.getElementById(this._tableName +
+ this._rowsInTable[0] + kColumns[0]);
+ const lastColumnOfLastRow = document.getElementById(this._tableName +
+ this._rowsInTable.slice(-1)[0] + kColumns.slice(-1)[0]);
+
+ synthesizeMouse(firstColumnOfFirstRow, 0 /* aOffsetX */,
+ 0 /* aOffsetY */, { type: "mousedown" } /* aEvent */);
+
+ const rectOfLastColumnOfLastRow =
+ lastColumnOfLastRow.getBoundingClientRect();
+
+ synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width
+ /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */,
+ { type: "mousemove" } /* aEvent */);
+
+ synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width
+ /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */,
+ { type: "mouseup" } /* aEvent */);
+ }
+
+ /**
+ * @return a promise.
+ */
+ async _copyToClipboard(aExpectedPastedHTML) {
+ const flavor = "text/html";
+
+ const expectedPastedHTML = (() => {
+ if (navigator.platform.includes(kPlatformWindows)) {
+ // TODO: ideally, this should be factored out, see bug 1669963.
+
+ // Windows wraps the pasted HTML, see
+ // https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842.
+ return kTextHtmlPrefixClipboardDataWindows +
+ aExpectedPastedHTML + kTextHtmlSuffixClipboardDataWindows;
+ }
+ return aExpectedPastedHTML;
+ })();
+
+ function validatorFn(aData) {
+ // The data's format doesn't specify whether there should be line
+ // breaks or whitspace between tags. Hence, remove them.
+ if (SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) ==
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)) {
+ return true;
+ }
+ info(`Waiting clipboard data: expected:\n"${
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)
+ }"\n, but got:\n"${
+ SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData)
+ }"`);
+ return false;
+ }
+
+ return SimpleTest.promiseClipboardChange(validatorFn,
+ () => synthesizeKey("c", { accelKey: true } /* aEvent*/), flavor);
+ }
+
+ _pasteToTargetElement() {
+ const editingHost = (this._editabilityMode ==
+ kEditabilityModeContenteditable) ?
+ document.getElementById(kTargetElementId) :
+ document;
+
+ let inputEvent;
+ function handleInputEvent(aEvent) {
+ if (aEvent.inputType == kInputEventInputTypeInsertFromPaste) {
+ editingHost.removeEventListener(kInputEventType, handleInputEvent);
+ SimpleTest.info(
+ 'Listened to an "' + kInputEventInputTypeInsertFromPaste + '" "'
+ + kInputEventType + ' event.');
+ inputEvent = aEvent;
+ }
+ }
+ editingHost.addEventListener(kInputEventType, handleInputEvent);
+
+ const targetElement = document.getElementById(kTargetElementId);
+ synthesizeMouseAtCenter(targetElement, {});
+ synthesizeKey("v", { accelKey: true } /* aEvent */);
+
+ ok(
+ inputEvent != undefined,
+ `An ${kInputEventType} whose "inputType" is ${
+ kInputEventInputTypeInsertFromPaste
+ } should've been fired on ${editingHost.localName}`
+ );
+ }
+ }
+
+ function ContainsRowWithParentTag(aTableName, aRowsInTable, aTagName) {
+ return !!FilterRowsWithParentTag(aTableName, aRowsInTable,
+ aTagName).length;
+ }
+
+ function DoesContainRowInTheadAndTbody(aTableName, aRowsInTable) {
+ return ContainsRowWithParentTag(aTableName, aRowsInTable, kTheadTagName) &&
+ ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName);
+ }
+
+ function DoesContainRowInTbodyAndTfoot(aTableName, aRowsInTable) {
+ return ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName)
+ && ContainsRowWithParentTag(aTableName, aRowsInTable, kTfootTagName);
+ }
+
+ async function runTests() {
+ const kClickSelectionTests = {
+ selectionMode : kSelectionModeClickSelection,
+ tablesToTest : ["t1", "t2", "t3", "t4", "t5"],
+ rowsToSelect : [
+ ["r1", "r2", "r3", "r4"],
+ ["r1"],
+ ["r2", "r3"],
+ ["r1", "r3"],
+ ["r3", "r4"],
+ ["r4"],
+ ],
+ };
+
+ const kDragSelectionTests = {
+ selectionMode : kSelectionModeDragSelection,
+ tablesToTest : ["t1", "t2", "t3", "t4", "t5"],
+ // Only consecutive rows when drag-selecting.
+ rowsToSelect : [
+ ["r1", "r2", "r3", "r4"],
+ ["r1"],
+ ["r2", "r3"],
+ ["r3", "r4"],
+ ["r4"],
+ ],
+ };
+
+ const kTestGroups = [kClickSelectionTests, kDragSelectionTests];
+
+ const kEditabilityModes = [
+ kEditabilityModeContenteditable,
+ kEditabilityModeDesignMode,
+ ];
+
+ for (const editabilityMode of kEditabilityModes) {
+ for (const testGroup of kTestGroups) {
+ for (const tableName of testGroup.tablesToTest) {
+ for (const rowsToSelect of testGroup.rowsToSelect) {
+ if (DoesContainRowInTheadAndTbody(tableName, rowsToSelect) ||
+ DoesContainRowInTbodyAndTfoot(tableName, rowsToSelect)) {
+ todo(false,
+ 'Rows to select (' + rowsToSelect.toString() + ') contains ' +
+ ' row in <tbody> and <thead> or <tfoot> of table "' +
+ tableName + '", see bug 1667786.');
+ continue;
+ }
+
+ const test = new Test(tableName, rowsToSelect, editabilityMode,
+ testGroup.selectionMode);
+ try {
+ await test._run();
+ } catch (ex) {
+ ok(false, `Aborting the following tests due to unexpected error: ${ex.message}`);
+ SimpleTest.finish();
+ return;
+ }
+ test._restoreStateOfDocumentBeforeRun();
+ }
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+ function onLoad() {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(runTests);
+ }
+ </script>
+</head>
+<body onload="onLoad()">
+<p id="display"></p>
+ <h4>Test for <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1639972">bug 1639972</a></h4>
+ <div id="content">
+ <div class="tableContainer">Table with <code>tbody</code> and <code>td</code>:
+ <table>
+ <tbody>
+ <tr id="t1r1">
+ <td id="t1r1c1">r1c1</td>
+ <td id="t1r1c2">r1c2</td>
+ <td id="t1r1c3">r1c3</td>
+ </tr>
+ <tr id="t1r2">
+ <td id="t1r2c1">r2c1</td>
+ <td id="t1r2c2">r2c2</td>
+ <td id="t1r2c3">r2c3</td>
+ </tr>
+ <tr id="t1r3">
+ <td id="t1r3c1">r3c1</td>
+ <td id="t1r3c2">r3c2</td>
+ <td id="t1r3c3">r3c3</td>
+ </tr>
+ <tr id="t1r4">
+ <td id="t1r4c1">r4c1</td>
+ <td id="t1r4c2">r4c2</td>
+ <td id="t1r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>tbody</code>, <code>td</code> and <code>th</code>:
+ <table>
+ <tbody>
+ <tr id="t2r1">
+ <th id="t2r1c1">r1c1</th>
+ <th id="t2r1c2">r1c2</th>
+ <th id="t2r1c3">r1c3</th>
+ </tr>
+ <tr id="t2r2">
+ <td id="t2r2c1">r2c1</td>
+ <td id="t2r2c2">r2c2</td>
+ <td id="t2r2c3">r2c3</td>
+ </tr>
+ <tr id="t2r3">
+ <td id="t2r3c1">r3c1</td>
+ <td id="t2r3c2">r3c2</td>
+ <td id="t2r3c3">r3c3</td>
+ </tr>
+ <tr id="t2r4">
+ <td id="t2r4c1">r4c1</td>
+ <td id="t2r4c2">r4c2</td>
+ <td id="t2r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code>:
+ <table>
+ <thead>
+ <tr id="t3r1">
+ <td id="t3r1c1">r1c1</td>
+ <td id="t3r1c2">r1c2</td>
+ <td id="t3r1c3">r1c3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t3r2">
+ <td id="t3r2c1">r2c1</td>
+ <td id="t3r2c2">r2c2</td>
+ <td id="t3r2c3">r2c3</td>
+ </tr>
+ <tr id="t3r3">
+ <td id="t3r3c1">r3c1</td>
+ <td id="t3r3c2">r3c2</td>
+ <td id="t3r3c3">r3c3</td>
+ </tr>
+ <tr id="t3r4">
+ <td id="t3r4c1">r4c1</td>
+ <td id="t3r4c2">r4c2</td>
+ <td id="t3r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code> and <code>th</code>:
+ <table>
+ <thead>
+ <tr id="t4r1">
+ <th id="t4r1c1">r1c1</th>
+ <th id="t4r1c2">r1c2</th>
+ <th id="t4r1c3">r1c3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t4r2">
+ <td id="t4r2c1">r2c1</td>
+ <td id="t4r2c2">r2c2</td>
+ <td id="t4r2c3">r2c3</td>
+ </tr>
+ <tr id="t4r3">
+ <td id="t4r3c1">r3c1</td>
+ <td id="t4r3c2">r3c2</td>
+ <td id="t4r3c3">r3c3</td>
+ </tr>
+ <tr id="t4r4">
+ <td id="t4r4c1">r4c1</td>
+ <td id="t4r4c2">r4c2</td>
+ <td id="t4r4c3">r4c3</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="tableContainer">Table with <code>thead</code>,
+ <code>tbody</code>, <code>tfoot</code>, and <code>td</code>:
+ <table>
+ <thead>
+ <tr id="t5r1">
+ <td id="t5r1c1">r1c1</td>
+ <td id="t5r1c2">r1c2</td>
+ <td id="t5r1c3">r1c3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="t5r2">
+ <td id="t5r2c1">r2c1</td>
+ <td id="t5r2c2">r2c2</td>
+ <td id="t5r2c3">r2c3</td>
+ </tr>
+ <tr id="t5r3">
+ <td id="t5r3c1">r3c1</td>
+ <td id="t5r3c2">r3c2</td>
+ <td id="t5r3c3">r3c3</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr id="t5r4">
+ <td id="t5r4c1">r4c1</td>
+ <td id="t5r4c2">r4c2</td>
+ <td id="t5r4c3">r4c3</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ <p>Target for pasting:
+ <div id="targetElement" contenteditable><!-- Some content so that it can be clicked on. -->X</div>
+ </p>
+ </div>
+</html>