<html>
<head>
<title>This tests the new EditContext APIs while in composition</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../resources/testdriver.js"></script>
<script src="../../resources/testdriver-actions.js"></script>
<script src="../../resources/testdriver-vendor.js"></script>
</head>
<body>
<div id="test"></div>
<div id="test2"></div>
<script>
test(function() {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const childDocument = iframe.contentDocument;
const childDiv = childDocument.createElement('div');
iframe.remove();
childDiv.editContext = new EditContext();
}, 'Setting .editContext on a stray div should not crash.');
test(function() {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
iframe.srcdoc = `<script>
const editContext = new EditContext();
window.top.document.querySelector("iframe").remove();
editContext.updateSelection(0, 0);
</scr` + `ipt>`;
}, 'Calling updateSelection in an EditContext in a detached frame should not crash.');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.createElement("div");
document.body.appendChild(test);
test.tabIndex = "0";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.focus();
test.editContext = editContext;
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
test.remove();
}, 'Testing attaching EditContext AFTER the element is focused.');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.setComposition("bar");
assert_equals(test.innerHTML, "BAR");
}, 'Testing EditContext TextUpdate');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "ABCD";
editContext.updateText(0, 4, "abcd");
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
let compositionStartData = [];
let compositionEndData = [];
editContext.addEventListener("compositionstart", e => {
compositionStartData.push(e.data);
});
editContext.addEventListener("compositionend", e => {
compositionEndData.push(e.data);
});
test.editContext = editContext;
test.focus();
editContext.updateSelection(2, 2);
textInputController.setComposition("foo");
assert_equals(editContext.text, "abfoocd");
assert_equals(test.innerHTML, "ABFOOCD");
assert_array_equals(compositionStartData, ["foo"], "Should get compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, [], "Should not get compositionend until terminating composition");
textInputController.setComposition("");
assert_equals(editContext.text, "abcd");
assert_equals(test.innerHTML, "ABCD");
assert_array_equals(compositionStartData, ["foo"], "Shouldn't get another compositionstart when setting empty composition");
assert_array_equals(compositionEndData, [""], "Should get compositionend when setting empty composition");
textInputController.setComposition("bar");
assert_equals(editContext.text, "abbarcd");
assert_equals(test.innerHTML, "ABBARCD");
assert_array_equals(compositionStartData, ["foo", "bar"], "Should get another compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, [""], "Should not get another compositionend until terminating composition");
textInputController.insertText("bar");
assert_equals(editContext.text, "abbarcd");
assert_equals(test.innerHTML, "ABBARCD");
assert_array_equals(compositionStartData, ["foo", "bar"], "Shouldn't get another compositionstart when committing composition");
assert_array_equals(compositionEndData, ["", "bar"], "Should get compositionend when committing composition");
textInputController.setComposition("baz");
assert_equals(editContext.text, "abbarbazcd");
assert_equals(test.innerHTML, "ABBARBAZCD");
assert_array_equals(compositionStartData, ["foo", "bar", "baz"], "Should get another compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, ["", "bar"], "Should not get another compositionend until terminating composition");
test.blur();
assert_equals(editContext.text, "abbarbazcd");
assert_equals(test.innerHTML, "ABBARBAZCD");
assert_array_equals(compositionStartData, ["foo", "bar", "baz"], "Shouldn't get compositionend when removing focus from EditContext");
assert_array_equals(compositionEndData, ["", "bar", "baz"], "Should get compositionend when removing focus from EditContext");
}, 'Testing EditContext composition text updates and events');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "ABCD";
editContext.updateText(0, 4, "abcd");
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
let compositionStartData = [];
let compositionEndData = [];
editContext.addEventListener("compositionstart", e => {
compositionStartData.push(e.data);
});
editContext.addEventListener("compositionend", e => {
compositionEndData.push(e.data);
});
test.editContext = editContext;
test.focus();
editContext.updateSelection(1, 3);
textInputController.setComposition("foo");
assert_equals(editContext.text, "afood");
assert_equals(test.innerHTML, "AFOOD");
assert_array_equals(compositionStartData, ["foo"], "Should get compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, [], "Should not get compositionend until terminating composition");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
textInputController.insertText("foo");
assert_array_equals(compositionStartData, ["foo"], "Should get compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, ["foo"], "Should not get compositionend until terminating composition");
assert_equals(editContext.text, "afood");
assert_equals(test.innerHTML, "AFOOD");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
editContext.updateSelection(4, 1);
textInputController.setComposition("bar");
assert_equals(editContext.text, "abard");
assert_equals(test.innerHTML, "ABARD");
assert_array_equals(compositionStartData, ["foo", "bar"], "Should get compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, ["foo"], "Should not get compositionend until terminating composition");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
textInputController.insertText("bar");
assert_equals(editContext.text, "abard");
assert_equals(test.innerHTML, "ABARD");
assert_array_equals(compositionStartData, ["foo", "bar"], "Should get compositionstart after setting non-empty composition");
assert_array_equals(compositionEndData, ["foo", "bar"], "Should not get compositionend until terminating composition");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
}, 'Testing EditContext composition text updates and events with non-collapsed selection');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "ABCD";
editContext.updateText(0, 4, "abcd");
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
let compositionStartData = [];
let compositionEndData = [];
editContext.addEventListener("compositionstart", e => {
assert_unreached("Should not get composition events when composition was not set");
});
editContext.addEventListener("compositionend", e => {
assert_unreached("Should not get composition events when composition was not set");
});
test.editContext = editContext;
test.focus();
editContext.updateSelection(1, 3);
textInputController.insertText("foo");
assert_equals(editContext.text, "afood");
assert_equals(test.innerHTML, "AFOOD");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
editContext.updateSelection(4, 1);
textInputController.insertText("bar");
assert_equals(editContext.text, "abard");
assert_equals(test.innerHTML, "ABARD");
assert_equals(editContext.selectionStart, 4);
assert_equals(editContext.selectionEnd, 4);
}, 'Testing EditContext insertText without composition');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
let textFormats = [];
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
editContext.addEventListener("textformatupdate", e => {
//TODO: Currently Chromium only fires default styles
textFormats = e.getTextFormats();
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
assert_equals(textFormats.length, 1);
assert_equals(textFormats[0].rangeStart, 0);
assert_equals(textFormats[0].rangeEnd, 3);
assert_equals(textFormats[0].underlineStyle, "Solid");
assert_equals(textFormats[0].underlineThickness, "Thin");
textInputController.setComposition("");
assert_equals(textFormats.length, 0);
}, 'Testing EditContext TextFormatUpdate');
test(function() {
const editContext1 = new EditContext();
const editContext2 = new EditContext();
assert_not_equals(editContext1, null);
assert_not_equals(editContext2, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext1.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
editContext2.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toLowerCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext1;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.setComposition("bar");
assert_equals(test.innerHTML, "BAR");
test.innerHTML = "";
test.editContext = editContext2;
textInputController.setComposition("HELLO");
assert_equals(test.innerHTML, "hello");
textInputController.setComposition("WORLD");
assert_equals(test.innerHTML, "world");
}, 'Testing Multiple EditContext TextUpdates');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
var compositionStartFired = 0;
var compositionEndFired = 0;
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
editContext.addEventListener("compositionstart", e => {
// Update the text in the div
compositionStartFired++;
});
editContext.addEventListener("compositionend", e => {
compositionEndFired++;
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
assert_equals(compositionStartFired, 1);
textInputController.setComposition("bar");
assert_equals(test.innerHTML, "BAR");
assert_equals(compositionStartFired, 1);
textInputController.insertText("bar");
assert_equals(test.innerHTML, "BAR");
assert_equals(compositionEndFired, 1);
}, 'Testing EditContext Composition Event');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("f");
assert_equals(test.innerHTML, "F");
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.insertText("foobar");
assert_equals(test.innerHTML, "FOOBAR");
}, 'Testing EditContext Text updates');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("f");
assert_equals(test.innerHTML, "F");
textInputController.setComposition("");
assert_equals(test.innerHTML, "");
textInputController.insertText("foobar");
assert_equals(test.innerHTML, "FOOBAR");
}, 'Testing EditContext Text updates with empty text');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
editContext.updateText(0, 1, "h");
assert_equals(editContext.text, "hoo");
editContext.updateText(0, 3, "bar");
assert_equals(editContext.text, "bar");
}, 'Testing EditContext TextChange');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
test.editContext = editContext;
test.focus();
assert_equals(editContext.text, "");
editContext.updateText(0, 0, "foo");
assert_equals(editContext.text, "foo");
// replace "oo" with "bar"
textInputController.setCompositionWithReplacementRange("bar", 1, 3);
assert_equals(editContext.text, "fbar");
}, 'The new text should apply to the replacement range');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.insertText("bar");
assert_equals(editContext.text, "bar");
assert_equals(editContext.selectionStart, 3);
assert_equals(editContext.selectionEnd, 3);
editContext.updateSelection(0, 0);
assert_equals(editContext.selectionStart, 0);
assert_equals(editContext.selectionEnd, 0);
textInputController.setComposition("foo");
assert_equals(editContext.text, "foobar");
textInputController.insertText("foo");
assert_equals(editContext.selectionStart, 3);
assert_equals(editContext.selectionEnd, 3);
}, 'Testing EditContext Selection Change');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
editContext.updateText(0, 1, "h");
assert_equals(editContext.text, "hoo");
editContext.updateText(0, 3, "bar");
assert_equals(editContext.text, "bar");
textInputController.insertText("bar");
assert_equals(editContext.text, "bar");
editContext.updateSelection(0, 0);
assert_equals(editContext.selectionStart, 0);
assert_equals(editContext.selectionEnd, 0);
textInputController.setComposition("foo");
assert_equals(editContext.text, "foobar");
textInputController.insertText("foo");
assert_equals(editContext.text, "foobar");
}, 'Testing EditContext Text And Selection Change');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.insertText("foo");
textInputController.setMarkedTextFromExistingText(0, 3);
textInputController.insertText("bar");
assert_equals(editContext.text, "bar");
textInputController.setMarkedTextFromExistingText(2, 3);
textInputController.insertText("t");
assert_equals(editContext.text, "bat");
}, 'Testing EditContext SetCompositionFromExistingText');
promise_test(async function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.insertText("foo");
assert_equals(editContext.text, "foo", "EditContext's text should have been updated");
assert_equals(editContext.selectionStart, 3, "EditContext's selectionStart should have been placed after inserted text");
assert_equals(editContext.selectionEnd, 3, "EditContext's selectionEnd should have been placed after inserted text");
textInputController.setMarkedTextFromExistingText(0, 3);
assert_equals(editContext.text, "foo", "SetCompositionFromExistingText should not change EditContext's text");
assert_equals(editContext.selectionStart, 3, "SetCompositionFromExistingText should not change EditContext's text");
assert_equals(editContext.selectionEnd, 3, "SetCompositionFromExistingText should not change EditContext's text");
await test_driver.send_keys(test, 'a');
assert_equals(editContext.text, "fooa", "Normal typing should be inserted at selection position, not composition position");
assert_equals(test.innerHTML, "FOOA", "Normal typing should be inserted at selection position, not composition position");
}, 'Testing EditContext SetCompositionFromExistingText followed by non-composition typing');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.insertText("foo");
assert_equals(editContext.selectionStart, 3);
assert_equals(editContext.selectionEnd, 3);
textInputController.extendSelectionAndDelete(3, 0);
assert_equals(editContext.selectionStart, 0);
assert_equals(editContext.selectionEnd, 0);
textInputController.insertText("bar");
assert_equals(editContext.text, "bar");
assert_equals(editContext.selectionStart, 3);
assert_equals(editContext.selectionEnd, 3);
textInputController.extendSelectionAndDelete(1, 0);
assert_equals(editContext.selectionStart, 2);
assert_equals(editContext.selectionEnd, 2);
assert_equals(editContext.text, "ba");
textInputController.insertText("t");
assert_equals(editContext.text, "bat");
}, 'Testing EditContext ExtendSelectionAndDelete');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
textInputController.unmarkText();
assert_equals(editContext.text, "foo");
textInputController.setComposition("bar");
assert_equals(editContext.text, "foobar");
}, 'Testing EditContext FinishComposingText');
test(function() {
const editContext1 = new EditContext();
assert_not_equals(editContext1, null);
const editContext2 = new EditContext();
assert_not_equals(editContext2, null);
const test = document.getElementById('test');
test.innerHTML = "";
// Add EditContext event listeners
editContext1.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toUpperCase() + buffer.substr(e.updateRangeEnd);
});
editContext2.addEventListener("textupdate", e => {
// Update the text in the div
const buffer = test.innerText;
test.innerHTML = buffer.substr(0, e.updateRangeStart) + e.text.toLowerCase() + buffer.substr(e.updateRangeEnd);
});
test.editContext = editContext1;
test.focus();
textInputController.setComposition("foo");
assert_equals(test.innerHTML, "FOO");
test.blur();
textInputController.setComposition("foo2");
assert_equals(test.innerHTML, "FOO");
const test2 = document.getElementById('test2');
test2.editContext = editContext2;
test2.focus();
textInputController.setComposition("BAR");
assert_equals(editContext2.text, "BAR");
assert_equals(test.innerHTML, "barFOO");
assert_equals(editContext1.text, "foo");
}, 'Testing EditContext blur');
test(function() {
// SetComposition should not crash when event handler removes document
const child = document.createElement("iframe");
document.body.appendChild(child);
const childDocument = child.contentDocument;
const editable = childDocument.createElement('div');
editable.contentEditable = true;
childDocument.body.appendChild(editable);
editable.addEventListener("focusin", e => {
const childEditContext = new EditContext();
editable.editContext = childEditContext;
childEditContext.addEventListener("textupdate", e => {
child.remove();
});
childEditContext.addEventListener("textformatupdate", e => {
});
});
editable.focus();
child.contentWindow.focus();
child.contentWindow.textInputController.setComposition("bar");
}, 'Testing EditContext Iframe Document Delete');
test(function() {
const editContext1 = new EditContext();
editContext1.addEventListener("textupdate", e => {
});
const test = document.getElementById('test');
test.editContext = editContext1;
test.focus();
gc()
textInputController.setComposition("bar");
}, 'Testing EditContext GC');
test(function() {
const editContext = new EditContext();
assert_not_equals(editContext, null);
const test = document.createElement("div");
document.body.appendChild(test);
test.editContext = editContext;
test.focus();
textInputController.insertText("abc")
textInputController.setComposition("def");
const rect1 = new DOMRect(0, 2, 4, 8);
const rect2 = new DOMRect(4, 2, 4, 8);
const rect3 = new DOMRect(8, 2, 4, 8);
editContext.updateCharacterBounds(3, [rect1, rect2, rect3]);
assert_array_equals(textInputController.firstRectForCharacterRange(2, 0), [], "0-width call at index 2 should get empty array");
assert_array_equals(textInputController.firstRectForCharacterRange(3, 0), [0, 2, 0, 8], "0-width call at index 3 should get caret position before first character");
assert_array_equals(textInputController.firstRectForCharacterRange(4, 0), [4, 2, 0, 8], "0-width call at index 4 should get caret position before second character");
assert_array_equals(textInputController.firstRectForCharacterRange(5, 0), [8, 2, 0, 8], "0-width call at index 5 should get caret position before third character");
assert_array_equals(textInputController.firstRectForCharacterRange(6, 0), [12, 2, 0, 8], "0-width call at index 6 should get caret position after third character");
assert_array_equals(textInputController.firstRectForCharacterRange(7, 0), [], "0-width call at index 7 should get empty array");
assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [0, 2, 4, 8], "1-width call at index 3 should get first character rect");
assert_array_equals(textInputController.firstRectForCharacterRange(4, 1), [4, 2, 4, 8], "1-width call at index 4 should get second character rect");
assert_array_equals(textInputController.firstRectForCharacterRange(5, 1), [8, 2, 4, 8], "1-width call at index 5 should get third character rect");
assert_array_equals(textInputController.firstRectForCharacterRange(6, 1), [], "1-width call at index 6 should get empty array");
assert_array_equals(textInputController.firstRectForCharacterRange(3, 3), [0, 2, 12, 8], "3-width call should join rects for characters 3-5");
test.remove();
}, 'Testing firstRectForCharacterRange');
test(function() {
const editContext = new EditContext();
const test = document.createElement("div");
document.body.appendChild(test);
test.editContext = editContext;
test.focus();
textInputController.insertText("abc");
textInputController.setComposition("def");
assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [], "No rects are provided if no bounds have been set");
const rect1 = new DOMRect(0, 2, 0, 0);
editContext.updateControlBounds(rect1);
assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [0, 2, 0, 0], "If there are no composition bounds and no selection bounds, fall back to control bounds");
const rect2 = new DOMRect(4, 2, 0, 0);
editContext.updateSelectionBounds(rect2);
assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [4, 2, 0, 0], "If there are no composition bounds, fall back to selection bounds");
const rect3 = new DOMRect(8, 2, 0, 0);
editContext.updateCharacterBounds(3, [rect3, rect3, rect3]);
assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [8, 2, 0, 0], "Character bounds should be used even if control and selection bounds are set");
test.remove();
}, 'firstRectForCharacterRange empty control bounds and empty selection bounds fallbacks');
</script>
</body>
</html>