chromium/third_party/blink/web_tests/external/wpt/input-events/input-events-get-target-ranges-deleting-range-across-editing-host-boundaries.tentative.html

<!DOCTYPE html>
<meta charset="utf-8">
<title>InputEvent.getTargetRanges() of deleting  a range across editing host boundaries</title>
<div contenteditable></div>
<script src="input-events-get-target-ranges.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script>
"use strict";

// This test just check whether the deleted content range(s) and target ranges of `beforeinput`
// are match or different.  The behavior should be defined by editing API.
// https://github.com/w3c/editing/issues/283

promise_test(async () => {
  initializeTest('<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>');
  await sendBackspaceKey();
  const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
  const kOnlyEditableTextDeletedCase = '<p>ab<span contenteditable="false">non-editable</span>def</p>';
  const kNonEditableElementDeleteCase = '<p>abdef</p>';
  if (gEditor.innerHTML === kNothingDeletedCase) {
    if (gBeforeinput.length === 0) {
      assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
      assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
      return;
    }
    assert_equals(gBeforeinput.length, 1,
      "If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
    assert_equals(gBeforeinput[0].cachedRanges.length, 0,
      `If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
        getRangeDescription(gBeforeinput[0].cachedRanges[0])
      })`);
    assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
      "If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
    assert_equals(gInput.length, 0,
      "If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
    return;
  }
  if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If only editable text is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If only editable text is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild,
        startOffset: 2,
        endContainer: gEditor.firstChild.firstChild,
        endOffset: 3,
      }),
      "If only editable text is deleted, its target range should be the deleted text range");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If only editable text is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If only editable text is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeleteCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    assert_in_array(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      [
        getRangeDescription({
          startContainer: gEditor.firstChild.firstChild,
          startOffset: 2,
          endContainer: gEditor.firstChild,
          endOffset: 2,
        }),
        getRangeDescription({
          startContainer: gEditor.firstChild.firstChild,
          startOffset: 2,
          endContainer: gEditor.firstChild.firstChild.nextSibling,
          endOffset: 0,
        }),
      ],
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  assert_in_array(gEditor.innerHTML,
    [
      kNothingDeletedCase,
      kOnlyEditableTextDeletedCase,
      kNonEditableElementDeleteCase,
    ], "The result content is unexpected");
}, 'Backspace at "<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>"');

