chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_test.js

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
GEN_INCLUDE(['../../testing/chromevox_e2e_test_base.js']);

/**
 * Gets the braille output and asserts that it matches expected values.
 * Annotations in the output that are primitive strings are ignored.
 */
function checkBrailleOutput(expectedText, expectedSpans, output) {
  const actualOutput = output.braille;
  // Remove string annotations.  These are tested in the speech output and
  // there's no need to clutter the tests with the corresponding braille
  // annotations.
  const actualSpans =
      actualOutput.spans_.filter(span => (typeof span.value !== 'string'));
  checkOutput_(
      expectedText, expectedSpans, actualOutput.toString(), actualSpans);
}

function checkSpeechOutput(expectedText, expectedSpans, output) {
  const actualOutput = output.speechOutputForTest;
  checkOutput_(
      expectedText, expectedSpans, actualOutput.toString(),
      actualOutput.spans_);
}

/** @private */
function checkOutput_(expectedText, expectedSpans, actualText, actualSpans) {
  assertEquals(expectedText, actualText);

  function describeSpanPrettyPrint(span) {
    return describeSpan(span)
        .replace(':', ': ')
        .replace('"value":', 'value:')
        .replace('"start":', 'start:')
        .replace('"end":', 'end:')
        .replace('"', '\'');
  }

  function describeSpan(span) {
    const obj = {value: span.value, start: span.start, end: span.end};
    if (obj.value instanceof OutputNodeSpan) {
      obj.value.node =
          (obj.value.node.name || '') + ' ' + obj.value.node.toString();
    }
    return JSON.stringify(obj);
  }

  function describeActualSpans() {
    return '\nAll actual spans:\n' +
        actualSpans.map(describeSpanPrettyPrint).join('\n');
  }

  function describeExpectedSpans() {
    return '\nAll expected spans:\n' +
        expectedSpans.map(describeSpanPrettyPrint).join('\n');
  }

  for (let i = 0, max = Math.max(expectedSpans.length, actualSpans.length);
       i < max; ++i) {
    const expectedSpan = expectedSpans[i];
    const actualSpan = actualSpans[i];
    if (!expectedSpan) {
      throw Error(
          'Unexpected span in ' + expectedText + ': ' +
          describeSpan(actualSpan) + describeActualSpans()) +
          describeExpectedSpans();
    }
    if (!actualSpan) {
      throw Error(
          'Missing expected span in ' + expectedText + ': ' +
          describeSpan(expectedSpan) + describeActualSpans()) +
          describeExpectedSpans();
    }
    let equal = true;
    if (expectedSpan.start !== actualSpan.start ||
        expectedSpan.end !== actualSpan.end) {
      equal = false;
    } else if (
        expectedSpan.value instanceof OutputNodeSpan &&
        (!(actualSpan.value instanceof OutputNodeSpan) ||
         expectedSpan.value.node !== actualSpan.value.node)) {
      equal = false;
    } else {
      equal =
          (JSON.stringify(expectedSpan.value) ===
           JSON.stringify(actualSpan.value));
    }
    if (!equal) {
      throw Error(
          'Spans differ in this text: "' + expectedText + '":\n' +
          'Expected: ' + describeSpan(expectedSpan) + '\n' +
          'Got     : ' + describeSpan(actualSpan) + describeActualSpans()) +
          describeExpectedSpans();
    }
  }
}

/**
 * Test fixture for output.js.
 */
ChromeVoxOutputE2ETest = class extends ChromeVoxE2ETest {
  /** @override */
  async setUpDeferred() {
    await super.setUpDeferred();

    globalThis.Dir = AutomationUtil.Dir;
    globalThis.RoleType = chrome.automation.RoleType;
    this.forceContextualLastOutput();
  }
};


AX_TEST_F('ChromeVoxOutputE2ETest', 'Links', async function() {
  const root = await this.runWithLoadedTree('<a href="#">Click here</a>');
  const el = root.firstChild.firstChild;
  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'Click here|Internal link|Press Search+Space to activate',
        'spans_': [
          // Attributes.
          {value: 'name', start: 0, end: 10},

          // Link earcon (based on the name).
          {value: {earcon: EarconId.LINK}, start: 0, end: 10},

          {value: {'delay': true}, start: 25, end: 55},
        ],
      },
      o.speechOutputForTest);
  checkBrailleOutput(
      'Click here intlnk', [{value: new OutputNodeSpan(el), start: 0, end: 17}],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'Checkbox', async function() {
  const root = await this.runWithLoadedTree('<input type="checkbox">');
  const el = root.firstChild.firstChild;
  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      '|Check box|Not checked|Press Search+Space to toggle',
      [
        {value: new OutputEarconAction(EarconId.CHECK_OFF), start: 0, end: 0},
        {value: 'role', start: 1, end: 10},
        {value: {'delay': true}, start: 23, end: 51},
      ],
      o);
  checkBrailleOutput(
      'chk ( )', [{value: new OutputNodeSpan(el), start: 0, end: 7}], o);
});

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'InLineTextBoxValueGetsIgnored',
    async function() {
      const root = await this.runWithLoadedTree('<p>OK');
      let el = root.firstChild.firstChild.firstChild;
      assertEquals('inlineTextBox', el.role);
      let range = CursorRange.fromNode(el);
      let o = new Output().withSpeechAndBraille(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK',
            'spans_': [
              // Attributes.
              {value: 'name', start: 0, end: 2},
            ],
          },
          o.speechOutputForTest);
      checkBrailleOutput(
          'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);

      el = root.firstChild.firstChild;
      assertEquals('staticText', el.role);
      range = CursorRange.fromNode(el);
      o = new Output().withSpeechAndBraille(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK',
            'spans_': [
              // Attributes.
              {value: 'name', start: 0, end: 2},
            ],
          },
          o.speechOutputForTest);
      checkBrailleOutput(
          'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);
    });

AX_TEST_F('ChromeVoxOutputE2ETest', 'Headings', async function() {
  const root = await this.runWithLoadedTree(`
      <h1>a</h1><h2>b</h2><h3>c</h3><h4>d</h4><h5>e</h5><h6>f</h6>
      <h1><a href="a.com">b</a></h1> `);
  let el = root.firstChild;
  for (let i = 1; i <= 6; ++i) {
    const range = CursorRange.fromNode(el);
    const o = new Output().withSpeechAndBraille(range, null, 'navigate');
    const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
    assertEqualsJSON(
        {
          string_: letter + '|Heading ' + i,
          'spans_': [
            // Attributes.
            {value: 'nameOrDescendants', start: 0, end: 1},
          ],
        },
        o.speechOutputForTest);
    checkBrailleOutput(
        letter + ' h' + i, [{value: new OutputNodeSpan(el), start: 0, end: 4}],
        o);
    el = el.nextSibling;
  }

  range = CursorRange.fromNode(el);
  o = new Output().withSpeechAndBraille(range, null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'b|Link|Heading 1',
        'spans_': [
          {value: 'name', start: 0, end: 1},
          {value: new OutputEarconAction(EarconId.LINK), start: 0, end: 1},
          {value: 'role', start: 2, end: 6},
        ],
      },
      o.speechOutputForTest);
  checkBrailleOutput(
      'b lnk h1',
      [
        {value: new OutputNodeSpan(el.firstChild.firstChild), start: 0, end: 1},
        {value: new OutputNodeSpan(el), start: 0, end: 8},
        {value: new OutputNodeSpan(el.firstChild), start: 2, end: 5},
      ],
      o);
});

