chromium/third_party/blink/web_tests/accessibility/inline-text-box-next-on-line.html

<!DOCTYPE html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>

<p id="paragraph">
  The <b id="quick">quick</b> brown fox<br>
  jumps over the lazy <b id="dog">dog</b>.
</p>

<div contentEditable="true" id="contentEditable">
  <div>
    <span>Line </span><span>one has one trailing space.</span><span> </span>
  </div>
  <div>
    <span>&#9;&#9;</span><span>Line</span><span> two has two leading tabs.</span>
  </div>
</div>

<p id="paragraphWithLink">
  Paragraph with a <a href="#">link</a> inside.
</p>

<ol id="list">
  <li>List item</li>
  <li><button aria-label="button"></button></li>
</ol>

<p id="paragraphWithAnonymousInline">
  Paragraph with an <span style="background-color: red;">anonymous inline</span> inside.
</p>

<!-- Adding "outline-style" on the <span> ensures that its inline line boxes are
     not culled. -->
<div id="multilineInline" contenteditable="true">
  This is a
  <span style="outline-style: solid">
    test with a<br>
    second
  </span>
  line.
</div>

<div id="anonymousMultilineInline" contenteditable="true">
  This is a
  <span aria-invalid="grammar">
    test with a<br>
    second
  </span>
  line.
</div>

<script>
test(() => {
  let axObj = accessibilityController.accessibleElementById('paragraph');
  // Find the first inline text box.
  while (axObj.childrenCount > 0)
    axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');

  let lineText = [];
  let lastInlineText = axObj;
  while (axObj && axObj.isValid) {
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.push(axObj.name.replace('AXValue: ', ''));
    lastInlineText = axObj;
    axObj = axObj.nextOnLine();
  }
  assert_array_equals(lineText, ['The ', 'quick', ' brown fox', '\n']);

  // Now walk backwards.
  lineText = [];
  axObj = lastInlineText;
  while (axObj && axObj.isValid) {
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.unshift(axObj.name.replace('AXValue: ', ''));
    axObj = axObj.previousOnLine();
  }
  assert_array_equals(lineText, ['The ', 'quick', ' brown fox', '\n']);
}, 'Test |NextOnLine| and |PreviousOnLine| on |AXInlineTextBox|.');

test(() => {
  let axEditable = accessibilityController.accessibleElementById('contentEditable');
  // There should be two lines in this content editable.
  assert_equals(axEditable.childrenCount, 2);
  let lines = [axEditable.childAtIndex(0), axEditable.childAtIndex(1)];
  let lineText = [
    ['Line ', 'one has one trailing space.'],
    ['Line', ' two has two leading tabs.']
  ];

  for (let i = 0; i < lines.length; ++i) {
    let currentLine = lines[i];
    assert_equals(currentLine.nextOnLine(), undefined);
    assert_equals(currentLine.previousOnLine(), undefined);
  }

  for (let i = 0; i < lines.length; ++i) {
    let currentLine = lines[i];
    let currentLineText = lineText[i];
    // There should be two spans per line since white space is removed.
    assert_equals(currentLine.childrenCount, 2);

    let span1 = currentLine.childAtIndex(0);
    let span2 = currentLine.childAtIndex(1);
    let inlineText1 = span1.childAtIndex(0);
    let inlineText2 = span2.childAtIndex(0);
    let spanText1 = currentLineText[0];
    let spanText2 = currentLineText[1];
    assert_equals(span1.role, 'AXRole: AXStaticText');
    assert_equals(span2.role, 'AXRole: AXStaticText');
    assert_equals(span1.name, spanText1);
    assert_equals(span2.name, spanText2);

    // |next/previousOnLine| APIs jump directly to the inline text boxes
    // skipping the parent span element.
    assert_equals(span1.nextOnLine(), inlineText2, 'span1 -> inlineText2');
    assert_equals(inlineText1.nextOnLine(), inlineText2, 'inlineText1 -> inlineText2');
    assert_equals(span2.previousOnLine(), inlineText1, 'span2 -> inlineText1');
    assert_equals(inlineText2.previousOnLine(), inlineText1, 'inlineText2 -> inlineText1');
  }
}, 'Test |NextOnLine| and |PreviousOnLine| on |AXLayoutObject|.');

test(() => {
  let axObj = accessibilityController.accessibleElementById('paragraphWithLink');
  // There should be two static text children and a link in this paragraph.
  assert_equals(axObj.childrenCount, 3);
  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXStaticText');

  let lineText = [];
  lineText.push(axObj.name);
  for (let i = 0; i < 2; ++i) {
    axObj = axObj.nextOnLine();
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.push(axObj.name);
  }
  assert_equals(axObj.nextOnLine(), undefined);

  for (let i = 0; i < 2; ++i) {
    axObj = axObj.previousOnLine();
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.push(axObj.name);
  }
  assert_equals(axObj.previousOnLine(), undefined);

  assert_array_equals(lineText, ['Paragraph with a ', 'link', ' inside.',
      'link', 'Paragraph with a ']);
}, 'Test |NextOnLine| and |PreviousOnLine| on paragraphs with links.');

