Commit 6d1721c1 authored by Eitan Isaacson's avatar Eitan Isaacson
Browse files

Bug 1832353 - P2: Convert editable text tests to browser tests. r=Jamie

parent e90263d6
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ support-files =
prefs =
  javascript.options.asyncstack_capture_debuggee_only=false

[browser_editabletext.js]
[browser_text.js]
[browser_text_caret.js]
[browser_text_paragraph_boundary.js]
+173 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

async function testEditable(browser, acc, aBefore = "", aAfter = "") {
  async function resetInput() {
    if (acc.childCount <= 1) {
      return;
    }

    let emptyInputEvent = waitForEvent(EVENT_TEXT_VALUE_CHANGE, "input");
    await invokeContentTask(browser, [], async () => {
      content.document.getElementById("input").innerHTML = "";
    });

    await emptyInputEvent;
  }

  // ////////////////////////////////////////////////////////////////////////
  // insertText
  await testInsertText(acc, "hello", 0, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
  await testInsertText(acc, "ma ", 0, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "ma hello", aAfter]);
  await testInsertText(acc, "ma", 2, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "mama hello", aAfter]);
  await testInsertText(acc, " hello", 10, aBefore.length);
  await isFinalValueCorrect(browser, acc, [
    aBefore,
    "mama hello hello",
    aAfter,
  ]);

  // ////////////////////////////////////////////////////////////////////////
  // deleteText
  await testDeleteText(acc, 0, 5, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hello hello", aAfter]);
  await testDeleteText(acc, 5, 6, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hellohello", aAfter]);
  await testDeleteText(acc, 5, 10, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
  await testDeleteText(acc, 0, 5, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);

  // XXX: clipboard operation tests don't work well with editable documents.
  if (acc.role == ROLE_DOCUMENT) {
    return;
  }

  await resetInput();

  // copyText and pasteText
  await testInsertText(acc, "hello", 0, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);

  await testCopyText(acc, 0, 1, aBefore.length, browser, "h");
  await testPasteText(acc, 1, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hhello", aAfter]);

  await testCopyText(acc, 5, 6, aBefore.length, browser, "o");
  await testPasteText(acc, 6, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hhelloo", aAfter]);

  await testCopyText(acc, 2, 3, aBefore.length, browser, "e");
  await testPasteText(acc, 1, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "hehelloo", aAfter]);

  // cut & paste
  await testCutText(acc, 0, 1, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "ehelloo", aAfter]);
  await testPasteText(acc, 2, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "ehhelloo", aAfter]);

  await testCutText(acc, 3, 4, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloo", aAfter]);
  await testPasteText(acc, 6, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloeo", aAfter]);

  await testCutText(acc, 0, 8, aBefore.length);
  await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);

  await resetInput();

  // ////////////////////////////////////////////////////////////////////////
  // setTextContents
  await testSetTextContents(acc, "hello", aBefore.length, [
    EVENT_TEXT_INSERTED,
  ]);
  await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
  await testSetTextContents(acc, "katze", aBefore.length, [
    EVENT_TEXT_REMOVED,
    EVENT_TEXT_INSERTED,
  ]);
  await isFinalValueCorrect(browser, acc, [aBefore, "katze", aAfter]);
}

addAccessibleTask(
  `<input id="input"/>`,
  async function (browser, docAcc) {
    await testEditable(browser, findAccessibleChildByID(docAcc, "input"));
  },
  { chrome: true, topLevel: true }
);

addAccessibleTask(
  `<style>
  #input::after {
    content: "pseudo element";
  }
</style>
<div id="input" contenteditable="true" role="textbox"></div>`,
  async function (browser, docAcc) {
    await testEditable(
      browser,
      findAccessibleChildByID(docAcc, "input"),
      "",
      "pseudo element"
    );
  },
  { chrome: true, topLevel: false /* bug 1834129 */ }
);

addAccessibleTask(
  `<style>
  #input::before {
    content: "pseudo element";
  }
</style>
<div id="input" contenteditable="true" role="textbox"></div>`,
  async function (browser, docAcc) {
    await testEditable(
      browser,
      findAccessibleChildByID(docAcc, "input"),
      "pseudo element"
    );
  },
  { chrome: true, topLevel: false /* bug 1834129 */ }
);

addAccessibleTask(
  `<style>
  #input::before {
    content: "before";
  }
  #input::after {
    content: "after";
  }
</style>
<div id="input" contenteditable="true" role="textbox"></div>`,
  async function (browser, docAcc) {
    await testEditable(
      browser,
      findAccessibleChildByID(docAcc, "input"),
      "before",
      "after"
    );
  },
  { chrome: true, topLevel: false /* bug 1834129 */ }
);

addAccessibleTask(
  ``,
  async function (browser, docAcc) {
    await testEditable(browser, docAcc);
  },
  {
    chrome: true,
    topLevel: true,
    contentDocBodyAttrs: { contentEditable: "true" },
  }
);
+144 −1
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@
/* exported createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS,
 BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN,
 BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER,
 readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence */
 readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence,
 isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText,
 testCopyText, testPasteText, testCutText, testSetTextContents */

// Load the shared-head file first.
Services.scriptloader.loadSubScript(
@@ -17,9 +19,13 @@ Services.scriptloader.loadSubScript(

// Loading and common.js from accessible/tests/mochitest/ for all tests, as
// well as promisified-events.js.

/* import-globals-from ../../mochitest/role.js */

loadScripts(
  { name: "common.js", dir: MOCHITESTS_DIR },
  { name: "text.js", dir: MOCHITESTS_DIR },
  { name: "role.js", dir: MOCHITESTS_DIR },
  { name: "promisified-events.js", dir: MOCHITESTS_DIR }
);

@@ -131,3 +137,140 @@ function testBoundarySequence(
    msg
  );
}

///////////////////////////////////////////////////////////////////////////////
// Editable text

async function waitForCopy(browser) {
  await BrowserTestUtils.waitForContentEvent(browser, "copy", false, evt => {
    return true;
  });

  let clipboardData = await invokeContentTask(browser, [], async () => {
    let text = await content.navigator.clipboard.readText();
    return text;
  });

  return clipboardData;
}

async function isFinalValueCorrect(
  browser,
  acc,
  expectedTextLeafs,
  msg = "Value is correct"
) {
  let value =
    acc.role == ROLE_ENTRY
      ? acc.value
      : await invokeContentTask(browser, [], () => {
          return content.document.body.textContent;
        });

  let [before, text, after] = expectedTextLeafs;
  let finalValue =
    before && after && !text
      ? [before, after].join(" ")
      : [before, text, after].join("");

  is(value.replace("\xa0", " "), finalValue, msg);
}

function waitForTextChangeEvents(acc, eventSeq) {
  let events = eventSeq.map(eventType => {
    return [eventType, acc];
  });

  if (acc.role == ROLE_ENTRY) {
    events.push([EVENT_TEXT_VALUE_CHANGE, acc]);
  }

  return waitForEvents(events);
}

async function testSetTextContents(acc, text, staticContentOffset, events) {
  acc.QueryInterface(nsIAccessibleEditableText);
  let evtPromise = waitForTextChangeEvents(acc, events);
  acc.setTextContents(text);
  let evt = (await evtPromise)[0];
  evt.QueryInterface(nsIAccessibleTextChangeEvent);
  is(evt.start, staticContentOffset);
}

async function testInsertText(
  acc,
  textToInsert,
  insertOffset,
  staticContentOffset
) {
  acc.QueryInterface(nsIAccessibleEditableText);

  let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
  acc.insertText(textToInsert, staticContentOffset + insertOffset);
  let evt = (await evtPromise)[0];
  evt.QueryInterface(nsIAccessibleTextChangeEvent);
  is(evt.start, staticContentOffset + insertOffset);
}

async function testDeleteText(
  acc,
  startOffset,
  endOffset,
  staticContentOffset
) {
  acc.QueryInterface(nsIAccessibleEditableText);

  let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
  acc.deleteText(
    staticContentOffset + startOffset,
    staticContentOffset + endOffset
  );
  let evt = (await evtPromise)[0];
  evt.QueryInterface(nsIAccessibleTextChangeEvent);
  is(evt.start, staticContentOffset + startOffset);
}

async function testCopyText(
  acc,
  startOffset,
  endOffset,
  staticContentOffset,
  browser,
  aExpectedClipboard = null
) {
  acc.QueryInterface(nsIAccessibleEditableText);
  let copied = waitForCopy(browser);
  acc.copyText(
    staticContentOffset + startOffset,
    staticContentOffset + endOffset
  );
  let clipboardText = await copied;
  if (aExpectedClipboard != null) {
    is(clipboardText, aExpectedClipboard, "Correct text in clipboard");
  }
}

async function testPasteText(acc, insertOffset, staticContentOffset) {
  acc.QueryInterface(nsIAccessibleEditableText);
  let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
  acc.pasteText(staticContentOffset + insertOffset);

  let evt = (await evtPromise)[0];
  evt.QueryInterface(nsIAccessibleTextChangeEvent);
  // XXX: In non-headless mode pasting text produces several text leaves
  // and the offset is not what we expect.
  // is(evt.start, staticContentOffset + insertOffset);
}

async function testCutText(acc, startOffset, endOffset, staticContentOffset) {
  acc.QueryInterface(nsIAccessibleEditableText);
  let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
  acc.cutText(
    staticContentOffset + startOffset,
    staticContentOffset + endOffset
  );

  let evt = (await evtPromise)[0];
  evt.QueryInterface(nsIAccessibleTextChangeEvent);
  is(evt.start, staticContentOffset + startOffset);
}
+0 −7
Original line number Diff line number Diff line
[DEFAULT]
support-files =
  editabletext.js
  !/accessible/tests/mochitest/*.js

[test_1.html]
[test_2.html]
+0 −409
Original line number Diff line number Diff line
/* import-globals-from ../common.js */
/* import-globals-from ../events.js */

/**
 * Perform all editable text tests.
 */
function editableTextTestRun() {
  this.add = function add(aTest) {
    this.seq.push(aTest);
  };

  this.run = function run() {
    this.iterate();
  };

  this.index = 0;
  this.seq = [];

  this.iterate = function iterate() {
    if (this.index < this.seq.length) {
      this.seq[this.index++].startTest(this);
      return;
    }

    this.seq = null;
    SimpleTest.finish();
  };
}

/**
 * Used to test nsIEditableTextAccessible methods.
 */
function editableTextTest(aID) {
  /**
   * Schedule a test, the given function with its arguments will be executed
   * when preceding test is complete.
   */
  this.scheduleTest = function scheduleTest(aFunc, ...aFuncArgs) {
    // A data container acts like a dummy invoker, it's never invoked but
    // it's used to generate real invoker when previous invoker was handled.
    var dataContainer = {
      func: aFunc,
      funcArgs: aFuncArgs,
    };
    this.mEventQueue.push(dataContainer);

    if (!this.mEventQueueReady) {
      this.unwrapNextTest();
      this.mEventQueueReady = true;
    }
  };

  /**
   * setTextContents test.
   */
  this.setTextContents = function setTextContents(aValue, aSkipStartOffset) {
    var testID = "setTextContents '" + aValue + "' for " + prettyName(aID);

    function setTextContentsInvoke() {
      dump(`\nsetTextContents '${aValue}'\n`);
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.setTextContents(aValue);
    }

    aSkipStartOffset = aSkipStartOffset || 0;
    var insertTripple = aValue
      ? [aSkipStartOffset, aSkipStartOffset + aValue.length, aValue]
      : null;
    var oldValue = getValue();
    var removeTripple = oldValue
      ? [aSkipStartOffset, aSkipStartOffset + oldValue.length, oldValue]
      : null;

    this.generateTest(
      removeTripple,
      insertTripple,
      setTextContentsInvoke,
      getValueChecker(aValue),
      testID
    );
  };

  /**
   * insertText test.
   */
  this.insertText = function insertText(aStr, aPos, aResStr, aResPos) {
    var testID =
      "insertText '" + aStr + "' at " + aPos + " for " + prettyName(aID);

    function insertTextInvoke() {
      dump(`\ninsertText '${aStr}' at ${aPos} pos\n`);
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.insertText(aStr, aPos);
    }

    var resPos = aResPos != undefined ? aResPos : aPos;
    this.generateTest(
      null,
      [resPos, resPos + aStr.length, aStr],
      insertTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  /**
   * copyText test.
   */
  this.copyText = function copyText(aStartPos, aEndPos, aClipboardStr) {
    var testID =
      "copyText from " +
      aStartPos +
      " to " +
      aEndPos +
      " for " +
      prettyName(aID);

    function copyTextInvoke() {
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.copyText(aStartPos, aEndPos);
    }

    this.generateTest(
      null,
      null,
      copyTextInvoke,
      getClipboardChecker(aClipboardStr),
      testID
    );
  };

  /**
   * copyText and pasteText test.
   */
  this.copyNPasteText = function copyNPasteText(
    aStartPos,
    aEndPos,
    aPos,
    aResStr
  ) {
    var testID =
      "copyText from " +
      aStartPos +
      " to " +
      aEndPos +
      "and pasteText at " +
      aPos +
      " for " +
      prettyName(aID);

    function copyNPasteTextInvoke() {
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.copyText(aStartPos, aEndPos);
      acc.pasteText(aPos);
    }

    this.generateTest(
      null,
      [aStartPos, aEndPos, getTextFromClipboard],
      copyNPasteTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  /**
   * cutText test.
   */
  this.cutText = function cutText(
    aStartPos,
    aEndPos,
    aResStr,
    aResStartPos,
    aResEndPos
  ) {
    var testID =
      "cutText from " +
      aStartPos +
      " to " +
      aEndPos +
      " for " +
      prettyName(aID);

    function cutTextInvoke() {
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.cutText(aStartPos, aEndPos);
    }

    var resStartPos = aResStartPos != undefined ? aResStartPos : aStartPos;
    var resEndPos = aResEndPos != undefined ? aResEndPos : aEndPos;
    this.generateTest(
      [resStartPos, resEndPos, getTextFromClipboard],
      null,
      cutTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  /**
   * cutText and pasteText test.
   */
  this.cutNPasteText = function copyNPasteText(
    aStartPos,
    aEndPos,
    aPos,
    aResStr
  ) {
    var testID =
      "cutText from " +
      aStartPos +
      " to " +
      aEndPos +
      " and pasteText at " +
      aPos +
      " for " +
      prettyName(aID);

    function cutNPasteTextInvoke() {
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.cutText(aStartPos, aEndPos);
      acc.pasteText(aPos);
    }

    this.generateTest(
      [aStartPos, aEndPos, getTextFromClipboard],
      [aPos, -1, getTextFromClipboard],
      cutNPasteTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  /**
   * pasteText test.
   */
  this.pasteText = function pasteText(aPos, aResStr) {
    var testID = "pasteText at " + aPos + " for " + prettyName(aID);

    function pasteTextInvoke() {
      var acc = getAccessible(aID, nsIAccessibleEditableText);
      acc.pasteText(aPos);
    }

    this.generateTest(
      null,
      [aPos, -1, getTextFromClipboard],
      pasteTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  /**
   * deleteText test.
   */
  this.deleteText = function deleteText(aStartPos, aEndPos, aResStr) {
    var testID =
      "deleteText from " +
      aStartPos +
      " to " +
      aEndPos +
      " for " +
      prettyName(aID);

    var oldValue = getValue().substring(aStartPos, aEndPos);
    var removeTripple = oldValue ? [aStartPos, aEndPos, oldValue] : null;

    function deleteTextInvoke() {
      var acc = getAccessible(aID, [nsIAccessibleEditableText]);
      acc.deleteText(aStartPos, aEndPos);
    }

    this.generateTest(
      removeTripple,
      null,
      deleteTextInvoke,
      getValueChecker(aResStr),
      testID
    );
  };

  // ////////////////////////////////////////////////////////////////////////////
  // Implementation details.

  function getValue() {
    var elm = getNode(aID);
    var elmClass = ChromeUtils.getClassName(elm);
    if (elmClass === "HTMLTextAreaElement" || elmClass === "HTMLInputElement") {
      return elm.value;
    }

    if (elmClass === "HTMLDocument") {
      return elm.body.textContent;
    }

    return elm.textContent;
  }

  /**
   * Common checkers.
   */
  function getValueChecker(aValue) {
    var checker = {
      check: function valueChecker_check() {
        is(getValue(), aValue, "Wrong value " + aValue);
      },
    };
    return checker;
  }

  function getClipboardChecker(aText) {
    var checker = {
      check: function clipboardChecker_check() {
        is(getTextFromClipboard(), aText, "Wrong text in clipboard.");
      },
    };
    return checker;
  }

  /**
   * Process next scheduled test.
   */
  this.unwrapNextTest = function unwrapNextTest() {
    var data = this.mEventQueue.mInvokers[this.mEventQueue.mIndex + 1];
    if (data) {
      data.func.apply(this, data.funcArgs);
    }
  };

  /**
   * Used to generate an invoker object for the sheduled test.
   */
  this.generateTest = function generateTest(
    aRemoveTriple,
    aInsertTriple,
    aInvokeFunc,
    aChecker,
    aInvokerID
  ) {
    var et = this;
    var invoker = {
      eventSeq: [],

      invoke: aInvokeFunc,
      finalCheck: function finalCheck() {
        // dumpTree(aID, `'${aID}' tree:`);

        aChecker.check();
        et.unwrapNextTest(); // replace dummy invoker on real invoker object.
      },
      getID: function getID() {
        return aInvokerID;
      },
    };

    if (aRemoveTriple) {
      let checker = new textChangeChecker(
        aID,
        aRemoveTriple[0],
        aRemoveTriple[1],
        aRemoveTriple[2],
        false
      );
      invoker.eventSeq.push(checker);
    }

    if (aInsertTriple) {
      let checker = new textChangeChecker(
        aID,
        aInsertTriple[0],
        aInsertTriple[1],
        aInsertTriple[2],
        true
      );
      invoker.eventSeq.push(checker);
    }

    // Claim that we don't want to fail when no events are expected.
    if (!aRemoveTriple && !aInsertTriple) {
      invoker.noEventsOnAction = true;
    }

    this.mEventQueue.mInvokers[this.mEventQueue.mIndex + 1] = invoker;
  };

  /**
   * Run the tests.
   */
  this.startTest = function startTest(aTestRun) {
    var testRunObj = aTestRun;
    var thisObj = this;
    this.mEventQueue.onFinish = function finishCallback() {
      // Notify textRun object that all tests were finished.
      testRunObj.iterate();

      // Help GC to avoid leaks (refer to aTestRun from local variable, drop
      // onFinish function).
      thisObj.mEventQueue.onFinish = null;

      return DO_NOT_FINISH_TEST;
    };

    this.mEventQueue.invoke();
  };

  this.mEventQueue = new eventQueue();
  this.mEventQueueReady = false;
}
Loading