promise_test(async () => {
  initializeTest('<p>abc<span contenteditable="false">non-[editable</span>de]f</p>');
  await sendBackspaceKey();
  const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
  const kOnlyEditableTextDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>f</p>';
  const kNonEditableElementDeletedCase = '<p>abcf</p>';;
  if (gEditor.innerHTML === kNothingDeletedCase) {
    if (gBeforeinput.length === 0) {
      assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
      assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
      return;
    }
    assert_equals(gBeforeinput.length, 1,
      "If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
    assert_equals(gBeforeinput[0].cachedRanges.length, 0,
      `If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
        getRangeDescription(gBeforeinput[0].cachedRanges[0])
      })`);
    assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
      "If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
    assert_equals(gInput.length, 0,
      "If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
    return;
  }
  if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If only editable text is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If only editable text is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.lastChild,
        startOffset: 0,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If only editable text is deleted, its target range should be the deleted text range");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If only editable text is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If only editable text is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  assert_in_array(gEditor.innerHTML,
    [
      kNothingDeletedCase,
      kOnlyEditableTextDeletedCase,
      kNonEditableElementDeletedCase,
    ], "The result content is unexpected");
}, 'Backspace at "<p>abc<span contenteditable="false">non-[editable</span>de]f</p>"');


promise_test(async () => {
  initializeTest('<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>');
  let firstRange = gSelection.getRangeAt(0);
  if (!firstRange ||
      firstRange.startContainer != gEditor.firstChild.firstChild.firstChild ||
      firstRange.startOffset != 1 ||
      firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
      firstRange.endOffset != 2) {
    assert_true(true, "Selection couldn't set across editing host boundaries");
    return;
  }
  await sendBackspaceKey();
  const kNothingDeletedCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">def</span></p>';
  const kOnlyEditableContentDeletedCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">f</span></p>';
  const kNonEditableElementDeletedCase = '<p contenteditable="false"><span contenteditable="">af</span></p>';
  const kDeleteEditableContentBeforeNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">def</span></p>';
  const kDeleteEditableContentAfterNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">f</span></p>';
  if (gEditor.innerHTML === kNothingDeletedCase) {
    if (gBeforeinput.length === 0) {
      assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
      assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
      return;
    }
    assert_equals(gBeforeinput.length, 1,
      "If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
    assert_equals(gBeforeinput[0].cachedRanges.length, 0,
      `If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
        getRangeDescription(gBeforeinput[0].cachedRanges[0])
      })`);
    assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
      "If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
    assert_equals(gInput.length, 0,
      "If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
    return;
  }
  if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If only editable text is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 2,
      "If only editable text is deleted, `beforeinput` event should have 2 target ranges");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.lastChild,
        endOffset: 3,
      }),
      "If only editable text is deleted, its first target range should be the deleted text range in the first text node");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
      getRangeDescription({
        startContainer: gEditor.firstChild.last.firstChild,
        startOffset: 0,
        endContainer: gEditor.firstChild.last.firstChild,
        endOffset: 2,
      }),
      "If only editable text is deleted, its second target range should be the deleted text range in the last text node");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If only editable text is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If only editable text is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.lastChild.firstChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.firstChild.firstChild,
        endOffset: 3,
      }),
      "If editable text before non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text before non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text before non-editable element is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.lastChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.lastChild.firstChild,
        endOffset: 3,
      }),
      "If editable text after non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text after non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text after non-editable element is deleted, `input` event should be fired");
    return;
  }
  assert_in_array(gEditor.innerHTML,
    [
      kNothingDeletedCase,
      kOnlyEditableContentDeletedCase,
      kNonEditableElementDeletedCase,
      kDeleteEditableContentBeforeNonEditableContentCase,
      kDeleteEditableContentAfterNonEditableContentCase,
    ], "The result content is unexpected");
}, 'Backspace at "<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>"');

promise_test(async () => {
  initializeTest('<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>');
  let firstRange = gSelection.getRangeAt(0);
  if (!firstRange ||
      firstRange.startContainer != gEditor.firstChild.firstChild ||
      firstRange.startOffset != 1 ||
      firstRange.endContainer != gEditor.querySelector("span span").firstChild ||
      firstRange.endOffset != 2) {
    assert_true(true, "Selection couldn't set across editing host boundaries");
    return;
  }
  await sendBackspaceKey();
  const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
  const kOnlyEditableContentDeletedCase = '<p>a<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
  const kNonEditableElementDeletedCase1 = '<p>af</p>';
  const kNonEditableElementDeletedCase2 = '<p>a<span contenteditable="">f</span></p>';
  const kDeleteEditableContentBeforeNonEditableContentCase ='<p>a<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
  const kDeleteEditableContentAfterNonEditableContentCase ='<p>abc<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
  if (gEditor.innerHTML === kNothingDeletedCase) {
    if (gBeforeinput.length === 0) {
      assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
      assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
      return;
    }
    assert_equals(gBeforeinput.length, 1,
      "If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
    assert_equals(gBeforeinput[0].cachedRanges.length, 0,
      `If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
        getRangeDescription(gBeforeinput[0].cachedRanges[0])
      })`);
    assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
      "If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
    assert_equals(gInput.length, 0,
      "If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
    return;
  }
  if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If only editable text is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 2,
      "If only editable text is deleted, `beforeinput` event should have 2 target ranges");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.firstChild,
        endOffset: 3,
      }),
      "If only editable text is deleted, its first target range should be the deleted text range in the first text node");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span span").firstChild,
        startOffset: 0,
        endContainer: gEditor.querySelector("span span").firstChild,
        endOffset: 2,
      }),
      "If only editable text is deleted, its second target range should be the deleted text range in the last text node");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If only editable text is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If only editable text is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    // XXX If the text nodes are merged, we need to cache it for here.
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild,
        startOffset: 1,
        endContainer: gEditor.querySelector("span").firstChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.firstChild,
        endOffset: 3,
      }),
      "If editable text before non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text before non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text before non-editable element is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span").firstChild,
        startOffset: 0,
        endContainer: gEditor.querySelector("span").firstChild,
        endOffset: 2,
      }),
      "If editable text after non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text after non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text after non-editable element is deleted, `input` event should be fired");
    return;
  }
  assert_in_array(gEditor.innerHTML,
    [
      kNothingDeletedCase,
      kOnlyEditableContentDeletedCase,
      kNonEditableElementDeletedCase1,
      kNonEditableElementDeletedCase2,
      kDeleteEditableContentBeforeNonEditableContentCase,
      kDeleteEditableContentAfterNonEditableContentCase,
    ], "The result content is unexpected");
}, 'Backspace at "<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>"');