test(() => {
  let axObj = accessibilityController.accessibleElementById('paragraphWithAnonymousInline');
  // There should be three static text children in this paragraph.
  assert_equals(axObj.childrenCount, 3);
  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXStaticText');

  let lineText = [];
  lineText.push(axObj.name);
  for (let i = 0; i < 2; ++i) {
    axObj = axObj.nextOnLine();
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.push(axObj.name);
  }
  assert_equals(axObj.nextOnLine(), undefined);

  for (let i = 0; i < 2; ++i) {
    axObj = axObj.previousOnLine();
    assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
    lineText.push(axObj.name);
  }
  assert_equals(axObj.previousOnLine(), undefined);

  assert_array_equals(lineText, ['Paragraph with an ', 'anonymous inline', ' inside.',
      'anonymous inline', 'Paragraph with an ']);
}, 'Test |NextOnLine| and |PreviousOnLine| on paragraphs with anonymous blocks.');

test(() => {
  let axObj = accessibilityController.accessibleElementById('list');
  // There should be two list items in this list.
  assert_equals(axObj.childrenCount, 2);

  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXListItem');
  // There should be a list marker and some text in this list item.
  assert_equals(axObj.childrenCount, 2);
  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXListMarker');

  let lineText = [];
  lineText.push(axObj.name);
  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  // Both the inline text box, as well as its static text parent, should be pointing to the same "previousOnLine".
  assert_equals(axObj.parentElement().previousOnLine(), axObj.previousOnLine());
  axObj = axObj.previousOnLine();
  axObj = axObj.parentElement();
  assert_equals(axObj.role, 'AXRole: AXListMarker');
  lineText.push(axObj.name);
  assert_array_equals(lineText, ['1. ', 'List item', '1. ']);

  axObj = accessibilityController.accessibleElementById('list');
  axObj = axObj.childAtIndex(1);
  assert_equals(axObj.role, 'AXRole: AXListItem');
  // There should be a list marker and a button in this list item.
  assert_equals(axObj.childrenCount, 2);
  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXListMarker');

  lineText = [];
  lineText.push(axObj.name);
  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXButton');
  lineText.push(axObj.name);
  axObj = axObj.previousOnLine();
  axObj = axObj.parentElement();
  assert_equals(axObj.role, 'AXRole: AXListMarker');
  lineText.push(axObj.name);
  assert_array_equals(lineText, ['2. ', 'button', '2. ']);
}, 'Test |NextOnLine| and |PreviousOnLine| on list markers.');

test(() => {
  let axObj = accessibilityController.accessibleElementById('multilineInline');

  // The tree is:
  //   [0] AXStaticText "This is a "
  //     AXInlineTextBox "This is a "
  //   [1] AXStaticText "test with a"
  //     AXInlineTextBox "test with a"
  //   [2] AXLineBreak "\n"
  //     AXInlineTextBox "\n"
  //   [3] AXStaticText "second "
  //     AXInlineTextBox "second "
  //   [4] AXStaticText "line."
  //     AXInlineTextBox "line."
  // There should be 5 static text children in this contenteditable.
  // The span is not in the accessibility tree at all.
  assert_equals(axObj.childrenCount, 5);

  const lineBreak = axObj.childAtIndex(2);
  assert_equals(lineBreak.role, 'AXRole: AXLineBreak');

  axObj = axObj.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXStaticText');


  //
  // Test first line.
  //

  let lineText = [];
  lineText.push(axObj.name);
  // Both the static text object as well as its only inline text box child,
  // should be connected to the same inline text box as their "nextOnLine".
  assert_equals(axObj.childrenCount, 1);
  assert_equals(axObj.childAtIndex(0).role, 'AXRole: AXInlineTextBox');
  assert_equals(axObj.childAtIndex(0).nextOnLine(), axObj.nextOnLine());

  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().nextOnLine(), lineBreak.childAtIndex(0),
                '"text with a".name.parentElement().nextOnLine()');
  assert_equals(axObj.nextOnLine(), lineBreak.childAtIndex(0),
                '"text with a".name.nextOnLine()');

  assert_equals(axObj.parentElement().previousOnLine(), axObj.previousOnLine());
  axObj = axObj.previousOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().previousOnLine(), undefined,
                '"This is a ".name.parentElement().nextOnLine()');
  assert_equals(axObj.previousOnLine(), undefined,
                '"This is a ".name.nextOnLine()');

  assert_array_equals(lineText, ['This is a ', 'test with a', 'This is a ']);

  //
  // Test second line.
  //

  axObj = accessibilityController.accessibleElementById('multilineInline');
  axObj = axObj.childAtIndex(4);
  assert_equals(axObj.role, 'AXRole: AXStaticText');

  lineText = [];
  lineText.push(axObj.name);
  // Both the static text object as well as its only inline text box child,
  // should be connected to the same inline text box as their "previousOnLine".
  assert_equals(axObj.childrenCount, 1);
  assert_equals(axObj.childAtIndex(0).role, 'AXRole: AXInlineTextBox');
  assert_equals(axObj.childAtIndex(0).previousOnLine(), axObj.previousOnLine());

  axObj = axObj.previousOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().previousOnLine(), undefined);
  assert_equals(axObj.previousOnLine(), undefined);

  assert_equals(axObj.parentElement().nextOnLine(), axObj.nextOnLine());
  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().nextOnLine(), undefined);
  assert_equals(axObj.nextOnLine(), undefined);

  assert_array_equals(lineText, ['line.', 'second ', 'line.']);
}, 'Test |NextOnLine| and |PreviousOnLine| on multiline inline elements.');

