Source code

Revision control

Copy as Markdown

Other Tools

<!doctype html>
<html>
<head>
<title>Testing Selection Events</title>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<div id="normal">
<span id="inner">A bunch of text in a span inside of a div which should be selected</span>
</div>
<div id="ce">
This is a random block of text
</div>
<input type="text" id="input" value="XXXXXXXXXXXXXXXXXXX" width="200"> <br>
<textarea id="textarea" width="200">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</textarea>
<script>
// Call the testing methods from the parent window
var is = parent.is;
var ok = parent.ok;
// spin() spins the event loop for two cycles, giving time for
// selectionchange events to be fired, and handled by our listeners.
function spin() {
return new Promise(function(a) {
parent.SimpleTest.executeSoon(function() {
parent.SimpleTest.executeSoon(a)
});
});
}
/**
* @param {Node} node
*/
function isProperSelectionChangeTarget(node) {
return node === document || node === input || node === textarea;
}
// The main test
parent.add_task(async function() {
await spin();
var selectstart = 0;
var selectionchange = 0;
var inputSelectionchange = 0;
var textareaSelectionchange = 0;
var cancel = false;
var selectstartTarget = null;
async function UpdateSelectEventsOnTextControlsPref({ selectstart, selectionchange }) {
await SpecialPowers.pushPrefEnv({
'set': [
['dom.select_events.textcontrols.selectstart.enabled', !!selectstart],
['dom.select_events.textcontrols.selectionchange.enabled', !!selectionchange],
]
});
}
await UpdateSelectEventsOnTextControlsPref({
selectstart: false,
selectionchange: false,
});
document.addEventListener('selectstart', function(aEvent) {
console.log("originaltarget", aEvent.originalTarget, "new", selectstartTarget);
is(aEvent.originalTarget, selectstartTarget,
"The original target of selectstart");
selectstartTarget = null;
console.log(selectstart);
selectstart++;
if (cancel) {
aEvent.preventDefault();
}
});
document.addEventListener('selectionchange', function(aEvent) {
ok(isProperSelectionChangeTarget(aEvent.target),
"The target of selectionchange should be one of document, input, or textarea");
console.log(selectionchange);
selectionchange++;
});
function elt(aId) { return document.getElementById(aId); }
function reset() {
selectstart = 0;
selectionchange = 0;
inputSelectionchange = 0;
textareaSelectionchange = 0;
cancel = false;
}
elt("input").addEventListener('selectionchange', function(aEvent) {
is (aEvent.originalTarget, elt("input"),
"The original target of selectionchange should be the input");
console.log(inputSelectionchange);
inputSelectionchange++;
});
elt("textarea").addEventListener('selectionchange', function(aEvent) {
is (aEvent.originalTarget, elt("textarea"),
"The original target of selectionchange should be the textarea");
console.log(textareaSelectionchange);
textareaSelectionchange++;
});
function checkEventCounts(
aTestDescription,
aSituationDescription,
aExpectedEventCounts
) {
let {
selectstartOnDocument = 0,
selectionchangeOnDocument = 0,
selectionchangeOnInput = 0,
selectionchangeOnTextarea = 0,
} = aExpectedEventCounts;
is(
selectstart,
selectstartOnDocument,
`${
aTestDescription
}: "selectstart" event on the document node should be fired ${
selectstartOnDocument
} times ${aSituationDescription}`
);
is(
selectionchange,
selectionchangeOnDocument,
`${
aTestDescription
}: "selectionchange" event on the document node should be fired ${
selectionchangeOnDocument
} times ${aSituationDescription}`
);
is(
inputSelectionchange,
selectionchangeOnInput,
`${
aTestDescription
}: "selectionchange" event on the <input> should be fired ${
selectionchangeOnInput
} times ${aSituationDescription}`
);
is(
textareaSelectionchange,
selectionchangeOnTextarea,
`${
aTestDescription
}: "selectionchange" event on the <textarea> should be fired ${
selectionchangeOnTextarea
} times ${aSituationDescription}`
);
}
async function testWithSynthesizingMouse(
aDescription,
aElement,
aOffset,
aType,
aExpectedEventCounts
) {
const eventObject = aType == "click" ? {} : { type: aType };
if (aOffset.y === undefined || aOffset.y === null) {
aOffset.y = 10;
}
synthesizeMouse(aElement, aOffset.x, aOffset.y, eventObject);
await spin();
checkEventCounts(
aDescription,
`after synthesizing ${aType} at ${aOffset.x}, ${aOffset.y}`,
aExpectedEventCounts
);
reset();
}
async function testWithSynthesizingKey(
aDescription,
aKey,
aEventObject,
aExpectedEventCounts
) {
synthesizeKey(aKey, aEventObject);
await spin();
checkEventCounts(
aDescription,
`after synthesizing a key press of "${aKey}"`,
aExpectedEventCounts
);
reset();
}
async function testWithSettingContentEditableAttribute(
aDescription,
aElement,
aContentEditableValue,
aExpectedEventCounts
) {
aElement.setAttribute("contenteditable",
aContentEditableValue ? "true" : "false");
await spin();
checkEventCounts(
aDescription,
`after setting contenteditable attribute to ${
aElement.getAttribute("contenteditable")
}`,
aExpectedEventCounts
);
reset();
}
var selection = document.getSelection();
function isCollapsed() {
is(selection.isCollapsed, true, "Selection is collapsed");
}
function isNotCollapsed() {
is(selection.isCollapsed, false, "Selection is not collapsed");
}
// Make sure setting the element to contentEditable doesn't cause any selectionchange events
await testWithSettingContentEditableAttribute(
"Setting contenteditable attribute to true of <div> should not change selection",
elt("ce"),
true,
{}
);
// Make sure setting the element to not be contentEditable doesn't cause any selectionchange events
await testWithSettingContentEditableAttribute(
'Setting contenteditable attribute to false of <div contenteditable="true"> should not change selection',
elt("ce"),
false,
{}
);
// Now make the div contentEditable and proceed with the test
await testWithSettingContentEditableAttribute(
'Setting contenteditable attribute to true of <div contenteditable="false"> should not change selection',
elt("ce"),
true,
{}
);
// Focus the contenteditable text
await testWithSynthesizingMouse(
'Clicking in <div contenteditable="true"> should change selection',
elt("ce"),
{ x: 100 },
"click",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
// Move the selection to the right, this should only fire selectstart once
selectstartTarget = elt("ce").firstChild;
await testWithSynthesizingKey(
'Synthesizing Shift-ArrowRight to select a character in the text node of <div contenteditable="true"> should start to select again and change selection',
"KEY_ArrowRight",
{ shiftKey: true },
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
await testWithSynthesizingKey(
'Synthesizing Shift-ArrowRight again to select 2 characters in the text node of <div contenteditable="true"> should change selection',
"KEY_ArrowRight",
{ shiftKey: true },
{ selectionchangeOnDocument: 1 }
);
isNotCollapsed();
// Move it back so that the selection is empty again
await testWithSynthesizingKey(
'Synthesizing Shift-ArrowLeft to shrink selection in the text node of <div contenteditable="true"> should change selection',
"KEY_ArrowLeft",
{ shiftKey: true },
{ selectionchangeOnDocument: 1 }
);
isNotCollapsed();
await testWithSynthesizingKey(
'Synthesizing Shift-ArrowLeft again to collapse selection in the text node of <div contenteditable="true"> should change selection',
"KEY_ArrowLeft",
{ shiftKey: true },
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
// Going from empty to non-empty should fire selectstart again
selectstartTarget = elt("ce").firstChild;
await testWithSynthesizingKey(
'Synthesizing Shift-ArrowLeft again to select a character on the other side in the text node of <div contenteditable="true"> should start to select and change selection',
"KEY_ArrowLeft",
{ shiftKey: true },
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
async function testWithSynthesizingMouseDrag(
aDescription,
aElement,
aSelectstartTarget
) {
// Select a region
await testWithSynthesizingMouse(
`Pressing left mouse button ${
aDescription
} should not start to select but should change selection`,
aElement,
{ x: 50 },
"mousedown",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
selectstartTarget = aSelectstartTarget;
await testWithSynthesizingMouse(
`Dragging mouse to right to extend selection ${
aDescription
} should start to select and change selection`,
aElement,
{ x: 100 },
"mousemove",
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
// Moving it more shouldn't trigger a start (move back to empty)
await testWithSynthesizingMouse(
`Dragging mouse to left to shrink selection ${
aDescription
} should change selection`,
aElement,
{ x: 75 },
"mousemove",
{ selectionchangeOnDocument: 1 }
);
isNotCollapsed();
await testWithSynthesizingMouse(
`Dragging mouse to left to collapse selection ${
aDescription
} should change selection`,
aElement,
{ x: 50 },
"mousemove",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
// Wiggling the mouse a little such that it doesn't select any
// characters shouldn't trigger a selection
await testWithSynthesizingMouse(
`Dragging mouse to bottom a bit ${
aDescription
} should not cause selection change`,
aElement,
{ x: 50, y: 11 },
"mousemove",
{}
);
isCollapsed();
// Moving the mouse again from an empty selection should trigger a
// selectstart
selectstartTarget = aSelectstartTarget;
await testWithSynthesizingMouse(
`Dragging mouse to left to extend selection ${
aDescription
} should start to select and change selection`,
aElement,
{ x: 25 },
"mousemove",
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
// Releasing the mouse shouldn't do anything
await testWithSynthesizingMouse(
`Releasing left mouse button to stop dragging ${
aDescription
} should not change selection`,
aElement,
{ x: 25 },
"mouseup",
{}
);
isNotCollapsed();
// And neither should moving your mouse around when the mouse
// button isn't pressed
await testWithSynthesizingMouse(
`Just moving mouse to right ${
aDescription
} should not start to select nor change selection`,
aElement,
{ x: 50 },
"mousemove",
{}
);
isNotCollapsed();
// Clicking in an random location should move the selection, but not perform a
// selectstart
await testWithSynthesizingMouse(
`Clicking to collapse selection ${
aDescription
} should cause only selection change`,
aElement,
{ x: 50 },
"click",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
// Clicking there again should do nothing
await testWithSynthesizingMouse(
`Clicking same position again ${
aDescription
} should not change selection`,
aElement,
{ x: 50 },
"click",
{}
);
isCollapsed();
// Selecting a region, and canceling the selectstart should mean that the
// selection remains collapsed
await testWithSynthesizingMouse(
`Pressing left mouse button on different character to move caret ${
aDescription
} should cause only selection change`,
aElement,
{ x: 75 },
"mousedown",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
cancel = true;
selectstartTarget = aSelectstartTarget;
await testWithSynthesizingMouse(
`Moving mouse to right to extend selection but selectstart event will be prevented default ${
aDescription
} should start to select and change selection`,
aElement,
{ x: 100 },
"mousemove",
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isCollapsed();
await testWithSynthesizingMouse(
`Releasing the left mouse button after dragging but selectstart was prevented the default ${
aDescription
} should not change selection`,
aElement,
{ x: 100 },
"mouseup",
{}
);
isCollapsed();
}
// Should work both on normal
await testWithSynthesizingMouseDrag(
"on the text node in the non-editable <div>",
elt("inner"),
elt("inner").firstChild
);
// and contenteditable fields
await testWithSynthesizingMouseDrag(
'on the text node in the editable <div contenteditable="true">',
elt("ce"),
elt("ce").firstChild
);
// and fields with elements in them
await testWithSynthesizingMouseDrag(
"on the text node in the non-editable <div>'s child",
elt("normal"),
elt("inner").firstChild
);
await testWithSynthesizingMouse(
"Clicking in the text node in the `<div>` should change selection",
elt("inner"),
{ x: 50 },
"click",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
reset();
// Select all should fire both selectstart and change
selectstartTarget = document.body;
await testWithSynthesizingKey(
"Select All when no editor has focus should start to select and select all content",
"a", { accelKey: true },
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
// Clear the selection
await testWithSynthesizingMouse(
"Clicking in the non-editable <div> should clear selection",
elt("inner"),
{ x: 50 },
"click",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
// Even if we already have a selection
await testWithSynthesizingMouse(
"Pressing the left mouse button in non-editable <div> should change selection",
elt("inner"),
{ x: 75 },
"mousedown",
{ selectionchangeOnDocument: 1 }
);
isCollapsed();
selectstartTarget = elt("inner").firstChild;
await testWithSynthesizingMouse(
"Dragging mouse to right to extend selection should start and change selection",
elt("inner"),
{ x: 100 },
"mousemove",
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
await testWithSynthesizingMouse(
"Releasing the left mouse button should not change selection",
elt("inner"),
{ x: 100 },
"mouseup",
{}
);
isNotCollapsed();
selectstartTarget = document.body;
await testWithSynthesizingKey(
"Select All when no editor has focus should start to select and select all content (again)",
"a",
{ accelKey: true },
{ selectstartOnDocument: 1, selectionchangeOnDocument: 1 }
);
isNotCollapsed();
// Clear the selection
await testWithSynthesizingMouse(
"Clicking in the non-editable <div> should clear selection (again)",
elt("inner"),
{ x: 50 },
"click",
{selectionchangeOnDocument: 1 }
);
isCollapsed();
// Make sure that a synthesized selection change doesn't fire selectstart
getSelection().removeAllRanges();
await spin();
is(
selectstart,
0,
"Selection.removeAllRanges() should not cause selectstart event"
);
is(
selectionchange,
1,
"Selection.removeAllRanges() should cause selectionchange event"
);
reset();
isCollapsed();
await (async function test_Selection_selectNode() {
const range = document.createRange();
range.selectNode(elt("inner"));
getSelection().addRange(range);
await spin();
is(
selectstart,
0,
"Selection.addRange() should not cause selectstart event"
);
is(
selectionchange,
1,
"Selection.addRange() should cause selectionchange event"
);
reset();
isNotCollapsed();
})();
// Change the range, without replacing
await (async function test_Selection_getRangeAt_selectNode() {
getSelection().getRangeAt(0).selectNode(elt("ce"));
await spin();
is(
selectstart,
0,
"Selection.getRangeAt(0).selectNode() should not cause selectstart event"
);
is(
selectionchange,
1,
"Selection.getRangeAt(0).selectNode() should cause selectionchange event"
);
reset();
isNotCollapsed();
})();
// Remove the range
getSelection().removeAllRanges();
await spin();
is(
selectstart,
0,
"Selection.removeAllRanges() should not cause selectstart event (again)"
);
is(
selectionchange,
1,
"Selection.removeAllRanges() should cause selectionchange event (again)"
);
reset();
isCollapsed();
for (const textControl of [elt("input"), elt("textarea")]) {
await UpdateSelectEventsOnTextControlsPref({
selectstart: false,
selectionchange: false,
});
// Without the dom.select_events.textcontrols.enabled pref,
// pressing the mouse shouldn't do anything.
await testWithSynthesizingMouse(
`Pressing the left mouse button in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection of the document`,
textControl,
{ x: 50 },
"mousedown",
{
// XXX Bug 1721287: For some reason we fire 2 selectchange events
// on the body when switching from the <input> to the <textarea>.
selectionchangeOnDocument:
document.activeElement != textControl &&
(document.activeElement.tagName.toLocaleLowerCase() == "input" ||
document.activeElement.tagName.toLocaleLowerCase() == "textarea")
? 2
: 1,
}
);
// Releasing the mouse shouldn't do anything
await testWithSynthesizingMouse(
`Releasing the left mouse button in <${
textControl.tagName.toLocaleLowerCase()
}> should not change any selection`,
textControl,
{ x: 50 },
"mouseup",
{}
);
for (const selectstart of [1, 0]) {
await UpdateSelectEventsOnTextControlsPref({
selectstart,
selectionchange: true,
});
const selectstartEventSetting = `selectstart in text controls is ${
selectstart ? "enabled" : "disabled"
}`;
const isInput = textControl.tagName.toLocaleLowerCase() == "input";
await testWithSynthesizingMouse(
`Pressing the left mouse button in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection (${selectstartEventSetting})`,
textControl,
{ x: 40 },
"mousedown",
{
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
selectstartTarget = textControl;
await testWithSynthesizingMouse(
`Dragging mouse to right to extend selection in <${
textControl.tagName.toLocaleLowerCase()
}> should start to select and change selection (${
selectstartEventSetting
})`,
textControl,
{ x: 100 },
"mousemove",
{
selectstartOnDocument: selectstart,
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
// Moving it more shouldn't trigger a start (move back to empty)
await testWithSynthesizingMouse(
`Dragging mouse to left to shrink selection in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection (${selectstartEventSetting})`,
textControl,
{ x: 75 },
"mousemove",
{
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
await testWithSynthesizingMouse(
`Dragging mouse to left to collapse selection in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection (${selectstartEventSetting})`,
textControl,
{ x: 40 },
"mousemove",
{
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
// Wiggling the mouse a little such that it doesn't select any
// characters shouldn't trigger a selection
await testWithSynthesizingMouse(
`Pressing the left mouse button at caret in <${
textControl.tagName.toLocaleLowerCase()
}> should not change selection (${selectstartEventSetting})`,
textControl,
{
x: 40,
y: 11,
},
"mousemove",
{}
);
// Moving the mouse again from an empty selection should trigger a
// selectstart
selectstartTarget = textControl;
await testWithSynthesizingMouse(
`Dragging mouse to left to extend selection in <${
textControl.tagName.toLocaleLowerCase()
}> should start to select and change selection (${
selectstartEventSetting
})`,
textControl,
{ x: 25 },
"mousemove",
{
selectstartOnDocument: selectstart,
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
// Releasing the mouse shouldn't do anything
await testWithSynthesizingMouse(
`Releasing the left mouse button in <${
textControl.tagName.toLocaleLowerCase()
}> should not change selection (${selectstartEventSetting})`,
textControl,
{ x: 25 },
"mouseup",
{}
);
// And neither should moving your mouse around when the mouse
// button isn't pressed
await testWithSynthesizingMouse(
`Just moving mouse to right in <${
textControl.tagName.toLocaleLowerCase()
}> should not start to select nor change selection (${
selectstartEventSetting
})`,
textControl,
{ x: 50 },
"mousemove",
{}
);
// Clicking in an random location should move the selection, but
// not perform a selectstart
await testWithSynthesizingMouse(
`Clicking in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection, but should not start selection (${
selectstartEventSetting
})`,
textControl,
{ x: 50 },
"click",
{
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
// Clicking there again should do nothing
await testWithSynthesizingMouse(
`Clicking at caret in <${
textControl.tagName.toLocaleLowerCase()
}> should not change selection (${selectstartEventSetting})`,
textControl,
{ x: 50 },
"click",
{}
);
// Selecting a region, and canceling the selectstart should mean that the
// selection remains collapsed
await testWithSynthesizingMouse(
`Pressing the left mouse button at different character in <${
textControl.tagName.toLocaleLowerCase()
}> should change selection (${selectstartEventSetting})`,
textControl,
{ x: 75 },
"mousedown",
{
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
cancel = true;
selectstartTarget = textControl;
await testWithSynthesizingMouse(
`Dragging mouse to right to extend selection in <${
textControl.tagName.toLocaleLowerCase()
}> but the default of selectstart is prevented should cause selectstart and selectionchange events (${
selectstartEventSetting
})`,
textControl,
{ x: 100 },
"mousemove",
{
selectstartOnDocument: selectstart,
selectionchangeOnDocument: 1,
selectionchangeOnInput: isInput ? 1 : 0,
selectionchangeOnTextarea: isInput ? 0 : 1,
}
);
await testWithSynthesizingMouse(
`Releasing the left mouse button in <${
textControl.tagName.toLocaleLowerCase()
}> should not cause changing selection (${selectstartEventSetting})`,
textControl,
{ x: 100 },
"mouseup",
{}
);
}
}
// Marking the input and textarea as display: none and then as visible again
// shouldn't trigger any changes, although the nodes will be re-framed
for (const textControl of [elt("input"), elt("textarea")]) {
await (async function test_set_display_of_text_control_to_none() {
textControl.setAttribute("style", "display: none;");
await spin();
checkEventCounts(
`Setting display of <${
textControl.tagName.toLocaleLowerCase()
}> to none`,
"",
{}
);
reset();
})();
await (async function test_remove_display_none_of_text_control() {
textControl.setAttribute("style", "");
await spin();
checkEventCounts(
`Removing display:none of <${
textControl.tagName.toLocaleLowerCase()
}>`,
"",
{}
);
reset();
})();
}
// When selection is at the end of contentEditable's content,
// clearing the content should trigger selection events.
await (async function test_removing_contenteditable() {
const savedContent = elt("ce").innerHTML;
document.getSelection().setBaseAndExtent(elt("ce"), 1, elt("ce"), 1);
await spin();
reset();
elt("ce").firstChild.remove();
await spin();
checkEventCounts(
'Removing <div contenteditable="true"> from the DOM tree',
"",
{ selectionchangeOnDocument: 1 }
);
elt("ce").innerHTML = savedContent;
await spin();
reset();
})();
});
</script>
</body>
</html>