promise_test(async () => {
  initializeTest('<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>');
  let firstRange = gSelection.getRangeAt(0);
  if (!firstRange ||
      firstRange.startContainer != gEditor.querySelector("span span").firstChild ||
      firstRange.startOffset != 1 ||
      firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
      firstRange.endOffset != 2) {
    assert_true(true, "Selection couldn't set across editing host boundaries");
    return;
  }
  await sendBackspaceKey();
  const kNothingDeletedCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>def</p>';
  const kOnlyEditableContentDeletedCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>f</p>';
  const kNonEditableElementDeletedCase1 = '<p><span contenteditable="false"><span contenteditable="">af</span></span></p>';
  const kNonEditableElementDeletedCase2 = '<p><span contenteditable="false"><span contenteditable="">a</span></span>f</p>';
  const kDeleteEditableContentBeforeNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>def</p>';
  const kDeleteEditableContentAfterNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>f</p>';
  if (gEditor.innerHTML === kNothingDeletedCase) {
    if (gBeforeinput.length === 0) {
      assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
      assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
      return;
    }
    assert_equals(gBeforeinput.length, 1,
      "If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
    assert_equals(gBeforeinput[0].cachedRanges.length, 0,
      `If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
        getRangeDescription(gBeforeinput[0].cachedRanges[0])
      })`);
    assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
      "If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
    assert_equals(gInput.length, 0,
      "If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
    return;
  }
  if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
    assert_equals(gBeforeinput.length, 1,
      "If only editable text is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 2,
      "If only editable text is deleted, `beforeinput` event should have 2 target ranges");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span span").firstChild,
        startOffset: 1,
        endContainer: gEditor.querySelector("span span").firstChild,
        endOffset: 3,
      }),
      "If only editable text is deleted, its first target range should be the deleted text range in the first text node");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
      getRangeDescription({
        startContainer: gEditor.firstChild.lastChild,
        startOffset: 0,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If only editable text is deleted, its second target range should be the deleted text range in the last text node");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If only editable text is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If only editable text is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    // XXX If the text nodes are merged, we need to cache it for here.
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span span").firstChild,
        startOffset: 1,
        endContainer: gEditor.querySelector("span span").lastChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span span").firstChild,
        startOffset: 1,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text and non-editable element are deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text and non-editable element are deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.querySelector("span span").firstChild,
        startOffset: 1,
        endContainer: gEditor.querySelector("span span").firstChild,
        endOffset: 3,
      }),
      "If editable text before non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text before non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text before non-editable element is deleted, `input` event should be fired");
    return;
  }
  if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
    assert_equals(gBeforeinput.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should be fired");
    assert_equals(gBeforeinput[0].cachedRanges.length, 1,
      "If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
    assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
      getRangeDescription({
        startContainer: gEditor.firstChild.lastChild,
        startOffset: 0,
        endContainer: gEditor.firstChild.lastChild,
        endOffset: 2,
      }),
      "If editable text after non-editable element is deleted, its target range should be only the deleted text");
    assert_equals(gBeforeinput[0].inputType, "deleteContent",
      "If editable text after non-editable element is deleted, its input type should be deleteContent");
    assert_equals(gInput.length, 1,
      "If editable text after non-editable element is deleted, `input` event should be fired");
    return;
  }
  assert_in_array(gEditor.innerHTML,
    [
      kNothingDeletedCase,
      kOnlyEditableContentDeletedCase,
      kNonEditableElementDeletedCase1,
      kNonEditableElementDeletedCase2,
      kDeleteEditableContentBeforeNonEditableContentCase,
      kDeleteEditableContentAfterNonEditableContentCase,
    ], "The result content is unexpected");
}, 'Backspace at "<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>"');

</script>