editor/libeditor/tests/test_pasting_table_rows.html
author Ryan VanderMeulen <rvandermeulen@mozilla.com>
Wed, 16 Jul 2025 03:22:15 +0300 (7 hours ago)
changeset 796707 3862f7d3e5c36c9d28f4b44ec4f530d43e7bcc8a
parent 628299 6e230842cdd404e1c89ad3d09b44415ba880aa68
permissions -rw-r--r--
Bug 1969980 - part 3.75 - Remove snackbar dependency from failing test. r=gmalekpour Differential Revision: https://phabricator.services.mozilla.com/D257412
<!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>