Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<html>
<head>
<title>Test for input event of text editor</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css"
href="/tests/SimpleTest/test.css" />
</head>
<body>
<div id="display">
<input type="text" id="input">
<textarea id="textarea"></textarea>
</div>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
<script class="testbody" type="application/javascript">
"use strict";
SimpleTest.waitForExplicitFinish();
SimpleTest.expectAssertions(0, 1); // In a11y module
SimpleTest.waitForFocus(runTests, window);
function runTests() {
const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word");
function doTests(aElement, aDescription, aIsTextarea) {
aDescription += ": ";
aElement.focus();
aElement.value = "";
/**
* Tester function.
*
* @param aTestData Class like object to run a set of tests.
* - action:
* Short explanation what it does.
* - cancelBeforeInput:
* true if preventDefault() of "beforeinput" should be
* called.
* @param aFunc Function to run test.
* @param aExpected Object which has:
* - value [optional]:
* Set string value if the test needs to check value of
* aElement.
* Set undefined if the test does not need to check it.
* - valueForCanceled [optional]:
* Set string value if canceling "beforeinput" does not
* keep the value before calling aFunc.
* - beforeInputEvent [optional]:
* Set object which has `cancelable`, `inputType` and `data`
* if a "beforeinput" event should be fired.
* Set null if "beforeinput" event shouldn't be fired.
* - inputEvent [optional]:
* Set object which has `inputType` and `data` if an "input" event
* should be fired if aTestData.cancelBeforeInput is not true.
* Set null if "input" event shouldn't be fired.
* Note that if expected "beforeinput" event is cancelable and
* aTestData.cancelBeforeInput is true, this is ignored.
*/
function runTest(aTestData, aFunc, aExpected) {
let initializing = false;
let beforeInputEvent = null;
let inputEvent = null;
let beforeInputHandler = (aEvent) => {
if (initializing) {
return;
}
ok(!beforeInputEvent,
`${aDescription}Multiple "beforeinput" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
if (aTestData.cancelBeforeInput) {
aEvent.preventDefault();
}
ok(aEvent.isTrusted,
`${aDescription}"beforeinput" event at ${aTestData.action} must be trusted`);
is(aEvent.target, aElement,
`${aDescription}"beforeinput" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
ok(aEvent instanceof InputEvent,
`${aDescription}"beforeinput" event at ${aTestData.action} should be dispatched with InputEvent interface`);
ok(aEvent.bubbles,
`${aDescription}"beforeinput" event at ${aTestData.action} must be bubbles`);
beforeInputEvent = aEvent;
};
let inputHandler = (aEvent) => {
if (initializing) {
return;
}
ok(!inputEvent,
`${aDescription}Multiple "input" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`);
ok(aEvent.isTrusted,
`${aDescription}"input" event at ${aTestData.action} must be trusted`);
is(aEvent.target, aElement, `"input" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`);
ok(aEvent instanceof InputEvent,
`${aDescription}"input" event at ${aTestData.action} should be dispatched with InputEvent interface`);
ok(!aEvent.cancelable,
`${aDescription}"input" event at ${aTestData.action} must not be cancelable`);
ok(aEvent.bubbles,
`${aDescription}"input" event at ${aTestData.action} must be bubbles`);
let duration = Math.abs(window.performance.now() - aEvent.timeStamp);
ok(duration < 30 * 1000,
`${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` +
`the current time but it differed by ${duration}ms)`);
inputEvent = aEvent;
};
if (aTestData.cancelBeforeInput &&
(aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined)) {
ok(false,
`${aDescription}cancelBeforeInput must not be true for ${aTestData.action} because "beforeinput" event is not expected`);
return;
}
try {
aElement.addEventListener("beforeinput", beforeInputHandler, true);
aElement.addEventListener("input", inputHandler, true);
let initialValue = aElement.value;
aFunc();
(function verify() {
try {
if (aExpected.value !== undefined) {
if (aTestData.cancelBeforeInput && aExpected.valueForCanceled === undefined) {
is(aElement.value, initialValue,
`${aDescription}the value should be "${initialValue}" after ${aTestData.action}`);
} else {
let expectedValue =
aTestData.cancelBeforeInput ? aExpected.valueForCanceled : aExpected.value;
is(aElement.value, expectedValue,
`${aDescription}the value should be "${expectedValue}" after ${aTestData.action}`);
}
}
if (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined) {
ok(!beforeInputEvent,
`${aDescription}"beforeinput" event shouldn't have been fired at ${aTestData.action}`);
} else {
ok(beforeInputEvent,
`${aDescription}"beforeinput" event should've been fired at ${aTestData.action}`);
is(beforeInputEvent.cancelable, aExpected.beforeInputEvent.cancelable,
`${aDescription}"beforeinput" event by ${aTestData.action} should be ${
aExpected.beforeInputEvent.cancelable ? "cancelable" : "not cancelable"
}`);
is(beforeInputEvent.inputType, aExpected.beforeInputEvent.inputType,
`${aDescription}inputType of "beforeinput" event by ${aTestData.action} should be "${aExpected.beforeInputEvent.inputType}"`);
is(beforeInputEvent.data, aExpected.beforeInputEvent.data,
`${aDescription}data of "beforeinput" event by ${aTestData.action} should be ${
aExpected.beforeInputEvent.data === null ? "null" : `"${aExpected.beforeInputEvent.data}"`
}`);
is(beforeInputEvent.dataTransfer, null,
`${aDescription}dataTransfer of "beforeinput" event by ${aTestData.action} should be null`);
is(beforeInputEvent.getTargetRanges().length, 0,
`${aDescription}getTargetRanges() of "beforeinput" event by ${aTestData.action} should return empty array`);
}
if ((
aTestData.cancelBeforeInput === true &&
aExpected.beforeInputEvent &&
aExpected.beforeInputEvent.cancelable
) || aExpected.inputEvent === null || aExpected.inputEvent === undefined) {
ok(!inputEvent,
`${aDescription}"input" event shouldn't have been fired at ${aTestData.action}`);
} else {
ok(inputEvent,
`${aDescription}"input" event should've been fired at ${aTestData.action}`);
is(inputEvent.cancelable, false,
`${aDescription}"input" event by ${aTestData.action} should be not be cancelable`);
is(inputEvent.inputType, aExpected.inputEvent.inputType,
`${aDescription}inputType of "input" event by ${aTestData.action} should be "${aExpected.inputEvent.inputType}"`);
is(inputEvent.data, aExpected.inputEvent.data,
`${aDescription}data of "input" event by ${aTestData.action} should be ${
aExpected.inputEvent.data === null ? "null" : `"${aExpected.inputEvent.data}"`
}`);
is(inputEvent.dataTransfer, null,
`${aDescription}dataTransfer of "input" event by ${aTestData.action} should be null`);
is(inputEvent.getTargetRanges().length, 0,
`${aDescription}getTargetRanges() of "input" event by ${aTestData.action} should return empty array`);
}
} catch (ex) {
ok(false, `${aDescription}unexpected exception at verifying test result of "${aTestData.action}": ${ex.toString()}`);
}
})();
} finally {
aElement.removeEventListener("beforeinput", beforeInputHandler, true);
aElement.removeEventListener("input", inputHandler, true);
}
}
function test_typing_a_in_empty_editor(aTestData) {
aElement.value = "";
aElement.focus();
runTest(
aTestData,
() => {
sendString("a");
},
{
value: "a",
beforeInputEvent: {
cancelable: true,
inputType: "insertText",
data: "a",
},
inputEvent: {
inputType: "insertText",
data: "a",
},
}
);
}
test_typing_a_in_empty_editor({
action: 'typing "a" and canceling beforeinput',
cancelBeforeInput: true,
});
test_typing_a_in_empty_editor({
action: 'typing "a"',
cancelBeforeInput: false,
});
function test_typing_backspace_to_delete_last_character(aTestData) {
aElement.value = "a";
aElement.focus();
aElement.setSelectionStart = "a".length;
runTest(
aTestData,
() => {
synthesizeKey("KEY_Backspace");
},
{
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentBackward",
data: null,
},
inputEvent: {
inputType: "deleteContentBackward",
data: null,
},
}
);
}
test_typing_backspace_to_delete_last_character({
actin: 'typing "Backspace" to delete "a" and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_typing_backspace_to_delete_last_character({
actin: 'typing "Backspace" to delete "a"',
cancelBeforeInput: false,
});
function test_typing_enter_in_empty_editor(aTestData) {
aElement.value = "";
aElement.focus();
runTest(
aTestData,
() => {
synthesizeKey("KEY_Enter");
},
aIsTextarea
? {
value: "\n",
beforeInputEvent: {
cancelable: true,
inputType: "insertLineBreak",
data: null,
},
inputEvent: {
inputType: "insertLineBreak",
data: null,
},
}
: {
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "insertLineBreak",
data: null,
},
}
);
}
test_typing_enter_in_empty_editor({
action: 'typing "Enter" in empty editor and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_typing_enter_in_empty_editor({
action: 'typing "Enter" in empty editor',
cancelBeforeInput: false,
});
(function test_setting_value(aTestData) {
aElement.value = "";
aElement.focus();
runTest(
aTestData,
() => {
aElement.value = "foo-bar";
},
{ value: "foo-bar" }
);
})({
action: "setting non-empty value",
});
(function test_setting_empty_value(aTestData) {
aElement.value = "foo-bar";
aElement.focus();
runTest(
aTestData,
() => {
aElement.value = "";
},
{ value: "" }
);
})({
action: "setting empty value",
});
(function test_typing_space_in_empty_editor(aTestData) {
aElement.value = "";
aElement.focus();
runTest(
aTestData,
() => {
sendString(" ");
},
{
value: " ",
beforeInputEvent: {
cancelable: true,
inputType: "insertText",
data: " ",
},
inputEvent: {
inputType: "insertText",
data: " ",
},
}
);
})({
action: "typing space",
});
(function test_typing_delete_at_end_of_editor(aTestData) {
aElement.value = " ";
aElement.focus();
runTest(
aTestData,
() => {
synthesizeKey("KEY_Delete");
},
{
value: " ",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentForward",
data: null,
},
}
);
})({
action: 'typing "Delete" at end of editor',
});
(function test_typing_arrow_left_to_move_caret(aTestData) {
aElement.value = " ";
aElement.focus();
aElement.selectionStart = 1;
runTest(
aTestData,
() => {
synthesizeKey("KEY_ArrowLeft");
},
{ value: " " }
);
})({
action: 'typing "ArrowLeft" to move caret',
});
function test_typing_delete_to_delete_last_character(aTestData) {
aElement.value = " ";
aElement.focus();
aElement.selectionStart = 0;
runTest(
aTestData,
() => {
synthesizeKey("KEY_Delete");
},
{
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentForward",
data: null,
},
inputEvent: {
inputType: "deleteContentForward",
data: null,
},
}
);
}
test_typing_delete_to_delete_last_character({
action: 'typing "Delete" to delete space and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_typing_delete_to_delete_last_character({
action: 'typing "Delete" to delete space',
cancelBeforeInput: false,
});
function test_undoing_deleting_last_character(aTestData) {
aElement.value = "a";
aElement.focus();
aElement.selectionStart = 0;
synthesizeKey("KEY_Delete");
runTest(
aTestData,
() => {
synthesizeKey("z", {accelKey: true});
},
{
value: "a",
beforeInputEvent: {
cancelable: true,
inputType: "historyUndo",
data: null,
},
inputEvent: {
inputType: "historyUndo",
data: null,
},
}
);
}
test_undoing_deleting_last_character({
action: 'undoing deleting last character and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_undoing_deleting_last_character({
action: "undoing deleting last character",
cancelBeforeInput: false,
});
(function test_undoing_without_undoable_transaction(aTestData) {
aElement.value = "a";
aElement.focus();
aElement.selectionStart = 0;
synthesizeKey("KEY_Delete");
synthesizeKey("z", {accelKey: true});
runTest(
aTestData,
() => {
synthesizeKey("z", {accelKey: true});
},
{ value: "a" }
);
})({
action: "trying to undo without undoable transaction"
});
function test_redoing_deleting_last_character(aTestData) {
aElement.value = "a";
aElement.focus();
aElement.selectionStart = 0;
synthesizeKey("KEY_Delete");
synthesizeKey("z", {accelKey: true});
runTest(
aTestData,
() => {
synthesizeKey("Z", {accelKey: true, shiftKey: true});
},
{
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "historyRedo",
data: null,
},
inputEvent: {
inputType: "historyRedo",
data: null,
},
}
);
}
test_redoing_deleting_last_character({
action: 'redoing deleting last character and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_redoing_deleting_last_character({
action: "redoing deleting last character",
cancelBeforeInput: false,
});
(function test_redoing_without_redoable_transaction(aTestData) {
aElement.value = "a";
aElement.focus();
aElement.selectionStart = 0;
synthesizeKey("KEY_Delete");
synthesizeKey("z", {accelKey: true});
synthesizeKey("Z", {accelKey: true, shiftKey: true});
runTest(
aTestData,
() => {
synthesizeKey("Z", {accelKey: true, shiftKey: true});
},
{ value: "" }
);
})({
action: "trying to redo without redoable transaction"
});
function test_typing_backspace_with_selecting_all_characters(aTestData) {
aElement.value = "abc";
aElement.focus();
aElement.select();
runTest(
aTestData,
() => {
synthesizeKey("KEY_Backspace");
},
{
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentBackward",
data: null,
},
inputEvent: {
inputType: "deleteContentBackward",
data: null,
},
}
);
}
test_typing_backspace_with_selecting_all_characters({
action: 'typing "Backspace" to delete all selected characters and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_typing_backspace_with_selecting_all_characters({
action: 'typing "Backspace" to delete all selected characters',
cancelBeforeInput: false,
});
function test_typing_delete_with_selecting_all_characters(aTestData) {
aElement.value = "abc";
aElement.focus();
aElement.select();
runTest(
aTestData,
() => {
synthesizeKey("KEY_Delete");
},
{
value: "",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentForward",
data: null,
},
inputEvent: {
inputType: "deleteContentForward",
data: null,
},
}
);
}
test_typing_delete_with_selecting_all_characters({
action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_typing_delete_with_selecting_all_characters({
action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"',
cancelBeforeInput: false,
});
function test_deleting_word_backward_from_its_end(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("abc def".length, "abc def".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
},
{
value: "abc ",
beforeInputEvent: {
cancelable: true,
inputType: "deleteWordBackward",
data: null,
},
inputEvent: {
inputType: "deleteWordBackward",
data: null,
},
}
);
}
test_deleting_word_backward_from_its_end({
action: 'deleting word backward from its end and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_deleting_word_backward_from_its_end({
action: 'deleting word backward from its end',
cancelBeforeInput: false,
});
function test_deleting_word_forward_from_its_start(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange(0, 0);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteWordForward");
},
{
value: kWordSelectEatSpaceToNextWord ? "def" : " def",
beforeInputEvent: {
cancelable: true,
inputType: "deleteWordForward",
data: null,
},
inputEvent: {
inputType: "deleteWordForward",
data: null,
},
}
);
}
test_deleting_word_forward_from_its_start({
action: 'deleting word forward from its start and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_deleting_word_forward_from_its_start({
action: "deleting word forward from its start",
cancelBeforeInput: false,
});
(function test_deleting_word_backward_from_middle_of_second_word(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("abc d".length, "abc de".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteWordBackward");
},
{
value: "abc df",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentBackward",
data: null,
},
inputEvent: {
inputType: "deleteContentBackward",
data: null,
},
}
);
})({
action: "removing characters backward from middle of second word",
});
(function test_deleting_word_forward_from_middle_of_first_word(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("a".length, "ab".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteWordForward");
},
{
value: "ac def",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentForward",
data: null,
},
inputEvent: {
inputType: "deleteContentForward",
data: null,
},
}
);
})({
action: "removing characters forward from middle of first word",
});
(function test_deleting_characters_backward_to_start_of_line(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("abc d".length, "abc d".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
},
{
value: "ef",
beforeInputEvent: {
cancelable: true,
inputType: "deleteSoftLineBackward",
data: null,
},
inputEvent: {
inputType: "deleteSoftLineBackward",
data: null,
},
}
);
})({
action: "removing characters backward to start of line"
});
(function test_deleting_characters_forward_to_end_of_line(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("ab".length, "ab".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
},
{
value: "ab",
beforeInputEvent: {
cancelable: true,
inputType: "deleteSoftLineForward",
data: null,
},
inputEvent: {
inputType: "deleteSoftLineForward",
data: null,
},
}
);
})({
action: "removing characters forward to end of line",
});
(function test_deleting_characters_backward_to_start_of_line_with_non_collapsed_selection(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("abc d".length, "abc_de".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine");
},
{
value: "abc df",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentBackward",
data: null,
},
inputEvent: {
inputType: "deleteContentBackward",
data: null,
},
}
);
})({
action: "removing characters backward to start of line (with selection in second word)",
});
(function test_deleting_characters_forward_to_end_of_line_with_non_collapsed_selection(aTestData) {
aElement.value = "abc def";
aElement.focus();
document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug.
aElement.setSelectionRange("a".length, "ab".length);
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine");
},
{
value: "ac def",
beforeInputEvent: {
cancelable: true,
inputType: "deleteContentForward",
data: null,
},
inputEvent: {
inputType: "deleteContentForward",
data: null,
},
}
);
})({
action: "removing characters forward to end of line (with selection in second word)",
});
function test_switching_text_direction_from_default(aTestData) {
try {
aElement.removeAttribute("dir");
aElement.scrollTop; // XXX Update the root frame
aElement.focus();
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_switchTextDirection");
if (aTestData.cancelBeforeInput) {
is(aElement.getAttribute("dir"), null,
`${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`);
} else {
is(aElement.getAttribute("dir"), "rtl",
`${aDescription}dir attribute of the element should've been set to "rtl" by ${aTestData.action}`);
}
},
{
beforeInputEvent: {
cancelable: true,
inputType: "formatSetBlockTextDirection",
data: "rtl",
},
inputEvent: {
inputType: "formatSetBlockTextDirection",
data: "rtl",
},
}
);
} finally {
aElement.removeAttribute("dir");
aElement.scrollTop; // XXX Update the root frame
}
}
test_switching_text_direction_from_default({
action: 'switching text direction from default to "rtl" and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_switching_text_direction_from_default({
action: 'switching text direction from default to "rtl"',
cancelBeforeInput: false,
});
function test_switching_text_direction_from_rtl_to_ltr(aTestData) {
try {
aElement.setAttribute("dir", "rtl");
aElement.scrollTop; // XXX Update the root frame
aElement.focus();
runTest(
aTestData,
() => {
SpecialPowers.doCommand(window, "cmd_switchTextDirection");
let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr";
is(aElement.getAttribute("dir"), expectedDirValue,
`${aDescription}dir attribute of the element should be "${expectedDirValue}" after ${aTestData.action}`);
},
{
beforeInputEvent: {
cancelable: true,
inputType: "formatSetBlockTextDirection",
data: "ltr",
},
inputEvent: {
inputType: "formatSetBlockTextDirection",
data: "ltr",
},
}
);
} finally {
aElement.removeAttribute("dir");
aElement.scrollTop; // XXX Update the root frame
}
}
test_switching_text_direction_from_rtl_to_ltr({
action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
cancelBeforeInput: true,
});
test_switching_text_direction_from_rtl_to_ltr({
action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"',
cancelBeforeInput: false,
});
}
doTests(document.getElementById("input"), "<input type=\"text\">", false);
doTests(document.getElementById("textarea"), "<textarea>", true);
SimpleTest.finish();
}
</script>
</body>
</html>