tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>