chromium/third_party/blink/web_tests/editing/input/edit-context.html

<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>