test(() => {
  let axEditable = accessibilityController.accessibleElementById('anonymousMultilineInline');
  // The tree is:
  //  [0] AXStaticText "This is a "
  //    AXInlineTextBox "This is a "
  //  [1] AXGenericContainer ""
  //    AXStaticText "test with a"
  //      AXInlineTextBox "test with a"
  //    AXLineBreak "\n"
  //      AXInlineTextBox "\n"
  //    AXStaticText "second "
  //      AXInlineTextBox "second "
  //  [2] AXStaticText "line."
  //    AXInlineTextBox "line."

  // There should be two static text objects and a span.
  // The span, represented by a generic container, should be the second child.
  assert_equals(axEditable.childrenCount, 3);
  axObj = axEditable.childAtIndex(0);
  assert_equals(axObj.role, 'AXRole: AXStaticText');

  const lineBreak = axEditable.childAtIndex(1).childAtIndex(1);
  assert_equals(lineBreak.role, 'AXRole: AXLineBreak');

  //
  // Test first line.
  //

  let lineText = [];
  lineText.push(axObj.name);
  // Both the static text object as well as its only inline text box child,
  // should be connected to the same inline text box as their "nextOnLine".
  assert_equals(axObj.childrenCount, 1);
  assert_equals(axObj.childAtIndex(0).role, 'AXRole: AXInlineTextBox');
  assert_equals(axObj.childAtIndex(0).nextOnLine(), axObj.nextOnLine());

  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().nextOnLine(), lineBreak.childAtIndex(0),
                '"This is a ".axObj.parentElement().nextOnLine()');
  assert_equals(axObj.nextOnLine(), lineBreak.childAtIndex(0),
                '"This is a ".axObj.nextOnLine()');
  // The span should also be connected to the inline text box that starts the first line.
  assert_equals(axEditable.childAtIndex(1).role, 'AXRole: AXGenericContainer');
  assert_equals(axObj.previousOnLine(),
                axEditable.childAtIndex(0).childAtIndex(0),
                'axEditable.childAtIndex(1).previousOnLine()');
  assert_equals(axObj.parentElement().previousOnLine(), axObj.previousOnLine(),
                '"test with a".parentElement().previousOnLine()');
  axObj = axObj.previousOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().previousOnLine(),
                undefined,
                '"This is a ".parentElement().previousOnLine()');
  assert_equals(axObj.previousOnLine(), undefined,
                '"This is a ".previousOnLine()');

  assert_array_equals(lineText, ['This is a ', 'test with a', 'This is a ']);

  //
  // Test second line.
  //

  axObj = axEditable.childAtIndex(2);
  assert_equals(axObj.role, 'AXRole: AXStaticText');

  lineText = [];
  lineText.push(axObj.name);
  // Both the static text object as well as its only inline text box child,
  // should be connected to the same inline text box as their "previousOnLine".
  assert_equals(axObj.childrenCount, 1);
  assert_equals(axObj.childAtIndex(0).role, 'AXRole: AXInlineTextBox');
  assert_equals(axObj.childAtIndex(0).previousOnLine(), axObj.previousOnLine(),
               '"line".childAtIndex(0).previousOnLine()');

  axObj = axObj.previousOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().previousOnLine(), undefined,
                '"second ".parentElement().previousOnLine()');
  assert_equals(axObj.previousOnLine(), undefined,
                '"second ".previousOnLine()');

  // The span should also be connected to the inline text box that ends the second line.
  assert_equals(axObj.nextOnLine(), axEditable.childAtIndex(2).childAtIndex(0),
                '"second ".nextOnLine()');
  assert_equals(axObj.parentElement().nextOnLine(),
                axEditable.childAtIndex(2).childAtIndex(0),
                '"second ".parentElement().nextOnLine()');

  axObj = axObj.nextOnLine();
  assert_equals(axObj.role, 'AXRole: AXInlineTextBox');
  lineText.push(axObj.name);
  assert_equals(axObj.parentElement().nextOnLine(), undefined,
                '"line.".parentElement().nextOnLine()');
  assert_equals(axObj.nextOnLine(), undefined,
                '"line.".nextOnLine()');

  assert_array_equals(lineText, ['line.', 'second ', 'line.']);
}, 'Test |NextOnLine| and |PreviousOnLine| on anonymous multiline inline elements.');

</script>