chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Include test fixture.
GEN_INCLUDE(['../testing/chromevox_e2e_test_base.js']);

/**
 * Test fixture for Live Regions.
 */
ChromeVoxLiveRegionsTest = class extends ChromeVoxE2ETest {
  async setUpDeferred() {
    await super.setUpDeferred();

    globalThis.EventType = chrome.automation.EventType;
    globalThis.RoleType = chrome.automation.RoleType;
    globalThis.TreeChangeType = chrome.automation.TreeChangeType;
  }

  /**
   * Simulates work done when users interact using keyboard, braille, or
   * touch.
   */
  simulateUserInteraction() {
    Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
  }
};


AX_TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionAddElement', async function() {
  const mockFeedback = this.createMockFeedback();
  const rootNode = await this.runWithLoadedTree(`
      <h1>Document with live region</h1>
      <p id="live" aria-live="assertive"></p>
      <button id="go">Go</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('live').innerHTML = 'Hello, world';
        }, false);
      </script>
    `);
  const go = rootNode.find({role: RoleType.BUTTON});
  mockFeedback.call(go.doDefault.bind(go))
      .expectCategoryFlushSpeech('Hello, world');
  await mockFeedback.replay();
});

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveRegionRemoveElement', async function() {
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
      <h1>Document with live region</h1>
      <p id="live" aria-live="assertive" aria-relevant="removals">Hello, world</p>
      <button id="go">Go</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('live').innerHTML = '';
        }, false);
      </script>
    `);
      const go = rootNode.find({role: RoleType.BUTTON});
      go.doDefault();
      mockFeedback.expectCategoryFlushSpeech('removed:')
          .expectQueuedSpeech('Hello, world');
      await mockFeedback.replay();
    });

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomic', async function() {
      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
      <div id="live" aria-live="assertive" aria-atomic="true">
        <div></div><div>Bravo</div><div></div>
      </div>
      <button id="go">Go</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.querySelectorAll('div div')[2].textContent = 'Charlie';
          document.querySelectorAll('div div')[0].textContent = 'Alpha';
        }, false);
      </script>
    `);
      const go = rootNode.find({role: RoleType.BUTTON});
      mockFeedback.call(go.doDefault.bind(go))
          .expectCategoryFlushSpeech('Alpha Bravo Charlie');
      await mockFeedback.replay();
    });

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomicText', async function() {
      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
      <h1 aria-atomic="true" id="live"aria-live="assertive">foo</h1>
      <button id="go">go</button>
      <script>
        document.getElementById('go').addEventListener('click', function(e) {
          document.getElementById('live').innerText = 'bar';
        });
      </script>
    `);
      const go = rootNode.find({role: RoleType.BUTTON});
      mockFeedback.call(go.doDefault.bind(go))
          .expectCategoryFlushSpeech('bar', 'Heading 1');
      await mockFeedback.replay();
    });

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveRegionChangeImageAlt', async function() {
      // Note that there is a live region outputted as a result of page load;
      // the test expects a live region announcement after a click on the
      // button, but the LiveRegions module has a half second filter for live
      // region announcements on the same node. Set that timeout to 0 to prevent
      // flakeyness.
      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
      <div id="live" aria-live="assertive">
        <img id="img" src="#" alt="Before">
      </div>
      <button id="go">Go</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('img').setAttribute('alt', 'After');
        }, false);
      </script>
    `);
      const go = rootNode.find({role: RoleType.BUTTON});
      mockFeedback.call(go.doDefault.bind(go))
          .expectCategoryFlushSpeech('After');
      await mockFeedback.replay();
    });

AX_TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionThenFocus', async function() {
  const mockFeedback = this.createMockFeedback();
  const rootNode = await this.runWithLoadedTree(`
      <div id="live" aria-live="assertive"></div>
      <button id="go">Go</button>
      <button id="focus">Focus</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('live').textContent = 'Live';
   setTimeout(function() {
            document.getElementById('focus').focus();
          }, 50);
        }, false);
      </script>
    `);
  // Due to the above timing component, the live region can come either
  // before or after the focus output. This depends on the EventBundle to
  // which we get the live region. It can either be in its own bundle or
  // be part of the bundle with the focus change. In either case, the
  // first event should be flushed; the second should either be queued (in
  // the case of the focus) or category flushed for the live region.
  let sawFocus = false;
  let sawLive = false;
  const focusOrLive = function(candidate) {
    sawFocus = candidate.text === 'Focus' || sawFocus;
    sawLive = candidate.text === 'Live' || sawLive;
    if (sawFocus && sawLive) {
      return candidate.queueMode !== QueueMode.FLUSH;
    } else if (sawFocus || sawLive) {
      return candidate.queueMode === QueueMode.FLUSH;
    }
  };
  const go = rootNode.find({role: RoleType.BUTTON});
  mockFeedback.call(this.simulateUserInteraction)
      .call(go.doDefault.bind(go))
      .expectSpeech(focusOrLive)
      .expectSpeech(focusOrLive);
  await mockFeedback.replay();
});