// TODO(crbug.com/41424286): test is flaky.
AX_TEST_F('ChromeVoxOutputE2ETest', 'DISABLED_Audio', async function() {
  const root =
      await this.runWithLoadedTree('<audio src="foo.mp3" controls></audio>');
  let el = root.find({role: RoleType.BUTTON});
  let range = CursorRange.fromNode(el);
  let o =
      new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');

  checkSpeechOutput(
      'play|Disabled|Button|audio|Tool bar',
      [
        {value: new OutputEarconAction(EarconId.BUTTON), start: 0, end: 4},
        {value: 'name', start: 21, end: 26},
        {value: 'role', start: 27, end: 35},
      ],
      o);

  checkBrailleOutput(
      'play xx btn audio tlbar',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 11},
        {value: new OutputNodeSpan(el.parent), start: 12, end: 23},
      ],
      o);

  // TODO(dtseng): Replace with a query.
  el = el.nextSibling.nextSibling.nextSibling;
  const prevRange = range;
  range = CursorRange.fromNode(el);
  o = new Output().withoutHints().withSpeechAndBraille(
      range, prevRange, 'navigate');
  checkSpeechOutput(
      '|audio time scrubber|Slider|0:00|Min 0|Max 0',
      [
        {value: 'name', start: 0, end: 0},
        {value: new OutputEarconAction(EarconId.SLIDER), start: 0, end: 0},
        {value: 'description', start: 1, end: 20},
        {value: 'role', start: 21, end: 27},
        {value: 'value', start: 28, end: 32},
      ],
      o);
  checkBrailleOutput(
      'audio time scrubber sldr 0:00 min:0 max:0',
      [{value: new OutputNodeSpan(el), start: 0, end: 41}], o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'Input', async function() {
  const root = await this.runWithLoadedTree(
      '<input type="text"></input>' +
      '<input type="email"></input>' +
      '<input type="password"></input>' +
      '<input type="tel"></input>' +
      '<input type="number"></input>' +
      '<input type="time"></input>' +
      '<input type="date"></input>' +
      '<input type="file"</input>' +
      '<input type="search"</input>' +
      '<input type="invalidType"</input>');
  const expectedSpansNonSearchBox = [
    {value: 'name', start: 0, end: 0},
    {value: new OutputEarconAction(EarconId.EDITABLE_TEXT), start: 0, end: 0},
    {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
    {value: 'value', start: 1, end: 1},
    {value: 'inputType', start: 2},
  ];
  const expectedSpansForSearchBox = [
    {value: 'name', start: 0, end: 0},
    {value: new OutputEarconAction(EarconId.EDITABLE_TEXT), start: 0, end: 0},
    {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
    {value: 'value', start: 1, end: 1},
    {value: 'role', start: 2, end: 8},
  ];

  const expectedSpeechValues = [
    '||Edit text',
    '||Edit text, email entry',
    '||Password edit text',
    '||Edit text numeric only',
    [
      '|Spin button',
      [
        {value: 'name', start: 0, end: 0},
        {value: new OutputEarconAction(EarconId.LISTBOX), start: 0, end: 0},
        {value: 'role', start: 1, end: 12},
      ],
    ],
    ['Time control', [{value: 'role', start: 0, end: 12}]],
    ['Date control', [{value: 'role', start: 0, end: 12}]],
    [
      'Choose File: No file chosen|Button',
      [
        {value: 'name', start: 0, end: 27},
        {value: new OutputEarconAction(EarconId.BUTTON), start: 0, end: 27},
        {value: 'role', start: 28, end: 34},
      ],
    ],
    '||Search',
    '||Edit text',
  ];
  // TODO(plundblad): Some of these are wrong, there should be an initial
  // space for the cursor in edit fields.
  const expectedBrailleValues = [
    ' ed',
    ' @ed 8dot',
    ' pwded',
    ' #ed',
    {string_: 'spnbtn', spans_: []},
    {string_: 'time'},
    {string_: 'date'},
    {string_: 'Choose File: No file chosen btn'},
    ' search',
    ' ed',
  ];
  assertEquals(expectedSpeechValues.length, expectedBrailleValues.length);

  let el = root.firstChild.firstChild;
  expectedSpeechValues.forEach(expectedValue => {
    const range = CursorRange.fromNode(el);
    const o = new Output().withoutHints().withSpeechAndBraille(
        range, null, 'navigate');
    let expectedSpansForValue = null;
    if (typeof expectedValue === 'object') {
      checkSpeechOutput(expectedValue[0], expectedValue[1], o);
    } else {
      expectedSpansForValue = expectedValue === '||Search' ?
          expectedSpansForSearchBox :
          expectedSpansNonSearchBox;
      expectedSpansForValue[4].end = expectedValue.length;
      checkSpeechOutput(expectedValue, expectedSpansForValue, o);
    }
    el = el.nextSibling;
  });

  el = root.firstChild.firstChild;
  expectedBrailleValues.forEach(expectedValue => {
    const range = CursorRange.fromNode(el);
    const o = new Output().withoutHints().withBraille(range, null, 'navigate');
    if (typeof expectedValue === 'string') {
      checkBrailleOutput(
          expectedValue,
          [
            {value: {startIndex: 0, endIndex: 0}, start: 0, end: 0},
            {
              value: new OutputNodeSpan(el),
              start: 0,
              end: expectedValue.length,
            },
          ],
          o);
    } else {
      let spans = [{
        value: new OutputNodeSpan(el),
        start: 0,
        end: expectedValue.string_.length,
      }];
      if (expectedValue.spans_) {
        spans = spans.concat(expectedValue.spans_);
      }

      checkBrailleOutput(expectedValue.string_, spans, o);
    }
    el = el.nextSibling;
  });
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'List', async function() {
  const root = await this.runWithLoadedTree(
      '<ul aria-label="first"><li aria-label="a">a<li>b<li>c</ul>');
  const el = root.firstChild.firstChild;
  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'a|List item|first|List|with 3 items',
      [
        {value: {earcon: EarconId.LIST_ITEM}, start: 0, end: 1},
        {value: 'name', start: 12, end: 17},
        {value: 'role', start: 18, end: 22},
      ],
      o);
  // TODO(plundblad): This output is wrong.  Add special handling for
  // braille here.
  checkBrailleOutput(
      'a lstitm first lst +3',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 8},
        {value: new OutputNodeSpan(el.parent), start: 9, end: 21},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ListWithoutSetSize', async function() {
  const root = await this.runWithLoadedTree(
      '<ul aria-label="first"><li aria-label="a">a<li>b<li>c</ul>');
  const el = root.firstChild.firstChild;
  Object.defineProperty(root.firstChild, 'setSize', {get: () => null});

  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');

  checkSpeechOutput(
      'a|List item|first|List',
      [
        {value: {earcon: EarconId.LIST_ITEM}, start: 0, end: 1},
        {value: 'name', start: 12, end: 17},
        {value: 'role', start: 18, end: 22},
      ],
      o);
  checkBrailleOutput(
      'a lstitm first lst',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 8},
        {value: new OutputNodeSpan(el.parent), start: 9, end: 18},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'Tree', async function() {
  const root = await this.runWithLoadedTree(`
    <ul role="tree" style="list-style-type:none">
      <li aria-expanded="true" role="treeitem">a
      <li role="treeitem">b
      <li aria-expanded="false" role="treeitem">c
    </ul>
  `);
  let el = root.firstChild.children[0].firstChild;
  let range = CursorRange.fromNode(el);
  let o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'a|Tree item|Expanded| 1 of 3 | level 1 |Tree|with 3 items',
      [
        {value: 'name', 'start': 0, end: 1},
        {value: 'state', start: 12, end: 20},
        {value: 'role', 'start': 40, end: 44},
      ],
      o);
  checkBrailleOutput(
      'a tritm - 1/3 level 1 tree +3',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 1},
        {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
        {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29},
      ],
      o);

  el = root.firstChild.children[1].firstChild;
  range = CursorRange.fromNode(el);
  o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'b|Tree item| 2 of 3 | level 1 |Tree|with 3 items',
      [
        {value: 'name', start: 0, end: 1},
        {value: 'role', 'start': 31, end: 35},
      ],
      o);
  checkBrailleOutput(
      'b tritm 2/3 level 1 tree +3',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 1},
        {value: new OutputNodeSpan(el.parent), start: 2, end: 20},
        {value: new OutputNodeSpan(el.parent.parent), start: 20, end: 27},
      ],
      o);

  el = root.firstChild.children[2].firstChild;
  range = CursorRange.fromNode(el);
  o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'c|Tree item|Collapsed| 3 of 3 | level 1 |Tree|with 3 items',
      [
        {value: 'name', 'start': 0, end: 1},
        {value: 'state', start: 12, end: 21},
        {value: 'role', 'start': 41, end: 45},
      ],
      o);
  checkBrailleOutput(
      'c tritm + 3/3 level 1 tree +3',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 1},
        {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
        {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'Menu', async function() {
  const site = `
    <div role="menu">
      <div role="menuitem">a</div>
      <div role="menuitemcheckbox">b</div>
      <div role="menuitemradio">c</div>
    </div>
    <div role="menubar" aria-orientation="horizontal"></div>
  `;
  const root = await this.runWithLoadedTree(site);
  let el = root.firstChild.firstChild;
  let range = CursorRange.fromNode(el);
  let o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'a|Menu item| 1 of 3 |Menu',
      [{value: 'name', start: 0, end: 1}, {value: 'role', start: 21, end: 25}],
      o);
  checkBrailleOutput(
      'a mnuitm 1/3 mnu',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 12},
        {value: new OutputNodeSpan(el.parent), start: 13, end: 16},
      ],
      o);

  // Ancestry.
  el = root.firstChild;
  range = CursorRange.fromNode(el);
  o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'Menu|with 3 items|' +
          'Press up or down arrow to navigate; enter to activate',
      [
        {value: 'role', start: 0, end: 4},
        {value: {delay: true}, start: 18, end: 71},
      ],
      o);

  el = root.lastChild;
  range = CursorRange.fromNode(el);
  o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'Menu bar|Press left or right arrow to navigate; enter to activate',
      [
        {value: 'role', start: 0, end: 8},
        {value: {delay: true}, start: 9, end: 65},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ListBox', async function() {
  const root = await this.runWithLoadedTree(`
    <select multiple>
      <option>1</option>
      <option>2</option>
    </select>
  `);
  const el = root.firstChild.firstChild.firstChild;
  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      '1|List item| 1 of 2 |Not selected|List box|with 2 items',
      [
        {value: 'name', start: 0, end: 1},
        {value: new OutputEarconAction(EarconId.LIST_ITEM), start: 0, end: 1},
        {value: 'role', start: 34, end: 42},
      ],
      o);
  checkBrailleOutput(
      '1 lstitm 1/2 ( ) lstbx +2',
      [
        {value: new OutputNodeSpan(el), start: 0, end: 16},
        {value: new OutputNodeSpan(el.parent), start: 17, end: 25},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'MessageIdAndEarconValidity', function() {
  const kNoBrailleMessageRequired = new Set([
    'comment',
    'contentDeletion',
    'contentInsertion',
    'docAbstract',
    'docAcknowledgments',
    'docAfterword',
    'docAppendix',
    'docBackLink',
    'docBiblioEntry',
    'docBibliography',
    'docBiblioRef',
    'docChapter',
    'docColophon',
    'docConclusion',
    'docCover',
    'docCredit',
    'docCredits',
    'docDedication',
    'docEndnote',
    'docEndnotes',
    'docEpigraph',
    'docEpilogue',
    'docErrata',
    'docExample',
    'docFootnote',
    'docForeword',
    'docGlossary',
    'docGlossRef',
    'docIndex',
    'docIntroduction',
    'docNoteRef',
    'docNotice',
    'docPageBreak',
    'docPageFooter',
    'docPageHeader',
    'docPageList',
    'docPart',
    'docPreface',
    'docPrologue',
    'docPullquote',
    'docQna',
    'docSubtitle',
    'docTip',
    'docToc',
    'graphicsDocument',
    'graphicsObject',
    'graphicsSymbol',
    'suggestion',
  ]);
  for (const key in OutputRoleInfo) {
    const value = OutputRoleInfo[key];
    if (value.msgId) {
      Msgs.getMsg(value.msgId);
      if (!kNoBrailleMessageRequired.has(key)) {
        Msgs.getMsg(value.msgId + '_brl');
      }
      assertFalse(/[A-Z]+/.test(value.msgId));
    }
  }
  for (const key in Output.STATE_INFO_) {
    const value = Output.STATE_INFO_[key];
    for (innerKey in value) {
      const innerValue = value[innerKey];
      if (typeof (innerValue) === 'boolean') {
        assertEquals('isRoleSpecific', innerKey);
        continue;
      }
      Msgs.getMsg(innerValue.msgId);
      Msgs.getMsg(innerValue.msgId + '_brl');
      assertFalse(/[A-Z]+/.test(innerValue.msgId));
    }
  }
  for (const key in Output.INPUT_TYPE_MESSAGE_IDS_) {
    const msgId = Output.INPUT_TYPE_MESSAGE_IDS_[key];
    assertFalse(/[A-Z]+/.test(msgId));
    Msgs.getMsg(msgId);
    Msgs.getMsg(msgId + '_brl');
  }
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'DivOmitsRole', async function() {
  const root = await this.runWithLoadedTree(`
    <div>that has content</div>
    <div></div>
    <div role='group'><div>nested content</div></div>
  `);
  const el = root.firstChild.firstChild;
  const range = CursorRange.fromNode(el);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'that has content', [{value: 'name', start: 0, end: 16}], o);
  checkBrailleOutput(
      'that has content', [{value: new OutputNodeSpan(el), start: 0, end: 16}],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'LessVerboseAncestry', async function() {
  const root = await this.runWithLoadedTree(`
    <div role="banner"><p>inside</p></div>
    <div role="banner"><p>inside</p></div>
    <div role="navigation"><p>inside</p></div>
  `);
  const first = root.children[0].firstChild;
  const second = root.children[1].firstChild;
  const third = root.children[2].firstChild;
  const firstRange = CursorRange.fromNode(first);
  const secondRange = CursorRange.fromNode(second);
  const thirdRange = CursorRange.fromNode(third);

  const oWithoutPrev = new Output().withSpeech(firstRange, null, 'navigate');
  const oWithPrev =
      new Output().withSpeech(secondRange, firstRange, 'navigate');
  const oWithPrevExit =
      new Output().withSpeech(thirdRange, secondRange, 'navigate');
  assertEquals('inside|Banner', oWithoutPrev.speechOutputForTest.string_);

  // Make sure we don't read the exited ancestry change.
  assertEquals('inside|Banner', oWithPrev.speechOutputForTest.string_);

  // Different role; do read the exited ancestry here.
  assertEquals('inside|Navigation', oWithPrevExit.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'Brief', async function() {
  const root = await this.runWithLoadedTree(`
    <div role="article"><p>inside</p></div>
  `);
  const node = root.children[0].firstChild;
  const range = CursorRange.fromNode(node);

  SettingsManager.set('useVerboseMode', false);
  const oWithoutPrev = new Output().withSpeech(range, null, 'navigate');
  assertEquals('inside', oWithoutPrev.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'AuralStyledHeadings', async function() {
  function toFixed(num) {
    return parseFloat(Number(num).toFixed(1));
  }
  const root = await this.runWithLoadedTree(`
      <h1>a</h1><h2>b</h2><h3>c</h3><h4>d</h4><h5>e</h5><h6>f</h6>
      <h1><a href="a.com">b</a></h1> `);
  let el = root.firstChild;
  for (let i = 1; i <= 6; ++i) {
    const range = CursorRange.fromNode(el);
    const o = new Output().withRichSpeech(range, null, 'navigate');
    const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
    assertEqualsJSON(
        {
          string_: letter + '|Heading ' + i,
          'spans_': [
            // Aural styles.
            {value: {'relativePitch': toFixed(-0.1 * i)}, start: 0, end: 0},

            // Attributes.
            {value: 'nameOrDescendants', start: 0, end: 1},

            {value: {'relativePitch': -0.2}, start: 2, end: 2},
          ],
        },
        o.speechOutputForTest);
    el = el.nextSibling;
  }
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ToggleButton', async function() {
  const root = await this.runWithLoadedTree(`
      <div role="button" aria-pressed="true">Subscribe</div>`);
  const el = root.firstChild;
  const o = new Output().withSpeechAndBraille(CursorRange.fromNode(el));
  assertEqualsJSON(
      {
        string_:
            '|Subscribe|Toggle Button|Pressed|Press Search+Space to toggle',
        spans_: [
          {value: {earcon: EarconId.CHECK_ON}, start: 0, end: 0},
          {value: 'name', start: 1, end: 10},
          {value: 'role', start: 11, end: 24},
          {value: {'delay': true}, start: 33, end: 61},
        ],
      },
      o.speechOutputForTest);
  assertEquals('Subscribe tgl btn =', o.braille.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'JoinDescendants', async function() {
  const root = await this.runWithLoadedTree(`
      <p>This</p>
      <p>fragment</p>
      <p>Should be separated</p>
      <p>with spaces</p>
    `);
  const unjoined = new Output().format('$descendants', root);
  assertEquals(
      'This|fragment|Should be separated|with spaces',
      unjoined.speechOutputForTest.string_);

  const joined = new Output().format('$joinedDescendants', root);
  assertEquals(
      'This fragment Should be separated with spaces',
      joined.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ComplexDiv', async function() {
  const root = await this.runWithLoadedTree(`
      <div><button>ok</button></div>
    `);
  const div = root.find({role: RoleType.GENERIC_CONTAINER});
  const o = new Output().withSpeech(CursorRange.fromNode(div));
  assertEquals('ok', o.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ContainerFocus', async function() {
  const root = await this.runWithLoadedTree(`
      <div role="grid">
        <div role="row" tabindex=0 aria-label="start"></div>
        <div role="row" tabindex=0 aria-label="end"></div>
      </div>
    `);
  const r1 = CursorRange.fromNode(root.firstChild.firstChild);
  const r2 = CursorRange.fromNode(root.firstChild.lastChild);
  assertEquals(
      'start|Row', new Output().withSpeech(r1, r2).speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'BraileWhitespace', async function() {
  const root = await this.runWithLoadedTree(`
    <p>this is a <em>test</em>of emphasized text</p>
  `);
  const start = root.firstChild.firstChild;
  const end = root.firstChild.lastChild;
  const range = new CursorRange(Cursor.fromNode(start), Cursor.fromNode(end));
  const o = new Output().withBraille(range, null, 'navigate');
  checkBrailleOutput(
      'this is a test of emphasized text',
      [
        {value: new OutputNodeSpan(start), start: 0, end: 10},
        {
          value: new OutputNodeSpan(start.nextSibling.firstChild),
          start: 10,
          end: 14,
        },
        {value: new OutputNodeSpan(end), start: 15, end: 33},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'BrailleAncestry', async function() {
  const root = await this.runWithLoadedTree(`
    <ul><li><a href="#">test</a></li></ul>
  `);
  const link = root.find({role: RoleType.LINK});
  // The 'inlineTextBox' found from root would return the inlineTextBox of
  // the list marker. Here we want the link's inlineTextBox.
  const text = link.find({role: RoleType.INLINE_TEXT_BOX});
  const listItem = root.find({role: RoleType.LIST_ITEM});
  const list = root.find({role: RoleType.LIST});
  let range = CursorRange.fromNode(text);
  let o = new Output().withBraille(range, null, 'navigate');
  checkBrailleOutput(
      'test lnk lstitm lst end',
      [
        {value: new OutputNodeSpan(text), start: 0, end: 4},
        {value: new OutputNodeSpan(link), start: 5, end: 8},
        {value: new OutputNodeSpan(listItem), start: 9, end: 15},
        {value: new OutputNodeSpan(list), start: 16, end: 23},
      ],
      o);

  // Now, test the "bullet" which comes before the above.
  const bullet = root.find({role: RoleType.LIST_MARKER});
  range = CursorRange.fromNode(bullet);
  o = new Output().withBraille(range, null, 'navigate');
  checkBrailleOutput(
      '\u2022 lstitm lst +1',
      [
        {value: new OutputNodeSpan(bullet), start: 0, end: 2},
        {value: new OutputNodeSpan(listItem), start: 2, end: 8},
        {value: new OutputNodeSpan(list), start: 9, end: 15},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'RangeOutput', async function() {
  const root = await this.runWithLoadedTree(`
    <div role="slider" aria-valuemin="1" aria-valuemax="10" aria-valuenow="2"
                       aria-label="volume"></div>
    <progress aria-valuemin="1" aria-valuemax="10"
              aria-valuenow="2" aria-label="volume"></progress>
    <meter aria-valuemin="1" aria-valuemax="10" aria-valuenow="2"
           aria-label="volume"></meter>
    <div role="spinbutton" aria-valuemin="1" aria-valuemax="10"
                           aria-valuenow="2" aria-label="volume"></div>
  `);
  let obj = root.find({role: RoleType.SLIDER});
  let o = new Output().withoutHints().withSpeech(CursorRange.fromNode(obj));
  checkSpeechOutput(
      'volume|Slider|2|Min 1|Max 10',
      [
        {value: 'name', start: 0, end: 6},
        {value: new OutputEarconAction(EarconId.SLIDER), start: 0, end: 6},
        {value: 'role', start: 7, end: 13},
        {value: 'value', start: 14, end: 15},
      ],
      o);

  obj = root.find({role: RoleType.PROGRESS_INDICATOR});
  o = new Output().withoutHints().withSpeech(CursorRange.fromNode(obj));
  checkSpeechOutput(
      'volume|Progress indicator|2|Min 1|Max 10',
      [
        {value: 'name', start: 0, end: 6},
        {value: 'role', start: 7, end: 25},
        {value: 'value', start: 26, end: 27},
      ],
      o);

  obj = root.find({role: RoleType.METER});
  o = new Output().withoutHints().withSpeech(CursorRange.fromNode(obj));
  checkSpeechOutput(
      'volume|Meter|2|Min 1|Max 10',
      [
        {value: 'name', start: 0, end: 6},
        {value: 'role', start: 7, end: 12},
        {value: 'value', start: 13, end: 14},
      ],
      o);

  obj = root.find({role: RoleType.SPIN_BUTTON});
  o = new Output().withoutHints().withSpeech(CursorRange.fromNode(obj));
  checkSpeechOutput(
      'volume|Spin button|2|Min 1|Max 10',
      [
        {value: 'name', start: 0, end: 6},
        {value: new OutputEarconAction(EarconId.LISTBOX), start: 0, end: 6},
        {value: 'role', start: 7, end: 18},
        {value: 'value', start: 19, end: 20},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'RoleDescription', async function() {
  const root = await this.runWithLoadedTree(`
    <div aria-label="hi" role="button" aria-roledescription="foo"></div>
  `);
  const obj = root.find({role: RoleType.BUTTON});
  const o = new Output().withoutHints().withSpeech(CursorRange.fromNode(obj));
  checkSpeechOutput(
      'hi|foo',
      [
        {value: 'name', start: 0, end: 2},
        {value: new OutputEarconAction(EarconId.BUTTON), start: 0, end: 2},
        {value: 'role', start: 3, end: 6},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ValidateCommonProperties', function() {
  // If you fail this test, you likely need to insert a $state, $restriction or
  // $description into the output rules for the printed roles. Typically,
  // $description goes towards the end of the output rule, though this depends
  // on the role. For example, it could make sense to put $description before
  // $value or $state.

  // You can also add the role to be excluded from this check. You are
  // encouraged to write a more intelligent output rule to provide friendlier
  // feedback. For example, 'not selected apple item 2 out of 3' coming from a
  // message template like '@smart_selection($state, $name, $indexInParent,
  // $childCount)'.
  // In such cases, you are responsible for ensuring you include all states and
  // descriptions somewhere in the output.

  const stateStr = '$state';
  const restrictionStr = '$restriction';
  const descStr = '$description';
  let missingState = [];
  let missingRestriction = [];
  let missingDescription = [];
  for (const key in OutputRule.RULES.navigate) {
    const speak = OutputRule.RULES.navigate[key].speak;
    if (!speak) {
      continue;
    }

    if (speak.indexOf(stateStr) === -1) {
      missingState.push(key);
    }
    if (speak.indexOf(restrictionStr) === -1) {
      missingRestriction.push(key);
    }
    if (speak.indexOf(descStr) === -1) {
      missingDescription.push(key);
    }
  }

  // This filters out known roles that don't have states or descriptions.
  const notStated = [
    RoleType.CLIENT,
    RoleType.EMBEDDED_OBJECT,
    RoleType.IME_CANDIDATE,
    RoleType.INLINE_TEXT_BOX,
    RoleType.LINE_BREAK,
    RoleType.LIST_MARKER,
    RoleType.PARAGRAPH,
    RoleType.ROOT_WEB_AREA,
    RoleType.STATIC_TEXT,
    RoleType.PLUGIN_OBJECT,
    RoleType.WINDOW,
  ];
  const notRestricted = [
    RoleType.ALERT,
    RoleType.ALERT_DIALOG,
    RoleType.CELL,
    RoleType.CLIENT,
    RoleType.EMBEDDED_OBJECT,
    RoleType.GENERIC_CONTAINER,
    RoleType.GRID_CELL,
    RoleType.IMAGE,
    RoleType.IME_CANDIDATE,
    RoleType.INLINE_TEXT_BOX,
    RoleType.LINE_BREAK,
    RoleType.LIST,
    RoleType.LIST_MARKER,
    RoleType.PARAGRAPH,
    RoleType.PLUGIN_OBJECT,
    RoleType.REGION,
    RoleType.ROOT_WEB_AREA,
    RoleType.ROW_HEADER,
    RoleType.STATIC_TEXT,
    RoleType.TABLE_HEADER_CONTAINER,
    RoleType.TIMER,
    RoleType.WINDOW,
  ];
  const notDescribed = [
    RoleType.CLIENT,
    RoleType.EMBEDDED_OBJECT,
    RoleType.IME_CANDIDATE,
    RoleType.INLINE_TEXT_BOX,
    RoleType.LINE_BREAK,
    RoleType.LIST_MARKER,
    RoleType.PARAGRAPH,
    RoleType.PLUGIN_OBJECT,
    RoleType.ROOT_WEB_AREA,
    RoleType.STATIC_TEXT,
    RoleType.WINDOW,
  ];
  missingState = missingState.filter(state => notStated.indexOf(state) === -1);
  missingRestriction = missingRestriction.filter(
      restriction => notRestricted.indexOf(restriction) === -1);
  missingDescription =
      missingDescription.filter(desc => notDescribed.indexOf(desc) === -1);

  assertEquals(
      0, missingState.length,
      'Unexpected missing states for output rules ' + missingState.join(' '));
  assertEquals(
      0, missingRestriction.length,
      'Unexpected missing restriction for output rules ' +
          missingRestriction.join(' '));
  assertEquals(
      0, missingDescription.length,
      'Unexpected missing descriptions for output rules ' +
          missingDescription.join(' '));
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ValidateRoles', function() {
  // If you fail this test, you likely need to insert a $role or
  // $roledescription into the output rules for the printed roles. Typically,
  // roles can be omitted (intentionally), but role descriptions cannot by W3C
  // spec (valid on all base markup). However, not all roles come from the web.
  //
  // Some enter rules do not need roles, depending on how they are used such as
  // if their speak rule is always triggered during navigation e.g. buttons.
  //
  // You can also add the role to be excluded from this check. You are
  // encouraged to write a more intelligent output rule to provide friendlier
  // feedback, but keep in mind role descriptions are required on all web-based
  // role.

  const roleOrRoleDescStr = '$role';
  const missingRole = [];
  const allowedMissingRoles = [
    RoleType.CLIENT,
    RoleType.GENERIC_CONTAINER,
    RoleType.EMBEDDED_OBJECT,
    RoleType.IME_CANDIDATE,
    RoleType.INLINE_TEXT_BOX,
    RoleType.LINE_BREAK,
    RoleType.LIST_MARKER,
    RoleType.ROOT_WEB_AREA,
    RoleType.STATIC_TEXT,
    RoleType.WINDOW,
  ];
  for (const key in OutputRule.RULES.navigate) {
    if (allowedMissingRoles.indexOf(key) !== -1) {
      continue;
    }
    const speak = OutputRule.RULES.navigate[key].speak;
    let enter = OutputRule.RULES.navigate[key].enter;
    if (enter && enter.speak) {
      enter = enter.speak;
    }

    if (speak && speak.indexOf(roleOrRoleDescStr) === -1) {
      missingRole.push(key);
    }
    if (enter && enter.indexOf(roleOrRoleDescStr) === -1) {
      missingRole.push(key);
    }
  }
  assertEquals(
      0, missingRole.length,
      'Unexpected missing role or role description for output rules ' +
          missingRole.join(' '));
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'InlineBraille', async function() {
  const root = await this.runWithLoadedTree(`
    <table border=1>
      <tr><td>Name</td><td id="active">Age</td><td>Address</td></tr>
    </table>
  `);
  const obj = root.find({role: RoleType.CELL});
  const o = new Output().withRichSpeechAndBraille(CursorRange.fromNode(obj));
  assertEquals(
      'Name|row 1 column 1|Table , 1 by 3', o.speechOutputForTest.string_);
  assertEquals('Name r1c1 Age r1c2 Address r1c3', o.braille.string_);
});

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'TextFieldObeysRoleDescription',
    async function() {
      const root = await this.runWithLoadedTree(`
    <div role="textbox" aria-roledescription="square"></div>
    <div role="region" aria-roledescription="circle"></div>
  `);
      const text = root.find({role: RoleType.TEXT_FIELD});

      // True even though |text| does not have editable state.
      assertTrue(AutomationPredicate.editText(text));

      let o = new Output().withRichSpeechAndBraille(CursorRange.fromNode(text));
      assertEquals('|square', o.speechOutputForTest.string_);
      assertEquals('square', o.braille.string_);

      const region = root.find({role: RoleType.REGION});
      o = new Output().withRichSpeechAndBraille(CursorRange.fromNode(region));
      assertEquals('circle', o.speechOutputForTest.string_);
      assertEquals('circle', o.braille.string_);
    });

AX_TEST_F('ChromeVoxOutputE2ETest', 'NestedList', async function() {
  const root = await this.runWithLoadedTree(`
    <ul role="tree">schedule
      <li role="treeitem">wake up
      <li role="treeitem">drink coffee
      <ul role="tree">tasks
        <li role="treeitem" aria-level='2'>meeting
        <li role="treeitem" aria-level='2'>lunch
      </ul>
      <li role="treeitem">cook dinner
    </ul>
  `);
  const lists = root.findAll({role: RoleType.TREE});
  const outerList = lists[0];
  const innerList = lists[1];

  let el = outerList.children[0];
  let startRange = CursorRange.fromNode(el);
  let o = new Output().withSpeech(startRange, null, 'navigate');
  assertEquals('schedule|Tree|with 3 items', o.speechOutputForTest.string_);

  el = outerList.children[1];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(outerList.children[0]), 'navigate');
  assertEquals(
      'wake up|Tree item|Not selected| 1 of 3 | level 1 ',
      o.speechOutputForTest.string_);

  el = outerList.children[2];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(outerList.children[0]), 'navigate');
  assertEquals(
      'drink coffee|Tree item|Not selected| 2 of 3 | level 1 ',
      o.speechOutputForTest.string_);

  el = outerList.children[3];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(outerList.children[0]), 'navigate');
  assertEquals(
      'cook dinner|Tree item|Not selected| 3 of 3 | level 1 ',
      o.speechOutputForTest.string_);

  el = innerList.children[0];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(outerList.children[2]), 'navigate');
  assertEquals('tasks|Tree|with 2 items', o.speechOutputForTest.string_);

  el = innerList.children[1];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(innerList.children[0]), 'navigate');
  assertEquals(
      'meeting|Tree item|Not selected| 1 of 2 | level 2 ',
      o.speechOutputForTest.string_);

  el = innerList.children[2];
  startRange = CursorRange.fromNode(el);
  o = new Output().withSpeech(
      startRange, CursorRange.fromNode(innerList.children[0]), 'navigate');
  assertEquals(
      'lunch|Tree item|Not selected| 2 of 2 | level 2 ',
      o.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'NoTooltipWithNameTitle', async function() {
  const root = await this.runWithLoadedTree(`
    <div role="group" title="title"></div>
    <div role="group" aria-label="label" title="title"></div>
    <div role="group" aria-describedby="desc" title="title"></div>
    <div role="group" aria-label="label" aria-describedby="desc" title="title">
    </div>
    <div role="group" aria-label=""></div>
    <p id="desc">describedby</p>
  `);
  const title = root.children[0];
  let o =
      new Output().withSpeech(CursorRange.fromNode(title), null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'title',
        spans_: [
          {value: 'nameOrDescendants', start: 0, end: 5},
        ],
      },
      o.speechOutputForTest);

  const labelTitle = root.children[1];
  o = new Output().withSpeech(
      CursorRange.fromNode(labelTitle), null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'label|title',
        spans_: [
          {value: 'nameOrDescendants', start: 0, end: 5},
          {value: 'description', start: 6, end: 11},
        ],
      },
      o.speechOutputForTest);

  const describedByTitle = root.children[2];
  o = new Output().withSpeech(
      CursorRange.fromNode(describedByTitle), null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'title|describedby',
        spans_: [
          {value: 'nameOrDescendants', start: 0, end: 5},
          {value: 'description', start: 6, end: 17},
        ],
      },
      o.speechOutputForTest);

  const labelDescribedByTitle = root.children[3];
  o = new Output().withSpeech(
      CursorRange.fromNode(labelDescribedByTitle), null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'label|describedby',
        spans_: [
          {value: 'nameOrDescendants', start: 0, end: 5},
          {value: 'description', start: 6, end: 17},
        ],
      },
      o.speechOutputForTest);

  // Hijack the 4th node to force tooltip to return a value. This can only
  // occur on ARC++ where tooltip gets set even if name and description
  // are both empty.
  const tooltip = root.children[4];
  Object.defineProperty(root.children[4], 'tooltip', {get: () => 'tooltip'});

  o = new Output().withSpeech(CursorRange.fromNode(tooltip), null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'tooltip',
        spans_: [{value: {'delay': true}, start: 0, end: 7}],
      },
      o.speechOutputForTest);
});

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'InitialSpeechProperties', async function() {
      const root = await this.runWithLoadedTree(`
    <p>test</p>  `);
      // Capture speech properties sent to tts.
      this.currentProperties = [];
      ChromeVox.tts.speak = (textString, queueMode, properties) => {
        this.currentProperties.push(properties);
      };

      const o = new Output().withSpeech(CursorRange.fromNode(root.firstChild));
      o.go();
      assertEqualsJSON([{category: TtsCategory.NAV}], this.currentProperties);
      this.currentProperties = [];

      o.withInitialSpeechProperties({
        phoneticCharacters: true,
        // This should not override existing value.
        category: TtsCategory.LIVE,
      });
      o.go();
      assertEqualsJSON(
          [{phoneticCharacters: true, category: TtsCategory.NAV}],
          this.currentProperties);
    });

AX_TEST_F('ChromeVoxOutputE2ETest', 'NameOrTextContent', async function() {
  const root = await this.runWithLoadedTree(`
        <div tabindex=0>
          <div aria-label="hello there world">
            <p>hello world</p>
          </div>
        </div>
      `);
  const focusableDiv = root.firstChild;
  assertEquals(RoleType.GENERIC_CONTAINER, focusableDiv.role);
  assertEquals(chrome.automation.NameFromType.CONTENTS, focusableDiv.nameFrom);
  const o = new Output().withSpeech(CursorRange.fromNode(focusableDiv));
  assertEquals('hello there world', o.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'AriaCurrentHint', async function() {
  const site = `
      <div aria-current="page">Home</div>
      <div aria-current="false">About</div>
      `;
  const root = await this.runWithLoadedTree(site);
  const currentDiv = root.firstChild;
  assertEquals(
      chrome.automation.AriaCurrentState.PAGE, currentDiv.ariaCurrentState);
  const o = new Output().withSpeech(
      CursorRange.fromNode(currentDiv), null, 'navigate');
  assertEquals('Home|Current page', o.speechOutputForTest.string_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'DelayHintVariants', async function() {
  const root = await this.runWithLoadedTree(`
    <div aria-errormessage="error" aria-invalid="true">OK</div>
    <div id="error" aria-label="error"></div>
  `);
  const div = root.children[0];
  const range = CursorRange.fromNode(div);

  let o = new Output().withSpeech(range, null, 'navigate');
  assertEqualsJSON(
      {string_: 'OK|error', spans_: [{value: 'name', start: 3, end: 8}]},
      o.speechOutputForTest);

  // Force a few properties to be set so that hints are triggered.
  Object.defineProperty(div, 'clickable', {get: () => true});

  o = new Output().withSpeech(range, null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'OK|error|Press Search+Space to activate',
        spans_: [
          {value: 'name', start: 3, end: 8},
          {value: {delay: true}, start: 9, end: 39},
        ],
      },
      o.speechOutputForTest);

  Object.defineProperty(div, 'placeholder', {get: () => 'placeholder'});
  o = new Output().withSpeech(range, null, 'navigate');
  assertEqualsJSON(
      {
        string_: 'OK|error|placeholder|Press Search+Space to activate',
        spans_: [
          {value: 'name', start: 3, end: 8},
          {value: {delay: true}, start: 9, end: 20},
          {start: 21, end: 51},
        ],
      },
      o.speechOutputForTest);
});

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'DelayHintWithActionLabel', async function() {
      const site = `<button>OK</button>`;
      const root = await this.runWithLoadedTree(site);
      const button = root.children[0];
      const range = CursorRange.fromNode(button);

      let o = new Output().withSpeech(range, null, 'navigate');

      // Force a few properties to be set so that hints are triggered.
      Object.defineProperty(button, 'clickable', {get: () => true});

      Object.defineProperty(
          button, 'doDefaultLabel', {get: () => 'click label'});
      o = new Output().withSpeech(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK|Press Search+Space to click label',
            spans_: [{value: {delay: true}, start: 3, end: 36}],
          },
          o.speechOutputForTest);
    });

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'DelayHintVariantsWithTouch', async function() {
      const site = `<button>OK</button>`;
      const root = await this.runWithLoadedTree(site);
      const button = root.children[0];
      const range = CursorRange.fromNode(button);

      // Force a few properties to be set so that hints are triggered.
      Object.defineProperty(button, 'clickable', {get: () => true});
      EventSource.set(EventSourceType.TOUCH_GESTURE);

      o = new Output().withSpeech(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK|Double tap to activate',
            spans_: [
              {value: {delay: true}, start: 3, end: 25},
            ],
          },
          o.speechOutputForTest);

      Object.defineProperty(button, 'doDefaultLabel', {get: () => 'tap label'});

      o = new Output().withSpeech(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK|Double tap to tap label',
            spans_: [{value: {delay: true}, start: 3, end: 26}],
          },
          o.speechOutputForTest);
    });

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'DelayHintWithLongClickLabel', async function() {
      const site = `<button>OK</button>`;
      const root = await this.runWithLoadedTree(site);
      const button = root.children[0];
      const range = CursorRange.fromNode(button);

      let o = new Output().withSpeech(range, null, 'navigate');

      // Force a few properties to be set so that hints are triggered.
      Object.defineProperty(button, 'longClickable', {get: () => true});
      Object.defineProperty(
          button, 'longClickLabel', {get: () => 'long click label'});

      o = new Output().withSpeech(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK|Press Search+Shift+Space to long click label',
            spans_: [{value: {delay: true}, start: 3, end: 47}],
          },
          o.speechOutputForTest);
    });

AX_TEST_F(
    'ChromeVoxOutputE2ETest', 'DelayHintLongClickFollowsClick',
    async function() {
      const site = `<button>OK</button>`;
      const root = await this.runWithLoadedTree(site);
      const button = root.children[0];
      const range = CursorRange.fromNode(button);

      let o = new Output().withSpeech(range, null, 'navigate');

      // Force a few properties to be set so that hints are triggered.
      Object.defineProperty(button, 'clickable', {get: () => true});
      Object.defineProperty(button, 'longClickable', {get: () => true});

      o = new Output().withSpeech(range, null, 'navigate');
      assertEqualsJSON(
          {
            string_: 'OK|Press Search+Space to activate|' +
                'Press Search+Shift+Space to long click',
            spans_: [
              {value: {delay: true}, start: 3, end: 33},
              {start: 34, end: 72},
            ],
          },
          o.speechOutputForTest);
    });

AX_TEST_F('ChromeVoxOutputE2ETest', 'WithoutFocusRing', async function() {
  const site = `<button></button>`;
  const root = await this.runWithLoadedTree(site);
  let called = false;
  FocusBounds.set = this.newCallback(() => {
    called = true;
  });

  const button = root.find({role: RoleType.BUTTON});

  // Triggers drawing of the focus ring.
  new Output().withSpeech(CursorRange.fromNode(button)).go();
  assertTrue(called);
  called = false;

  // Does not trigger drawing of the focus ring.
  new Output().withSpeech(CursorRange.fromNode(button)).withoutFocusRing().go();
  assertFalse(called);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ARCCheckbox', async function() {
  const root = await this.runWithLoadedTree('<input type="checkbox">');
  const checkbox = root.firstChild.firstChild;

  Object.defineProperty(
      checkbox, 'checkedStateDescription',
      {get: () => 'checked state description'});
  const range = CursorRange.fromNode(checkbox);
  const o =
      new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      '|Check box|checked state description',
      [
        {value: new OutputEarconAction(EarconId.CHECK_OFF), start: 0, end: 0},
        {value: 'role', start: 1, end: 10},
        {value: 'checkedStateDescription', start: 11, end: 36},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ARCCustomAction', async function() {
  const root = await this.runWithLoadedTree('<p>test</p>');
  const actionable = root.firstChild.firstChild;
  Object.defineProperty(actionable, 'customActions', {
    get: () => [{id: 0, description: 'custom action description'}],
  });
  const range = CursorRange.fromNode(actionable);
  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
  checkSpeechOutput(
      'test|Actions available. Press Search+Ctrl+A to view',
      [
        {value: 'name', start: 0, end: 4},
        {value: {delay: true}, start: 5, end: 51},
      ],
      o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ContextOrder', async function() {
  this.resetContextualOutput();
  const root =
      await this.runWithLoadedTree('<p>test</p><div role="menu">a</div>');
  let o = new Output().withSpeech(CursorRange.fromNode(root));
  assertEquals('last', o.contextOrder_);

  const p = root.find({role: RoleType.PARAGRAPH});
  const menu = root.find({role: RoleType.MENU});
  o = new Output().withSpeech(
      CursorRange.fromNode(p), CursorRange.fromNode(menu));
  assertEquals('last', o.contextOrder_);

  o = new Output().withSpeech(
      CursorRange.fromNode(menu), CursorRange.fromNode(p));
  assertEquals('first', o.contextOrder_);

  o = new Output().withSpeech(
      CursorRange.fromNode(menu.firstChild), CursorRange.fromNode(p));
  assertEquals('first', o.contextOrder_);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'TreeGridLevel', async function() {
  const site = `
    <table id="treegrid" role="treegrid" aria-label="Inbox">
      <tbody>
        <tr role="row" aria-level="1" aria-posinset="1" aria-setsize="1"
            aria-expanded="true" aria-label="Treegrid faq">
          <td role="gridcell">Treegrids are awesome</td>
          <td role="gridcell">Want to learn how to use them?</td>
        </tr>
      </tbody>
    </table>
  `;
  const root = await this.runWithLoadedTree(site);
  const row = root.find({role: RoleType.ROW});
  let range = CursorRange.fromNode(row);
  let o =
      new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');

  checkSpeechOutput(
      ' level 1 |Treegrid faq|Expanded|Row',
      [
        {value: 'name', 'start': 10, 'end': 22},
        {value: 'state', start: 23, end: 31},
        {value: 'role', start: 32, end: 35},
      ],
      o);

  range = CursorRange.fromNode(row.firstChild);
  o = new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');

  checkSpeechOutput(
      'Treegrids are awesome| level 1 |row 1 column 1',
      [{value: 'name', start: 0, end: 21}], o);

  range = CursorRange.fromNode(row.lastChild);
  o = new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');

  checkSpeechOutput(
      'Want to learn how to use them?|row 1 column 2',
      [{value: 'name', start: 0, end: 30}], o);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'FocusFollowText', async function() {
  const site = `<p>Hello World</p>
                <button>Button</button>
                <div>New Div</div>`;
  const root = await this.runWithLoadedTree(site);
  let called = false;
  let actualBounds = {};

  // Mock call to Accessibility Common extension
  chrome.accessibilityPrivate.setChromeVoxFocus = ((bounds) => {
    called = true;
    actualBounds = bounds;
  });

  // Triggers drawing of the focus ring on text node
  const text = root.find({role: RoleType.STATIC_TEXT});
  new Output().withSpeech(CursorRange.fromNode(text)).go();
  assertTrue(called);
  assertEquals(text.location.left, actualBounds.left);
  assertEquals(text.location.top, actualBounds.top);
  assertEquals(text.location.width, actualBounds.width);
  assertEquals(text.location.height, actualBounds.height);

  // Shift focus to interactive element
  const button = root.find({role: RoleType.BUTTON});
  new Output().withSpeech(CursorRange.fromNode(button)).go();
  assertEquals(button.location.left, actualBounds.left);
  assertEquals(button.location.top, actualBounds.top);
  assertEquals(button.location.width, actualBounds.width);
  assertEquals(button.location.height, actualBounds.height);

  // Ensure focus is shifted to new type of node that contains text
  const div = root.find({role: RoleType.GENERIC_CONTAINER});
  new Output().withSpeech(CursorRange.fromNode(div)).go();
  assertEquals(div.location.left, actualBounds.left);
  assertEquals(div.location.top, actualBounds.top);
  assertEquals(div.location.width, actualBounds.width);
  assertEquals(div.location.height, actualBounds.height);
});

AX_TEST_F('ChromeVoxOutputE2ETest', 'ToStringEmptyOutput', async function() {
  assertEquals('', new Output().toString());
});