AX_TEST_F('ChromeVoxLiveRegionsTest', 'FocusThenLiveRegion', async function() {
  const mockFeedback = this.createMockFeedback();
  const rootNode = await this.runWithLoadedTree(`
      <div id="live" aria-live="assertive"></div>
      <button id="go">Go</button>
      <button id="focus">Focus</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('focus').focus();
   setTimeout(function() {
            document.getElementById('live').textContent = 'Live';
          }, 200);
        }, false);
      </script>
    `);
  const go = rootNode.find({role: RoleType.BUTTON});
  mockFeedback.call(this.simulateUserInteraction)
      .call(go.doDefault.bind(go))
      .expectSpeech('Focus')
      .expectSpeech(candidate => {
        return candidate.text === 'Live' &&
            (candidate.queueMode === QueueMode.CATEGORY_FLUSH ||
             candidate.queueMode === QueueMode.QUEUE);
      });
  await mockFeedback.replay();
});

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveRegionCategoryFlush', async function() {
      // Adjust the live region queue time to be shorter (i.e. flushes happen
      // for live regions coming 1 ms in time). Also, can help with flakeyness.
      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 1;
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
      <div id="live1" aria-live="assertive"></div>
      <div id="live2" aria-live="assertive"></div>
      <button id="go">Go</button>
      <button id="focus">Focus</button>
      <script>
        document.getElementById('go').addEventListener('click', function() {
          document.getElementById('live1').textContent = 'Live1';
          setTimeout(function() {
            document.getElementById('live2').textContent = 'Live2';
          }, 1000);
        }, false);
      </script>
    `);
      const go = rootNode.find({role: RoleType.BUTTON});
      mockFeedback.call(go.doDefault.bind(go))
          .expectCategoryFlushSpeech('Live1')
          .expectCategoryFlushSpeech('Live2');
      await mockFeedback.replay();
    });

AX_TEST_F('ChromeVoxLiveRegionsTest', 'SilentOnNodeChange', async function() {
  const mockFeedback = this.createMockFeedback();
  const rootNode = await this.runWithLoadedTree(`
    <p>start</p>
    <button>first</button>
    <div role="button" id="live" aria-live="assertive">
      hello!
    </div>
    <script>
      let live = document.getElementById('live');
      let pressed = true;
      setInterval(function() {
        live.setAttribute('aria-pressed', pressed);
        pressed = !pressed;
      }, 50);
    </script>
  `);
  const focusAfterNodeChange = setTimeout.bind(window, function() {
    root.firstChild.nextSibling.focus();
  }, 1000);
  mockFeedback.call(focusAfterNodeChange)
      .expectSpeech('hello!')
      .expectNextSpeechUtteranceIsNot('hello!')
      .expectNextSpeechUtteranceIsNot('hello!');
  await mockFeedback.replay();
});

AX_TEST_F('ChromeVoxLiveRegionsTest', 'SimulateTreeChanges', async function() {
  const mockFeedback = this.createMockFeedback();
  const root = await this.runWithLoadedTree(`
    <button></button>
    <div aria-live="assertive">
      <p>hello</p><p>there</p>
    </div>
  `);
  const live = new LiveRegions(ChromeVoxState.instance);
  const [t1, t2] = root.findAll({role: RoleType.STATIC_TEXT});
  mockFeedback.expectSpeech('hello there')
      .clearPendingOutput()
      .call(function() {
        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t2});
        live.onTreeChange(
            {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
      })
      .expectNextSpeechUtteranceIsNot('hello')
      .expectSpeech('there')
      .clearPendingOutput();
  mockFeedback
      .call(function() {
        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t1});
        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t2});
        live.onTreeChange(
            {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
      })
      .expectSpeech('hello')
      .expectSpeech('there');
  await mockFeedback.replay();
});

// Flaky: https://crbug.com/945199
AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'DISABLED_LiveStatusOff', async function() {
      const mockFeedback = this.createMockFeedback();
      const rootNode = await this.runWithLoadedTree(`
    <div><input aria-live="off" type="text"></input></div>
    <script>
      let input = document.querySelector('input');
      let div = document.querySelector('div');
      let clicks = 0;
      div.addEventListener('click', () => {
        clicks++;
        if (clicks === 1) {
          input.value = 'bb';
          input.selectionStart = 2;
          input.selectionEnd = 2;
        } else if (clicks === 2) {
          input.value = 'bba';
          input.selectionStart = 3;
          input.selectionEnd = 3;
        }
      });
    </script>
  `);
      const input = root.find({role: RoleType.TEXT_FIELD});
      const clickInput = input.parent.doDefault.bind(input.parent);
      mockFeedback.call(input.focus.bind(input))
          .call(clickInput)
          .expectSpeech('bb')
          .clearPendingOutput()
          .call(clickInput)
          .expectNextSpeechUtteranceIsNot('bba')
          .expectSpeech('a');
      await mockFeedback.replay();
    });

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'TreeChangeOnIgnoredNode', async function() {
      const mockFeedback = this.createMockFeedback();
      const root = await this.runWithLoadedTree(`
    <button></button>
    <script>
      const button = document.body.children[0];
      button.addEventListener('click', () => {
        const ignored = document.createElement('div');
        ignored.setAttribute('role', 'presentation');
        const alert = document.createElement('div');
        alert.setAttribute('role', 'alert');
        alert.textContent = 'hi';
        ignored.appendChild(alert);
        document.body.appendChild(ignored);
      });
    </script>
  `);
      const button = root.find({role: chrome.automation.RoleType.BUTTON});
      mockFeedback.call(button.doDefault.bind(button))
          .expectSpeech('Alert', 'hi');
      await mockFeedback.replay();
    });

AX_TEST_F('ChromeVoxLiveRegionsTest', 'ShouldIgnoreLiveRegion', function() {
  const liveRegions = new LiveRegions(ChromeVoxState.instance);

  const mockParentNode = {};
  mockParentNode.root = {role: chrome.automation.RoleType.DESKTOP};
  mockParentNode.state = {};

  const mockNode = {};
  mockNode.role = chrome.automation.RoleType.ROOT_WEB_AREA;
  mockNode.root = mockNode;
  mockNode.parent = mockParentNode;
  mockNode.state = {};

  mockParentNode.role = chrome.automation.RoleType.WINDOW;
  assertFalse(liveRegions.shouldIgnoreLiveRegion_(mockNode));
  mockParentNode.state[chrome.automation.StateType.INVISIBLE] = true;
  assertTrue(liveRegions.shouldIgnoreLiveRegion_(mockNode));
});

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'LiveIgnoredToUnignored', async function() {
      const mockFeedback = this.createMockFeedback();
      const root = await this.runWithLoadedTree(`
    <button></button>
    <div aria-live="polite" style="display:none">hello</div>
    <div aria-live="polite" hidden>there</div>
    <script>
      const [button, div1, div2] = document.body.children;
      let clickCount = 0;
      button.addEventListener('click', () => {
        clickCount++;
        switch (clickCount) {
          case 1:
            div1.style.display = 'block';
            break;
          case 2:
            div2.hidden = false;
        }
      });
    </script>
  `);
      const button = root.find({role: chrome.automation.RoleType.BUTTON});
      mockFeedback.call(button.doDefault.bind(button))
          .expectSpeech('hello')
          .call(button.doDefault.bind(button))
          .expectSpeech('there');
      await mockFeedback.replay();
    });

AX_TEST_F(
    'ChromeVoxLiveRegionsTest', 'AnnounceDesktopLiveRegionChanged',
    async function() {
      const mockFeedback = this.createMockFeedback();
      await this.runWithLoadedTree(``);

      const fakeEvent = containerLiveStatus => {
        return {
          target: {
            containerLiveStatus,
            name: containerLiveStatus,
            root: this.desktop_,
            children: [],
            standardActions: [],
            htmlAttributes: {},
            state: {},
            unclippedLocation: {},
            addEventListener() {},
            makeVisible() {},
            removeEventListener() {},
            setAccessibilityFocus() {},
          },
          type: EventType.LIVE_REGION_CHANGED,
        };
      };

      const onLiveRegionChanged = status => () =>
          DesktopAutomationInterface.instance.onLiveRegionChanged_(
              fakeEvent(status));

      mockFeedback.call(onLiveRegionChanged('assertive'))
          .expectSpeechWithQueueMode('assertive', QueueMode.CATEGORY_FLUSH)
          .call(onLiveRegionChanged('polite'))
          .expectSpeechWithQueueMode('polite', QueueMode.QUEUE);
      await mockFeedback.replay();
    });