| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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_next_e2e_test_base.js']); |
| |
| GEN_INCLUDE(['../testing/fake_objects.js']); |
| |
| /** |
| * Test fixture for Background. |
| */ |
| ChromeVoxBackgroundTest = class extends ChromeVoxNextE2ETest { |
| /** @override */ |
| setUp() { |
| super.setUp(); |
| window.doGesture = this.doGesture; |
| window.simulateHitTestResult = this.simulateHitTestResult; |
| window.press = this.press; |
| window.Mod = constants.ModifierFlag; |
| window.ActionType = chrome.automation.ActionType; |
| |
| this.forceContextualLastOutput(); |
| } |
| |
| simulateHitTestResult(node) { |
| return () => { |
| GestureCommandHandler.pointerHandler_.handleHitTestResult(node); |
| }; |
| } |
| |
| press(keyCode, modifiers) { |
| return function() { |
| EventGenerator.sendKeyPress(keyCode, modifiers); |
| }; |
| } |
| |
| get linksAndHeadingsDoc() { |
| return ` |
| <p>start</p> |
| <a href='#a'>alpha</a> |
| <a href='#b'>beta</a> |
| <p> |
| <h1>charlie</h1> |
| <a href='foo'>delta</a> |
| </p> |
| <a href='#bar'>echo</a> |
| <h2>foxtraut</h2> |
| <p>end<span>of test</span></p> |
| `; |
| } |
| |
| get buttonDoc() { |
| return ` |
| <p>start</p> |
| <button>hello button one</button> |
| cats |
| <button>hello button two</button> |
| <p>end</p> |
| `; |
| } |
| |
| get formsDoc() { |
| return ` |
| <select id="fruitSelect"> |
| <option>apple</option> |
| <option>grape</option> |
| <option> banana</option> |
| </select> |
| `; |
| } |
| |
| get iframesDoc() { |
| return ` |
| <p>start</p> |
| <button>Before</button> |
| <iframe srcdoc="<button>Inside</button><h1>Inside</h1>"></iframe> |
| <button>After</button> |
| `; |
| } |
| |
| get disappearingObjectDoc() { |
| return ` |
| <p>start</p> |
| <div role="group"> |
| <p>Before1</p> |
| <p>Before2</p> |
| <p>Before3</p> |
| </div> |
| <div role="group"> |
| <p id="disappearing">Disappearing</p> |
| </div> |
| <div role="group"> |
| <p>After1</p> |
| <p>After2</p> |
| <p>After3</p> |
| </div> |
| <div id="live" aria-live="assertive"></div> |
| <div id="delete" role="button">Delete</div> |
| <script> |
| document.getElementById('delete').addEventListener('click', function() { |
| let d = document.getElementById('disappearing'); |
| d.parentElement.removeChild(d); |
| document.getElementById('live').innerText = 'Deleted'; |
| document.body.offsetTop |
| }); |
| </script> |
| `; |
| } |
| |
| get detailsDoc() { |
| return ` |
| <p aria-details="details">start</p> |
| <div role="group"> |
| <p>Before</p> |
| <p id="details">Details</p> |
| <p>After</p> |
| </div> |
| `; |
| } |
| |
| get comboBoxDoc() { |
| return ` |
| <div id="combo-box-label">Choose an item</div> |
| <div aria-labelledby="combo-box-label" role="combobox"> |
| <input type="text" aria-controls="combo-box-list-box"> |
| <ul role="listbox" id="combo-box-list-box" hidden> |
| <li role="option" tabindex="-1">Item 1</li> |
| <li role="option" tabindex="-1">Item 2</li> |
| </ul> |
| </div> |
| `; |
| } |
| |
| get listBoxDoc() { |
| return ` |
| <p>Start</p> |
| <div role="listbox" aria-expanded="false" aria-label="Select an item"> |
| <div aria-selected="true" tabindex="0" role="option"> |
| <span>Listbox item one</span> |
| </div> |
| <div aria-selected="false" tabindex="-1" role="option"> |
| <span>Listbox item two</span> |
| </div> |
| <div aria-selected="false" role="option"> |
| <span>Listbox item three</span> |
| </div> |
| </div> |
| <button>Click</button> |
| `; |
| } |
| |
| get nestedListDoc() { |
| return ` |
| <div> |
| <ul> |
| <li>Lemons</li> |
| <li>Oranges</li> |
| <li>Berries |
| <ul> |
| <li>Strawberries</li> |
| <li>Raspberries</li> |
| </ul> |
| </li> |
| <li>Bananas</li> |
| </ul> |
| </div> |
| `; |
| } |
| |
| /** |
| * Fires an onCustomSpokenFeedbackToggled event with enabled state of |
| * |enabled|. |
| * @param {boolean} enabled TalkBack-enabled state. |
| */ |
| dispatchOnCustomSpokenFeedbackToggledEvent(enabled) { |
| chrome.accessibilityPrivate.onCustomSpokenFeedbackToggled.dispatch(enabled); |
| } |
| }; |
| |
| /** |
| * Specific test fixture for tests that need a test server running. |
| */ |
| ChromeVoxBackgroundTestWithTestServer = class extends ChromeVoxBackgroundTest { |
| get testServer() { |
| return true; |
| } |
| }; |
| |
| |
| |
| /** Tests that ChromeVox classic is in this context. */ |
| SYNC_TEST_F('ChromeVoxBackgroundTest', 'ClassicNamespaces', function() { |
| assertEquals('function', typeof (ChromeVoxBackground)); |
| }); |
| |
| /** Tests that ChromeVox's background object is not available globally. */ |
| SYNC_TEST_F('ChromeVoxBackgroundTest', 'NextNamespaces', function() { |
| assertEquals(undefined, window.Background); |
| }); |
| |
| /** Tests consistency of navigating forward and backward. */ |
| TEST_F('ChromeVoxBackgroundTest', 'ForwardBackwardNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { |
| mockFeedback.expectSpeech('start').expectBraille('start'); |
| |
| mockFeedback.call(doCmd('nextLink')) |
| .expectSpeech('alpha', 'Link') |
| .expectBraille('alpha lnk'); |
| mockFeedback.call(doCmd('nextLink')) |
| .expectSpeech('beta', 'Link') |
| .expectBraille('beta lnk'); |
| mockFeedback.call(doCmd('nextLink')) |
| .expectSpeech('delta', 'Link') |
| .expectBraille('delta lnk'); |
| mockFeedback.call(doCmd('previousLink')) |
| .expectSpeech('beta', 'Link') |
| .expectBraille('beta lnk'); |
| mockFeedback.call(doCmd('nextHeading')) |
| .expectSpeech('charlie', 'Heading 1') |
| .expectBraille('charlie h1'); |
| mockFeedback.call(doCmd('nextHeading')) |
| .expectSpeech('foxtraut', 'Heading 2') |
| .expectBraille('foxtraut h2'); |
| mockFeedback.call(doCmd('previousHeading')) |
| .expectSpeech('charlie', 'Heading 1') |
| .expectBraille('charlie h1'); |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('delta', 'Link') |
| .expectBraille('delta lnk'); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('echo', 'Link') |
| .expectBraille('echo lnk'); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('foxtraut', 'Heading 2') |
| .expectBraille('foxtraut h2'); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('end') |
| .expectBraille('end'); |
| mockFeedback.call(doCmd('previousObject')) |
| .expectSpeech('foxtraut', 'Heading 2') |
| .expectBraille('foxtraut h2'); |
| mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut'); |
| mockFeedback.call(doCmd('nextLine')) |
| .expectSpeech('end', 'of test') |
| .expectBraille('endof test'); |
| |
| mockFeedback.call(doCmd('jumpToTop')) |
| .expectSpeech('start') |
| .expectBraille('start'); |
| mockFeedback.call(doCmd('jumpToBottom')) |
| .expectSpeech('of test') |
| .expectBraille('of test'); |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'CaretNavigation', function() { |
| // TODO(plundblad): Add braille expectations when crbug.com/523285 is fixed. |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { |
| mockFeedback.expectSpeech('start'); |
| mockFeedback.call(doCmd('nextCharacter')).expectSpeech('t'); |
| mockFeedback.call(doCmd('nextCharacter')).expectSpeech('a'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('alpha', 'Link'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('alpha', 'Link'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('charlie', 'Heading 1'); |
| mockFeedback.call(doCmd('nextLine')).expectSpeech('delta', 'Link'); |
| mockFeedback.call(doCmd('nextLine')).expectSpeech('echo', 'Link'); |
| mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut', 'Heading 2'); |
| mockFeedback.call(doCmd('nextLine')).expectSpeech('end', 'of test'); |
| mockFeedback.call(doCmd('nextCharacter')).expectSpeech('n'); |
| mockFeedback.call(doCmd('previousCharacter')).expectSpeech('e'); |
| mockFeedback.call(doCmd('previousCharacter')) |
| .expectSpeech('t', 'Heading 2'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('foxtraut'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('echo', 'Link'); |
| mockFeedback.call(doCmd('previousCharacter')).expectSpeech('a', 'Link'); |
| mockFeedback.call(doCmd('previousCharacter')).expectSpeech('t'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('echo', 'Link'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| /** Tests that individual buttons are stops for move-by-word functionality. */ |
| TEST_F( |
| 'ChromeVoxBackgroundTest', 'CaretNavigationMoveThroughButtonByWord', |
| function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.buttonDoc, function() { |
| mockFeedback.expectSpeech('start'); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('hello button one', 'Button'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('start'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('hello'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('button'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('one'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('cats'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('hello'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('button'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('two'); |
| mockFeedback.call(doCmd('nextWord')).expectSpeech('end'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('two'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('button'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('hello'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('cats'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('one'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('button'); |
| mockFeedback.call(doCmd('previousWord')).expectSpeech('hello'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SelectSingleBasic', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.formsDoc, function() { |
| mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed') |
| .expectBraille('apple btn +popup +3 +') |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('grape', /2 of 3/) |
| .expectBraille('grape 2/3') |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('banana', /3 of 3/) |
| .expectBraille('banana 3/3'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ContinuousRead', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.linksAndHeadingsDoc, function() { |
| mockFeedback.expectSpeech('start') |
| .call(doCmd('readFromHere')) |
| .expectSpeech( |
| 'start', 'alpha', 'Link', 'beta', 'Link', 'charlie', 'Heading 1'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'InitialFocus', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<a href="a">a</a>', function(rootNode) { |
| mockFeedback.expectSpeech('a').expectSpeech('Link'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AriaLabel', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = '<a aria-label="foo" href="a">a</a>'; |
| this.runWithLoadedTree(site, function(rootNode) { |
| rootNode.find({role: RoleType.LINK}).focus(); |
| mockFeedback.expectSpeech('foo') |
| .expectSpeech('Link') |
| .expectSpeech('Press Search+Space to activate') |
| .expectBraille('foo lnk'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenu', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>before</p><a href="a">a</a>', function(rootNode) { |
| const go = rootNode.find({role: RoleType.LINK}); |
| mockFeedback.call(go.focus.bind(go)) |
| .expectSpeech('a', 'Link') |
| .call(doCmd('contextMenu')) |
| .expectSpeech(/menu opened/) |
| .call(press(KeyCode.ESCAPE)) |
| .expectSpeech('a', 'Link'); |
| mockFeedback.replay(); |
| }.bind(this)); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'BrailleRouting', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const route = function(position) { |
| assertTrue(ChromeVoxState.instance.onBrailleKeyEvent( |
| {command: BrailleKeyCommand.ROUTING, displayPosition: position}, |
| mockFeedback.lastMatchedBraille)); |
| }; |
| const site = ` |
| <p>start</p> |
| <button type="button" id="btn1">Click me</button> |
| <p>Some text</p> |
| <button type="button" id="btn2">Focus me</button> |
| <p>Some more text</p> |
| <input type="text" id ="text" value="Edit me"> |
| <script> |
| document.getElementById('btn1').addEventListener('click', function() { |
| document.getElementById('btn2').focus(); |
| }, false); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| const button1 = |
| rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Click me'}}); |
| const textField = rootNode.find({role: RoleType.TEXT_FIELD}); |
| mockFeedback.expectBraille('start') |
| .call(button1.focus.bind(button1)) |
| .expectBraille(/^Click me btn/) |
| .call(route.bind(null, 5)) |
| .expectBraille(/Focus me btn/) |
| .call(textField.focus.bind(textField)) |
| .expectBraille('Edit me ed', {startIndex: 0}) |
| .call(route.bind(null, 3)) |
| .expectBraille('Edit me ed', {startIndex: 3}) |
| .call(function() { |
| assertEquals(3, textField.textSelStart); |
| }); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'FocusInputElement', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <input id="name" value="Lancelot"> |
| <input id="quest" value="Grail"> |
| <input id="color" value="Blue"> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| const name = rootNode.find({attributes: {value: 'Lancelot'}}); |
| const quest = rootNode.find({attributes: {value: 'Grail'}}); |
| const color = rootNode.find({attributes: {value: 'Blue'}}); |
| |
| mockFeedback.call(quest.focus.bind(quest)) |
| .expectSpeech('Grail', 'Edit text') |
| .call(color.focus.bind(color)) |
| .expectSpeech('Blue', 'Edit text') |
| .call(name.focus.bind(name)) |
| .expectNextSpeechUtteranceIsNot('Blue') |
| .expectSpeech('Lancelot', 'Edit text'); |
| mockFeedback.replay(); |
| }.bind(this)); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'UseEditableState', function() { |
| const site = ` |
| <input type="text"></input> |
| <p tabindex=0>hi</p> |
| `; |
| this.runWithLoadedTree(site, async function(rootNode) { |
| const nonEditable = rootNode.find({role: RoleType.PARAGRAPH}); |
| const editable = rootNode.find({role: RoleType.TEXT_FIELD}); |
| |
| nonEditable.focus(); |
| await new Promise(resolve => { |
| this.listenOnce(nonEditable, 'focus', resolve); |
| }); |
| assertTrue(!DesktopAutomationHandler.instance.textEditHandler); |
| |
| editable.focus(); |
| await new Promise(resolve => { |
| this.listenOnce(editable, 'focus', resolve); |
| }); |
| assertNotNullNorUndefined( |
| DesktopAutomationHandler.instance.textEditHandler); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EarconsForControls', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Initial focus will be on something that's not a control.</p> |
| <a href="#">MyLink</a> |
| <button>MyButton</button> |
| <input type=checkbox> |
| <input type=checkbox checked> |
| <input> |
| <select multiple><option>1</option></select> |
| <select><option>2</option></select> |
| <input type=range value=5> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('MyLink') |
| .expectEarcon(Earcon.LINK) |
| .call(doCmd('nextObject')) |
| .expectSpeech('MyButton') |
| .expectEarcon(Earcon.BUTTON) |
| .call(doCmd('nextObject')) |
| .expectSpeech('Check box') |
| .expectEarcon(Earcon.CHECK_OFF) |
| .call(doCmd('nextObject')) |
| .expectSpeech('Check box') |
| .expectEarcon(Earcon.CHECK_ON) |
| .call(doCmd('nextObject')) |
| .expectSpeech('Edit text') |
| .expectEarcon(Earcon.EDITABLE_TEXT) |
| |
| // Editable text Search re-mappings are in effect. |
| .call(doCmd('toggleStickyMode')) |
| .expectSpeech('Sticky mode enabled') |
| .call(doCmd('nextObject')) |
| .expectSpeech('List box') |
| .expectEarcon(Earcon.LISTBOX) |
| .call(doCmd('nextObject')) |
| .expectSpeech('Button', 'has pop up') |
| .expectEarcon(Earcon.POP_UP_BUTTON) |
| .call(doCmd('nextObject')) |
| .expectSpeech(/Slider/) |
| .expectEarcon(Earcon.SLIDER); |
| |
| mockFeedback.replay(); |
| }.bind(this)); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'GlobsToRegExp', function() { |
| this.newCallback(async () => { |
| const module = await import('./background.js'); |
| const Background = module.Background; |
| assertEquals('/^()$/', Background.globsToRegExp_([]).toString()); |
| assertEquals( |
| '/^(http:\\/\\/host\\/path\\+here)$/', |
| Background.globsToRegExp_(['http://host/path+here']).toString()); |
| assertEquals( |
| '/^(url1.*|u.l2|.*url3)$/', |
| Background.globsToRegExp_(['url1*', 'u?l2', '*url3']).toString()); |
| })(); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ShouldNotFocusIframe', function() { |
| const site = ` |
| <iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe> |
| <button>outside</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const iframe = root.find({role: RoleType.IFRAME}); |
| const button = root.find({role: RoleType.BUTTON}); |
| |
| assertEquals('iframe', iframe.role); |
| assertEquals('button', button.role); |
| |
| let didFocus = false; |
| iframe.addEventListener('focus', function() { |
| didFocus = true; |
| }); |
| const b = ChromeVoxState.instance; |
| b.currentRange_ = cursors.Range.fromNode(button); |
| doCmd('previousElement'); |
| assertFalse(didFocus); |
| }.bind(this)); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ShouldFocusLink', function() { |
| const site = ` |
| <div><a href="#">mylink</a></div> |
| <button>after</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const link = root.find({role: RoleType.LINK}); |
| const button = root.find({role: RoleType.BUTTON}); |
| |
| assertEquals('link', link.role); |
| assertEquals('button', button.role); |
| |
| const didFocus = false; |
| link.addEventListener('focus', this.newCallback(function() { |
| // Success |
| })); |
| const b = ChromeVoxState.instance; |
| b.currentRange_ = cursors.Range.fromNode(button); |
| doCmd('previousElement'); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NoisySlider', function() { |
| const mockFeedback = this.createMockFeedback(); |
| // Slider aria-valuetext must change otherwise blink suppresses event. |
| const site = ` |
| <button id="go">go</button> |
| <div id="slider" tabindex=0 role="slider"></div> |
| <script> |
| function update() { |
| let s = document.getElementById('slider'); |
| s.setAttribute('aria-valuetext', ''); |
| s.setAttribute('aria-valuetext', 'noisy'); |
| setTimeout(update, 500); |
| } |
| update(); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const go = root.find({role: RoleType.BUTTON}); |
| const slider = root.find({role: RoleType.SLIDER}); |
| const focusButton = go.focus.bind(go); |
| const focusSlider = slider.focus.bind(slider); |
| mockFeedback.call(focusButton) |
| .expectNextSpeechUtteranceIsNot('noisy') |
| .call(focusSlider) |
| .expectSpeech('noisy') |
| .expectSpeech('noisy') |
| .replay(); |
| }.bind(this)); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'Checkbox', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div id="go" role="checkbox">go</div> |
| <script> |
| let go = document.getElementById('go'); |
| let isChecked = true; |
| go.addEventListener('click', function(e) { |
| if (isChecked) { |
| go.setAttribute('aria-checked', true); |
| } else { |
| go.removeAttribute('aria-checked'); |
| } |
| isChecked = !isChecked; |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const cbx = root.find({role: RoleType.CHECK_BOX}); |
| const click = cbx.doDefault.bind(cbx); |
| const focus = cbx.focus.bind(cbx); |
| mockFeedback.call(focus) |
| .expectSpeech('go') |
| .expectSpeech('Check box') |
| .expectSpeech('Not checked') |
| .call(click) |
| .expectSpeech('go') |
| .expectSpeech('Check box') |
| .expectSpeech('Checked') |
| .call(click) |
| .expectSpeech('go') |
| .expectSpeech('Check box') |
| .expectSpeech('Not checked') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MixedCheckbox', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = '<div id="go" role="checkbox" aria-checked="mixed">go</div>'; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('go', 'Check box', 'Partially checked').replay(); |
| }); |
| }); |
| |
| /** Tests navigating into and out of iframes using nextButton */ |
| TEST_F( |
| 'ChromeVoxBackgroundTest', 'ForwardNavigationThroughIframeButtons', |
| function() { |
| const mockFeedback = this.createMockFeedback(); |
| |
| let running = false; |
| const runTestIfIframeIsLoaded = function(rootNode) { |
| if (running) { |
| return; |
| } |
| |
| // Return if the iframe hasn't loaded yet. |
| const iframe = rootNode.find({role: RoleType.IFRAME}); |
| const childDoc = iframe.firstChild; |
| if (!childDoc || childDoc.children.length === 0) { |
| return; |
| } |
| |
| running = true; |
| const beforeButton = |
| rootNode.find({role: RoleType.BUTTON, name: 'Before'}); |
| beforeButton.focus(); |
| mockFeedback.expectSpeech('Before', 'Button'); |
| mockFeedback.call(doCmd('nextButton')).expectSpeech('Inside', 'Button'); |
| mockFeedback.call(doCmd('nextButton')).expectSpeech('After', 'Button'); |
| mockFeedback.call(doCmd('previousButton')) |
| .expectSpeech('Inside', 'Button'); |
| mockFeedback.call(doCmd('previousButton')) |
| .expectSpeech('Before', 'Button'); |
| mockFeedback.replay(); |
| }.bind(this); |
| |
| this.runWithLoadedTree(this.iframesDoc, function(rootNode) { |
| chrome.automation.getDesktop(function(desktopNode) { |
| runTestIfIframeIsLoaded(rootNode); |
| |
| desktopNode.addEventListener('loadComplete', function(evt) { |
| runTestIfIframeIsLoaded(rootNode); |
| }, true); |
| }); |
| }); |
| }); |
| |
| /** Tests navigating into and out of iframes using nextObject */ |
| TEST_F( |
| 'ChromeVoxBackgroundTest', 'ForwardObjectNavigationThroughIframes', |
| function() { |
| const mockFeedback = this.createMockFeedback(); |
| |
| let running = false; |
| const runTestIfIframeIsLoaded = function(rootNode) { |
| if (running) { |
| return; |
| } |
| |
| // Return if the iframe hasn't loaded yet. |
| const iframe = rootNode.find({role: 'iframe'}); |
| const childDoc = iframe.firstChild; |
| if (!childDoc || childDoc.children.length === 0) { |
| return; |
| } |
| |
| running = true; |
| const suppressFocusActionOutput = function() { |
| DesktopAutomationHandler.announceActions = false; |
| }; |
| const beforeButton = |
| rootNode.find({role: RoleType.BUTTON, name: 'Before'}); |
| mockFeedback.call(beforeButton.focus.bind(beforeButton)) |
| .expectSpeech('Before', 'Button') |
| .call(suppressFocusActionOutput) |
| .call(doCmd('nextObject')) |
| .expectSpeech('Inside', 'Button'); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Inside', 'Heading 1'); |
| mockFeedback.call(doCmd('nextObject')).expectSpeech('After', 'Button'); |
| mockFeedback.call(doCmd('previousObject')) |
| .expectSpeech('Inside', 'Heading 1'); |
| mockFeedback.call(doCmd('previousObject')) |
| .expectSpeech('Inside', 'Button'); |
| mockFeedback.call(doCmd('previousObject')) |
| .expectSpeech('Before', 'Button'); |
| mockFeedback.replay(); |
| }.bind(this); |
| |
| this.runWithLoadedTree(this.iframesDoc, function(rootNode) { |
| chrome.automation.getDesktop(function(desktopNode) { |
| runTestIfIframeIsLoaded(rootNode); |
| |
| desktopNode.addEventListener('loadComplete', function(evt) { |
| runTestIfIframeIsLoaded(rootNode); |
| }, true); |
| }); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SelectOptionSelected', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <select> |
| <option>apple |
| <option>banana |
| <option>grapefruit |
| </select> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const select = root.find({role: RoleType.POP_UP_BUTTON}); |
| const clickSelect = select.doDefault.bind(select); |
| const selectLastOption = () => { |
| const options = select.findAll({role: RoleType.LIST_BOX_OPTION}); |
| options[options.length - 1].doDefault(); |
| }; |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Button', 'Press Search+Space to activate') |
| .call(clickSelect) |
| .expectSpeech('apple') |
| .expectSpeech('Button') |
| .expectSpeech('Expanded') |
| .call(selectLastOption) |
| .expectNextSpeechUtteranceIsNot('apple') |
| .expectSpeech('grapefruit') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ToggleButton', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div aria-pressed="mixed" role="button">boldface</div> |
| <div aria-pressed="true" role="button">ok</div> |
| <div aria-pressed="false" role="button">cancel</div> |
| <div aria-pressed role="button">close</div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const b = ChromeVoxState.instance; |
| const move = doCmd('nextObject'); |
| mockFeedback.call(move) |
| .expectSpeech('boldface') |
| .expectSpeech('Toggle Button') |
| .expectSpeech('Partially pressed') |
| |
| .call(move) |
| .expectSpeech('ok') |
| .expectSpeech('Toggle Button') |
| .expectSpeech('Pressed') |
| |
| .call(move) |
| .expectSpeech('cancel') |
| .expectSpeech('Toggle Button') |
| .expectSpeech('Not pressed') |
| |
| .call(move) |
| .expectSpeech('close') |
| .expectSpeech('Button') |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EditText', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <input type="text"></input> |
| <input role="combobox" type="text"></input> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const nextEditText = doCmd('nextEditText'); |
| const previousEditText = doCmd('previousEditText'); |
| mockFeedback.call(nextEditText) |
| .expectSpeech('Combo box') |
| .call(previousEditText) |
| .expectSpeech('Edit text') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ComboBox', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.comboBoxDoc, function() { |
| mockFeedback.expectSpeech('Edit text', 'Choose an item', 'Combo box') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'BackwardForwardSync', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div aria-label="Group" role="group" tabindex=0> |
| <input type="text"></input> |
| </div> |
| <ul> |
| <li tabindex=0> |
| <button>ok</button> |
| </li> |
| </ul> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const listItem = root.find({role: RoleType.LIST_ITEM}); |
| |
| mockFeedback.call(listItem.focus.bind(listItem)) |
| .expectSpeech('ok', 'List item') |
| .call(this.doCmd('nextObject')) |
| .expectSpeech('\u2022 ') // bullet |
| .call(this.doCmd('nextObject')) |
| .expectSpeech('Button') |
| .call(this.doCmd('previousObject')) |
| .expectSpeech('\u2022 ') // bullet |
| .call(this.doCmd('previousObject')) |
| .expectSpeech('List item') |
| .call(this.doCmd('previousObject')) |
| .expectSpeech('Edit text') |
| .call(this.doCmd('previousObject')) |
| .expectSpeech('Group') |
| .replay(); |
| }); |
| }); |
| |
| /** Tests that navigation works when the current object disappears. */ |
| TEST_F('ChromeVoxBackgroundTest', 'DisappearingObject', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.disappearingObjectDoc, function(rootNode) { |
| const deleteButton = |
| rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Delete'}}); |
| const pressDelete = deleteButton.doDefault.bind(deleteButton); |
| mockFeedback.expectSpeech('start').expectBraille('start'); |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Before1') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Before2') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Before3') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Disappearing') |
| .call(pressDelete) |
| .expectSpeech('Deleted') |
| .call(doCmd('nextObject')) |
| .expectSpeech('After1') |
| .call(doCmd('nextObject')) |
| .expectSpeech('After2') |
| .call(doCmd('previousObject')) |
| .expectSpeech('After1') |
| .call(doCmd('dumpTree')); |
| |
| /* |
| // This is broken by cl/1260523 making tree updating (more) |
| asynchronous. |
| // TODO(aboxhall/dtseng): Add a function to wait for next tree update? |
| mockFeedback |
| .call(doCmd('previousObject')) |
| .expectSpeech('Before3'); |
| */ |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| /** Tests that focus jumps to details properly when indicated. */ |
| TEST_F('ChromeVoxBackgroundTest', 'JumpToDetails', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.detailsDoc, function(rootNode) { |
| mockFeedback.call(doCmd('jumpToDetails')).expectSpeech('Details'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ButtonNameValueDescription', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = '<input type="submit" aria-label="foo" value="foo"></input>'; |
| this.runWithLoadedTree(site, function(root) { |
| const btn = root.find({role: RoleType.BUTTON}); |
| mockFeedback.call(btn.focus.bind(btn)) |
| .expectSpeech('foo') |
| .expectSpeech('Button') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NameFromHeadingLink', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>before</p> |
| <h1><a href="google.com">go</a><p>here</p></h1> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const link = root.find({role: RoleType.LINK}); |
| mockFeedback.call(link.focus.bind(link)) |
| .expectSpeech('go') |
| .expectSpeech('Link') |
| .expectSpeech('Heading 1') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'OptionChildIndexCount', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="listbox"> |
| <p>Fruits</p> |
| <div role="option">apple</div> |
| <div role="option">banana</div> |
| </div> |
| `; |
| |
| this.runWithLoadedTree(site, function(root) { |
| // Select first child of the list box, similar to what happens if navigated |
| // by Tab. |
| const firstChild = root.find({role: RoleType.PARAGRAPH}); |
| mockFeedback |
| .call( |
| () => ChromeVoxState.instance.setCurrentRange( |
| cursors.Range.fromNode(firstChild))) |
| .call(doCmd('nextObject')) |
| .expectSpeech('List box') |
| .expectSpeech('Fruits') |
| .call(doCmd('nextObject')) |
| .expectSpeech('apple') |
| .expectSpeech(' 1 of 2 ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('banana') |
| .expectSpeech(' 2 of 2 ') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ListMarkerIsIgnored', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<ul><li>apple</ul>', function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectNextSpeechUtteranceIsNot('listMarker') |
| .expectSpeech('\u2022 apple') // bullet apple |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SymetricComplexHeading', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h4><p>NW</p><p>NE</p></h4> |
| <h4><p>SW</p><p>SE</p></h4> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextHeading')) |
| .expectNextSpeechUtteranceIsNot('NE') |
| .expectSpeech('NW') |
| .call(doCmd('previousHeading')) |
| .expectNextSpeechUtteranceIsNot('NE') |
| .expectSpeech('NW') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ContentEditableJumpSyncsRange', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div contenteditable> |
| <h1>Top News</h1> |
| <h1>Most Popular</h1> |
| <h1>Sports</h1> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const assertRangeHasText = function(text) { |
| return function() { |
| assertEquals( |
| text, ChromeVoxState.instance.getCurrentRange().start.node.name); |
| }; |
| }; |
| |
| mockFeedback.call(doCmd('nextEditText')) |
| .expectSpeech('Top News Most Popular Sports') |
| .call(doCmd('nextHeading')) |
| .expectSpeech('Top News') |
| .call(assertRangeHasText('Top News')) |
| .call(doCmd('nextHeading')) |
| .expectSpeech('Most Popular') |
| .call(assertRangeHasText('Most Popular')) |
| .call(doCmd('nextHeading')) |
| .expectSpeech('Sports') |
| .call(assertRangeHasText('Sports')) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'Selection', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>simple</p> |
| <p>doc</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| // Fakes a toggleSelection command. |
| root.addEventListener('textSelectionChanged', function() { |
| if (root.focusOffset === 3) { |
| CommandHandler.onCommand('toggleSelection'); |
| } |
| }, true); |
| |
| mockFeedback.call(doCmd('toggleSelection')) |
| .expectSpeech('simple', 'selected') |
| .call(doCmd('nextCharacter')) |
| .expectSpeech('i', 'selected') |
| .call(doCmd('previousCharacter')) |
| .expectSpeech('i', 'unselected') |
| .call(doCmd('nextCharacter')) |
| .call(doCmd('nextCharacter')) |
| .expectSpeech('End selection', 'sim') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'BasicTableCommands', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <table border=1> |
| <tr><td>name</td><td>title</td><td>address</td><td>phone</td></tr> |
| <tr><td>Dan</td><td>Mr</td><td>666 Elm Street</td><td>212 222 5555</td></tr> |
| </table> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextRow')) |
| .expectSpeech('Dan', 'row 2 column 1') |
| .call(doCmd('previousRow')) |
| .expectSpeech('name', 'row 1 column 1') |
| .call(doCmd('previousRow')) |
| .expectSpeech('No cell above') |
| .call(doCmd('nextCol')) |
| .expectSpeech('title', 'row 1 column 2') |
| .call(doCmd('nextRow')) |
| .expectSpeech('Mr', 'row 2 column 2') |
| .call(doCmd('previousRow')) |
| .expectSpeech('title', 'row 1 column 2') |
| .call(doCmd('nextCol')) |
| .expectSpeech('address', 'row 1 column 3') |
| .call(doCmd('nextCol')) |
| .expectSpeech('phone', 'row 1 column 4') |
| .call(doCmd('nextCol')) |
| .expectSpeech('No cell right') |
| .call(doCmd('previousRow')) |
| .expectSpeech('No cell above') |
| .call(doCmd('nextRow')) |
| .expectSpeech('212 222 5555', 'row 2 column 4') |
| .call(doCmd('nextRow')) |
| .expectSpeech('No cell below') |
| .call(doCmd('nextCol')) |
| .expectSpeech('No cell right') |
| .call(doCmd('previousCol')) |
| .expectSpeech('666 Elm Street', 'row 2 column 3') |
| .call(doCmd('previousCol')) |
| .expectSpeech('Mr', 'row 2 column 2') |
| |
| .call(doCmd('goToRowLastCell')) |
| .expectSpeech('212 222 5555', 'row 2 column 4') |
| .call(doCmd('goToRowLastCell')) |
| .expectSpeech('212 222 5555') |
| .call(doCmd('goToRowFirstCell')) |
| .expectSpeech('Dan', 'row 2 column 1') |
| .call(doCmd('goToRowFirstCell')) |
| .expectSpeech('Dan') |
| |
| .call(doCmd('goToColFirstCell')) |
| .expectSpeech('name', 'row 1 column 1') |
| .call(doCmd('goToColFirstCell')) |
| .expectSpeech('name') |
| .call(doCmd('goToColLastCell')) |
| .expectSpeech('Dan', 'row 2 column 1') |
| .call(doCmd('goToColLastCell')) |
| .expectSpeech('Dan') |
| |
| .call(doCmd('goToLastCell')) |
| .expectSpeech('212 222 5555', 'row 2 column 4') |
| .call(doCmd('goToLastCell')) |
| .expectSpeech('212 222 5555') |
| .call(doCmd('goToFirstCell')) |
| .expectSpeech('name', 'row 1 column 1') |
| .call(doCmd('goToFirstCell')) |
| .expectSpeech('name') |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MissingTableCells', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <table border=1> |
| <tr><td>a</td><td>b</td><td>c</td></tr> |
| <tr><td>d</td><td>e</td></tr> |
| <tr><td>f</td></tr> |
| </table> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('goToRowLastCell')) |
| .expectSpeech('c', 'row 1 column 3') |
| .call(doCmd('goToRowLastCell')) |
| .expectSpeech('c') |
| .call(doCmd('goToRowFirstCell')) |
| .expectSpeech('a', 'row 1 column 1') |
| .call(doCmd('goToRowFirstCell')) |
| .expectSpeech('a') |
| |
| .call(doCmd('nextCol')) |
| .expectSpeech('b', 'row 1 column 2') |
| |
| .call(doCmd('goToColLastCell')) |
| .expectSpeech('e', 'row 2 column 2') |
| .call(doCmd('goToColLastCell')) |
| .expectSpeech('e') |
| .call(doCmd('goToColFirstCell')) |
| .expectSpeech('b', 'row 1 column 2') |
| .call(doCmd('goToColFirstCell')) |
| .expectSpeech('b') |
| |
| .call(doCmd('goToFirstCell')) |
| .expectSpeech('a', 'row 1 column 1') |
| .call(doCmd('goToFirstCell')) |
| .expectSpeech('a') |
| .call(doCmd('goToLastCell')) |
| .expectSpeech('f', 'row 3 column 1') |
| .call(doCmd('goToLastCell')) |
| .expectSpeech('f') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'DisabledState', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = '<button aria-disabled="true">ok</button>'; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('ok', 'Disabled', 'Button').replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'HeadingLevels', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h1>1</h1><h2>2</h2><h3>3</h3><h4>4</h4><h5>5</h5><h6>6</h6> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const makeLevelAssertions = function(level) { |
| mockFeedback.call(doCmd('nextHeading' + level)) |
| .expectSpeech('Heading ' + level) |
| .call(doCmd('nextHeading' + level)) |
| .expectEarcon('wrap') |
| .call(doCmd('previousHeading' + level)) |
| .expectEarcon('wrap'); |
| }; |
| for (let i = 1; i <= 6; i++) { |
| makeLevelAssertions(i); |
| } |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EditableNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div contenteditable>this is a test</div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('this is a test') |
| .call(doCmd('nextObject')) |
| .expectSpeech('this is a test') |
| .call(doCmd('nextWord')) |
| .expectSpeech('is') |
| .call(doCmd('nextWord')) |
| .expectSpeech('a') |
| .call(doCmd('nextWord')) |
| .expectSpeech('test') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigationMovesFocus', function() { |
| const site = ` |
| <p>start</p> |
| <input type="text"></input> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| this.listenOnce( |
| root.find({role: RoleType.TEXT_FIELD}), 'focus', function(e) { |
| const focus = ChromeVoxState.instance.currentRange.start.node; |
| assertEquals(RoleType.TEXT_FIELD, focus.role); |
| assertTrue(focus.state.focused); |
| }); |
| doCmd('nextEditText')(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'BrailleCaretNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>This is a<em>test</em> of inline braille<br>with a second line</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const text = 'This is a'; |
| mockFeedback.call(doCmd('nextCharacter')) |
| .expectBraille(text, {startIndex: 1, endIndex: 2}) // h |
| .call(doCmd('nextCharacter')) |
| .expectBraille(text, {startIndex: 2, endIndex: 3}) // i |
| .call(doCmd('nextWord')) |
| .expectBraille(text, {startIndex: 5, endIndex: 7}) // is |
| .call(doCmd('previousWord')) |
| .expectBraille(text, {startIndex: 0, endIndex: 4}) // This |
| .call(doCmd('nextLine')) |
| // Ensure nothing is selected when the range covers the entire line. |
| .expectBraille('with a second line', {startIndex: -1, endIndex: -1}) |
| .replay(); |
| }); |
| }); |
| |
| // This tests ChromeVox's special support for following an in-page link |
| // if you force-click on it. Compare with InPageLinks, below. |
| TEST_F('ChromeVoxBackgroundTest', 'ForceClickInPageLinks', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <a href="#there">hi</a> |
| <button id="there">there</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('hi', 'Internal link') |
| .call(doCmd('forceClickOnCurrentItem')) |
| .expectSpeech('there', 'Button') |
| .replay(); |
| }); |
| }); |
| |
| // This tests ChromeVox's handling of the scrolledToAnchor event, which is |
| // fired when the users follows an in-page link or the document otherwise |
| // gets navigated to an in-page link target by the url fragment changing, |
| // not necessarily due to directly clicking on the link via ChromeVox. |
| // |
| // Note: this test needs the test server running because the browser |
| // does not follow same-page links on data urls (because it modifies the |
| // url fragment, and any change to the url is disallowed for a data url). |
| TEST_F('ChromeVoxBackgroundTestWithTestServer', 'InPageLinks', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(undefined, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Jump', 'Internal link') |
| .call(press(KeyCode.RETURN)) |
| .expectSpeech('Found It') |
| .call(doCmd('nextHeading')) |
| .expectSpeech('Continue Here', 'Heading 2') |
| .replay(); |
| }.bind(this), { |
| url: `${testRunnerParams.testServerBaseUrl}accessibility/in_page_links.html` |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ListItem', function() { |
| this.resetContextualOutput(); |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <ul><li>apple<li>grape<li>banana</ul> |
| <ol><li>pork<li>beef<li>chicken</ol> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextLine')) |
| .expectSpeech('\u2022 apple', 'List item') |
| .expectBraille('\u2022 apple lstitm lst +3') |
| .call(doCmd('nextLine')) |
| .expectSpeech('\u2022 grape', 'List item') |
| .expectBraille('\u2022 grape lstitm') |
| .call(doCmd('nextLine')) |
| .expectSpeech('\u2022 banana', 'List item') |
| .expectBraille('\u2022 banana lstitm lst end') |
| |
| // Object nav should be the same. |
| .call(doCmd('nextObject')) |
| .expectSpeech('1. pork', 'List item') |
| .expectBraille('1. pork lstitm lst +3') |
| .call(doCmd('nextObject')) |
| .expectSpeech('2. beef', 'List item') |
| .expectBraille('2. beef lstitm') |
| |
| // Mixing with line nav. |
| .call(doCmd('nextLine')) |
| .expectSpeech('3. chicken', 'List item') |
| .expectBraille('3. chicken lstitm lst end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'BusyHeading', function() { |
| this.resetContextualOutput(); |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <h2><a href="#">Lots</a><a href="#">going</a><a href="#">here</a></h2> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| // In the past, this would have inserted the 'heading 2' after the first |
| // link's output. Make sure it goes to the end. |
| mockFeedback.call(doCmd('nextLine')) |
| .expectSpeech( |
| 'Lots', 'Link', 'going', 'Link', 'here', 'Link', 'Heading 2') |
| .expectBraille('Lots lnk going lnk here lnk h2') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NodeVsSubnode', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<a href="#">test</a>', function(root) { |
| const link = root.find({role: RoleType.LINK}); |
| function outputLinkRange(start, end) { |
| return function() { |
| new Output() |
| .withSpeech(new cursors.Range( |
| new cursors.Cursor(link, start), new cursors.Cursor(link, end))) |
| .go(); |
| }; |
| } |
| |
| mockFeedback.call(outputLinkRange(0, 0)) |
| .expectSpeech('test', 'Internal link') |
| .call(outputLinkRange(0, 1)) |
| .expectSpeech('t') |
| .call(outputLinkRange(1, 1)) |
| .expectSpeech('test', 'Internal link') |
| .call(outputLinkRange(1, 2)) |
| .expectSpeech('e') |
| .call(outputLinkRange(1, 3)) |
| .expectNextSpeechUtteranceIsNot('Internal link') |
| .expectSpeech('es') |
| .call(outputLinkRange(0, 4)) |
| .expectSpeech('test', 'Internal link') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NativeFind', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <a href="#">grape</a> |
| <a href="#">pineapple</a> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(press(KeyCode.F, {ctrl: true})) |
| .expectSpeech('Find', 'Edit text') |
| .call(press(KeyCode.G)) |
| .expectSpeech('grape', 'Link') |
| .call(press(KeyCode.BACK)) |
| .call(press(KeyCode.L)) |
| .expectSpeech('pineapple', 'Link') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EditableKeyCommand', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <input type="text"></input> |
| <textarea>test</textarea> |
| <div role="textbox" contenteditable>test</div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const assertCurNode = function(node) { |
| return function() { |
| assertEquals(node, ChromeVoxState.instance.currentRange.start.node); |
| }; |
| }; |
| |
| const textField = root.firstChild; |
| const textArea = textField.nextSibling; |
| const contentEditable = textArea.nextSibling; |
| |
| mockFeedback.call(assertCurNode(textField)) |
| .call(doCmd('nextObject')) |
| .call(assertCurNode(textArea)) |
| .call(doCmd('nextObject')) |
| .call(assertCurNode(contentEditable)) |
| .call(doCmd('previousObject')) |
| .expectSpeech('Text area') |
| .call(assertCurNode(textArea)) |
| .call(doCmd('previousObject')) |
| .call(assertCurNode(textField)) |
| |
| .replay(); |
| }); |
| }); |
| |
| // TODO(crbug.com/935678): Test times out flakily in MSAN builds. |
| TEST_F_WITH_PREAMBLE( |
| ` |
| #if defined(MEMORY_SANITIZER) |
| #define MAYBE_TextSelectionAndLiveRegion DISABLED_TextSelectionAndLiveRegion |
| #else |
| #define MAYBE_TextSelectionAndLiveRegion TextSelectionAndLiveRegion |
| #endif |
| `, |
| 'ChromeVoxBackgroundTest', 'MAYBE_TextSelectionAndLiveRegion', function() { |
| DesktopAutomationHandler.announceActions = true; |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree( |
| ` |
| <p>start</p> |
| <div><input value="test" type="text"></input></div> |
| <div id="live" aria-live="assertive"></div> |
| <script> |
| const input = document.querySelector('input'); |
| const [div, live] = document.querySelectorAll('div'); |
| let clicks = 0; |
| div.addEventListener('click', function() { |
| clicks++; |
| if (clicks == 1) { |
| live.textContent = 'go'; |
| } else if (clicks == 2) { |
| input.selectionStart = 1; |
| live.textContent = 'queued'; |
| } else { |
| input.selectionStart = 2; |
| live.textContent = 'interrupted'; |
| } |
| }); |
| </script> |
| `, |
| function(root) { |
| const textField = root.find({role: RoleType.TEXT_FIELD}); |
| const div = textField.parent; |
| mockFeedback.call(textField.focus.bind(textField)) |
| .expectSpeech('Edit text') |
| .call(div.doDefault.bind(div)) |
| .expectSpeechWithQueueMode('go', QueueMode.CATEGORY_FLUSH) |
| |
| .call(div.doDefault.bind(div)) |
| .expectSpeechWithQueueMode('queued', QueueMode.QUEUE) |
| .expectSpeechWithQueueMode('e', QueueMode.CATEGORY_FLUSH) |
| |
| .call(div.doDefault.bind(div)) |
| .expectSpeechWithQueueMode('interrupted', QueueMode.QUEUE) |
| .expectSpeechWithQueueMode('s', QueueMode.CATEGORY_FLUSH) |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TableColumnHeaders', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="grid"> |
| <div role="rowgroup"> |
| <div role="row"> |
| <div role="columnheader">city</div> |
| <div role="columnheader">state</div> |
| <div role="columnheader">zip</div> |
| </div> |
| </div> |
| <div role="rowgroup"> |
| <div role="row"> |
| <div role="gridcell">Mountain View</div> |
| <div role="gridcell">CA</div> |
| <div role="gridcell">94043</div> |
| </div> |
| <div role="row"> |
| <div role="gridcell">San Jose</div> |
| <div role="gridcell">CA</div> |
| <div role="gridcell">95128</div> |
| </div> |
| </div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextRow')) |
| .expectSpeech('Mountain View', 'row 2 column 1') |
| .call(doCmd('nextRow')) |
| .expectNextSpeechUtteranceIsNot('city') |
| .expectSpeech('San Jose', 'row 3 column 1') |
| .call(doCmd('nextCol')) |
| .expectSpeech('CA', 'row 3 column 2', 'state') |
| .call(doCmd('previousRow')) |
| .expectSpeech('CA', 'row 2 column 2') |
| .call(doCmd('previousRow')) |
| .expectSpeech('state', 'row 1 column 2') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ActiveDescendantUpdates', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div aria-label="container" tabindex=0 role="group" id="active" |
| aria-activedescendant="1"> |
| <div id="1" role="treeitem" aria-selected="false"></div> |
| <div id="2" role="treeitem" aria-selected="true"></div> |
| <script> |
| let alt = false; |
| let active = document.getElementById('active'); |
| let one = document.getElementById('1'); |
| let two = document.getElementById('2'); |
| active.addEventListener('click', function() { |
| let descendant = alt ? one : two; |
| active.setAttribute('aria-activedescendant', descendant.id); |
| alt = !alt; |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const group = root.firstChild; |
| mockFeedback.call(group.focus.bind(group)) |
| .call(group.doDefault.bind(group)) |
| .expectSpeech('Tree item', ' 2 of 2 ') |
| .call(group.doDefault.bind(group)) |
| .expectSpeech('Tree item', 'Not selected', ' 1 of 2 ') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>before content editable</p> |
| <div role="textbox" contenteditable>this<br>is<br>a<br>test</div> |
| <p>after content editable, before text area</p> |
| <textarea style="word-spacing: 1000px">this is a test</textarea> |
| <p>after text area</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const assertBeginning = function(expected) { |
| const textEditHandler = DesktopAutomationHandler.instance.textEditHandler; |
| assertNotNullNorUndefined(textEditHandler); |
| assertEquals(expected, textEditHandler.isSelectionOnFirstLine()); |
| }; |
| const assertEnd = function(expected) { |
| const textEditHandler = DesktopAutomationHandler.instance.textEditHandler; |
| assertNotNullNorUndefined(textEditHandler); |
| assertEquals(expected, textEditHandler.isSelectionOnLastLine()); |
| }; |
| const [contentEditable, textArea] = |
| root.findAll({role: RoleType.TEXT_FIELD}); |
| |
| this.listenOnce(contentEditable, EventType.FOCUS, function() { |
| mockFeedback.call(assertBeginning.bind(this, true)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('is') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('a') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('test') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, true)) |
| |
| .call(textArea.focus.bind(textArea)) |
| .expectSpeech('Text area') |
| .call(assertBeginning.bind(this, true)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(40 /* ArrowDown */)) |
| .expectSpeech('is') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(40 /* ArrowDown */)) |
| .expectSpeech('a') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, false)) |
| |
| .call(press(40 /* ArrowDown */)) |
| .expectSpeech('test') |
| .call(assertBeginning.bind(this, false)) |
| .call(assertEnd.bind(this, true)) |
| |
| .replay(); |
| |
| // TODO: soft line breaks currently won't work in <textarea>. |
| }.bind(this)); |
| contentEditable.focus(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SelectDoesNotSyncNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <select> |
| <option>apple</option> |
| <option>grape</option> |
| </select> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const select = root.find({role: RoleType.POP_UP_BUTTON}); |
| mockFeedback.expectSpeech('Button', 'has pop up', 'Collapsed') |
| .call(select.doDefault.bind(select)) |
| .expectSpeech('Expanded') |
| .call( |
| () => assertEquals( |
| select, ChromeVoxState.instance.currentRange.start.node)) |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('grape', 'List item', ' 2 of 2 ') |
| .call( |
| () => assertEquals( |
| select, ChromeVoxState.instance.currentRange.start.node)) |
| .call(press(KeyCode.UP)) |
| .expectSpeech('apple', 'List item', ' 1 of 2 ') |
| .call( |
| () => assertEquals( |
| select, ChromeVoxState.instance.currentRange.start.node)) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresLabels', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>before</p> |
| <p id="label">label</p> |
| <a href="#next" id="lebal">lebal</a> |
| <h2 id="headingLabel">headingLabel</h2> |
| <p>after</p> |
| <button aria-labelledby="label headingLabel"></button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('before') |
| .call(doCmd('nextObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('nextObject')) |
| .expectSpeech('headingLabel', 'Heading 2') |
| .call(doCmd('nextObject')) |
| .expectSpeech('after') |
| .call(doCmd('previousObject')) |
| .expectSpeech('headingLabel', 'Heading 2') |
| .call(doCmd('previousObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('previousObject')) |
| .expectSpeech('before') |
| .call(doCmd('nextObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('nextObject')) |
| .expectSpeech('headingLabel', 'Heading 2') |
| .call(doCmd('nextObject')) |
| .expectSpeech('after') |
| .call(doCmd('nextObject')) |
| .expectSpeech('label headingLabel', 'Button') |
| .call(doCmd('nextObject')) |
| .expectEarcon(Earcon.WRAP) |
| .call(doCmd('nextObject')) |
| .expectSpeech('before') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresDescriptions', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>before</p> |
| <p id="desc">label</p> |
| <a href="#next" id="csed">lebal</a> |
| <p>after</p> |
| <button aria-describedby="desc"></button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('before') |
| .call(doCmd('nextObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('nextObject')) |
| .expectSpeech('after') |
| .call(doCmd('previousObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('previousObject')) |
| .expectSpeech('before') |
| .call(doCmd('nextObject')) |
| .expectSpeech('lebal', 'Link') |
| .call(doCmd('nextObject')) |
| .expectSpeech('after') |
| .call(doCmd('nextObject')) |
| .expectSpeech('label', 'lebal', 'Button') |
| .call(doCmd('nextObject')) |
| .expectEarcon(Earcon.WRAP) |
| .call(doCmd('nextObject')) |
| .expectSpeech('before') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MathContentViaInnerHtml', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="math"> |
| <semantics> |
| <mrow class="MJX-TeXAtom-ORD"> |
| <mstyle displaystyle="true" scriptlevel="0"> |
| <mi>a</mi> |
| <mo stretchy="false">(</mo> |
| <mi>y</mi> |
| <mo>+</mo> |
| <mi>m</mi> |
| <msup> |
| <mo stretchy="false">)</mo> |
| <mrow class="MJX-TeXAtom-ORD"> |
| <mn>2</mn> |
| </mrow> |
| </msup> |
| <mo>+</mo> |
| <mi>b</mi> |
| <mo stretchy="false">(</mo> |
| <mi>y</mi> |
| <mo>+</mo> |
| <mi>m</mi> |
| <mo stretchy="false">)</mo> |
| <mo>+</mo> |
| <mi>c</mi> |
| <mo>=</mo> |
| <mn>0.</mn> |
| </mstyle> |
| </mrow> |
| <annotation encoding="application/x-tex">{\displaystyle a(y+m)^{2}+b(y+m)+c=0.}</annotation> |
| </semantics> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('a ( y + m ) squared + b ( y + m ) + c = 0 .') |
| .expectSpeech('Press up, down, left, or right to explore math') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'GestureGranularity', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>This is a test</p> |
| <h2>hello</h2> |
| <a href="#">greetings</a> |
| <h2>here</h2> |
| <button>and</button> |
| <a href="#">there</a> |
| <button>world</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Word') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('is') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('a') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('is') |
| |
| .call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Character') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('s') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('i') |
| |
| .call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Form field control') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('and', 'Button') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('world', 'Button') |
| |
| .call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Link') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('greetings', 'Internal link') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('there', 'Internal link') |
| |
| .call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Heading') |
| .call(doGesture(Gesture.SWIPE_DOWN1)) |
| .expectSpeech('hello', 'Heading 2') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('here', 'Heading 2') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('hello', 'Heading 2') |
| |
| .call(doGesture(Gesture.SWIPE_LEFT3)) |
| .expectSpeech('Line') |
| .call(doGesture(Gesture.SWIPE_UP1)) |
| .expectSpeech('This is a test') |
| |
| .call(doGesture(Gesture.SWIPE_RIGHT3)) |
| .expectSpeech('Heading') |
| .call(doGesture(Gesture.SWIPE_RIGHT3)) |
| .expectSpeech('Internal link') |
| .call(doGesture(Gesture.SWIPE_RIGHT3)) |
| .expectSpeech('Form field control') |
| .call(doGesture(Gesture.SWIPE_RIGHT3)) |
| .expectSpeech('Character') |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'LinesFilterWhitespace', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div role="list"> |
| <div role="listitem"> |
| <span>Munich</span> |
| <span>London</span> |
| </div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('start') |
| .clearPendingOutput() |
| .call(doCmd('nextLine')) |
| .expectSpeech('Munich') |
| .expectNextSpeechUtteranceIsNot(' ') |
| .expectSpeech('London') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TabSwitchAndRefreshRecovery', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>tab1</p>', function(root1) { |
| this.runWithLoadedTree('<p>tab2</p>', function(root2) { |
| mockFeedback.expectSpeech('tab2') |
| .clearPendingOutput() |
| .call(press(KeyCode.TAB, {shift: true, ctrl: true})) |
| .expectSpeech('tab1') |
| .clearPendingOutput() |
| .call(press(KeyCode.TAB, {ctrl: true})) |
| .expectSpeech('tab2') |
| .clearPendingOutput() |
| .call(press(KeyCode.R, {ctrl: true})) |
| |
| // ChromeVox stays on the same node due to tree path recovery. |
| .call(() => { |
| assertEquals( |
| 'tab2', ChromeVoxState.instance.currentRange.start.node.name); |
| }) |
| .replay(); |
| }); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ListName', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div id="_md-chips-wrapper-76" tabindex="-1" class="md-chips md-readonly" |
| aria-setsize="4" aria-label="Favorite Sports" role="list" |
| aria-describedby="chipsNote"> |
| <div role="listitem">Baseball</div> |
| <div role="listitem">Hockey</div> |
| <div role="listitem">Lacrosse</div> |
| <div role="listitem">Football</div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('Favorite Sports').replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'LayoutTable', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <table><tr><td>start</td></tr></table><p>end</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('start') |
| .call(doCmd('nextObject')) |
| .expectNextSpeechUtteranceIsNot('row 1 column 1') |
| .expectNextSpeechUtteranceIsNot('Table , 1 by 1') |
| .expectSpeech('end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ReinsertedNodeRecovery', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div> |
| <button id="start">start</button> |
| <button id="hot">hot</button> |
| </div> |
| <button id="end">end</button> |
| <script> |
| let div = document.body.firstElementChild; |
| let start = document.getElementById('start'); |
| document.getElementById('hot').addEventListener('focus', (evt) => { |
| let hot = evt.target; |
| hot.remove(); |
| div.insertAfter(hot, start); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('start') |
| .clearPendingOutput() |
| .call(doCmd('nextObject')) |
| .call(doCmd('nextObject')) |
| .call(doCmd('nextObject')) |
| .expectSpeech('end', 'Button') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PointerTargetsLeafNode', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role=button><p>Washington</p></div> |
| <div role=button><p>Adams</p></div> |
| <div role=button><p>Jefferson</p></div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const button = |
| root.find({role: RoleType.BUTTON, attributes: {name: 'Jefferson'}}); |
| const buttonP = button.firstChild; |
| assertNotNullNorUndefined(buttonP); |
| const buttonText = buttonP.firstChild; |
| assertNotNullNorUndefined(buttonText); |
| mockFeedback.call(simulateHitTestResult(buttonText)) |
| .expectSpeech('Jefferson') |
| .expectSpeech('Button') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueNow', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div id="slider" role="slider" tabindex="0" aria-valuemin="0" |
| aria-valuenow="50" aria-valuemax="100"></div> |
| <script> |
| let slider = document.getElementById('slider'); |
| slider.addEventListener('click', () => { |
| slider.setAttribute('aria-valuenow', |
| parseInt(slider.getAttribute('aria-valuenow'), 10) + 1); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const slider = root.find({role: RoleType.SLIDER}); |
| assertNotNullNorUndefined(slider); |
| mockFeedback.call(slider.doDefault.bind(slider)) |
| .expectSpeech('51') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueText', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div id="slider" role="slider" tabindex="0" aria-valuemin="0" |
| aria-valuenow="50" aria-valuemax="100" aria-valuetext="tiny"></div> |
| <script> |
| let slider = document.getElementById('slider'); |
| slider.addEventListener('click', () => { |
| slider.setAttribute('aria-valuenow', |
| parseInt(slider.getAttribute('aria-valuenow'), 10) + 1); |
| slider.setAttribute('aria-valuetext', 'large'); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const slider = root.find({role: RoleType.SLIDER}); |
| assertNotNullNorUndefined(slider); |
| mockFeedback.clearPendingOutput() |
| .call(slider.doDefault.bind(slider)) |
| .expectNextSpeechUtteranceIsNot('51') |
| .expectSpeech('large') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SelectValidityOutput', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <label for="in1">Name:</label> |
| <input id="in1" required> |
| <script> |
| const in1 = document.querySelector('input'); |
| in1.addEventListener('focus', () => { |
| setTimeout(() => { |
| in1.setCustomValidity('Please enter name'); |
| in1.reportValidity(); |
| }, 500); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Name:') |
| .expectSpeech('Edit text') |
| .expectSpeech('Required') |
| .expectNextSpeechUtteranceIsNot('Alert') |
| .expectSpeech('Please enter name') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EventFromAction', function() { |
| const site = '<button>ok</button><button>cancel</button>'; |
| this.runWithLoadedTree(site, function(root) { |
| const button = root.findAll({role: RoleType.BUTTON})[1]; |
| button.addEventListener(EventType.FOCUS, this.newCallback(function(evt) { |
| assertEquals(RoleType.BUTTON, evt.target.role); |
| assertEquals('action', evt.eventFrom); |
| assertEquals('cancel', evt.target.name); |
| assertEquals('focus', evt.eventFromAction); |
| })); |
| |
| button.focus(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EventFromUser', function() { |
| const site = '<button>ok</button><button>cancel</button>'; |
| this.runWithLoadedTree(site, function(root) { |
| const button = root.findAll({role: RoleType.BUTTON})[1]; |
| button.addEventListener(EventType.FOCUS, this.newCallback(function(evt) { |
| assertEquals(RoleType.BUTTON, evt.target.role); |
| assertEquals('user', evt.eventFrom); |
| assertEquals('cancel', evt.target.name); |
| })); |
| |
| press(KeyCode.TAB)(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ReadPhoneticPronunciationTest', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <button>This is a button</button> |
| <input type="text"></input> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| root.find({role: RoleType.BUTTON}).focus(); |
| mockFeedback.call(doCmd('readPhoneticPronunciation')) |
| .expectSpeech( |
| 'T: tango, h: hotel, i: india, s: sierra, : , i: india, ' + |
| 's: sierra, : , a: alpha, : , b: bravo, u: uniform, t: tango, ' + |
| 't: tango, o: oscar, n: november') |
| .call(doCmd('nextWord')) |
| .call(doCmd('readPhoneticPronunciation')) |
| .expectSpeech('i: india, s: sierra') |
| .call(doCmd('previousWord')) |
| .call(doCmd('readPhoneticPronunciation')) |
| .expectSpeech('T: tango, h: hotel, i: india, s: sierra') |
| .call(doCmd('nextWord')) |
| .call(doCmd('nextWord')) |
| .call(doCmd('nextWord')) |
| .call(doCmd('readPhoneticPronunciation')) |
| .expectSpeech( |
| 'b: bravo, u: uniform, t: tango, t: tango, o: oscar, ' + |
| 'n: november') |
| .call(doCmd('nextEditText')) |
| .call(doCmd('readPhoneticPronunciation')) |
| .expectSpeech('No available text for this item'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SimilarItemNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h3><a href="#a">inner</a></h3> |
| <p>some text</p> |
| <button>some other text</button> |
| <a href="#b">outer1</a> |
| <h3>outer2</h3> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| assertEquals( |
| RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role); |
| assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name); |
| mockFeedback.call(doCmd('nextSimilarItem')) |
| .expectSpeech('outer1', 'Link') |
| .call(doCmd('nextSimilarItem')) |
| .expectSpeech('inner', 'Link') |
| .call(doCmd('nextSimilarItem')) |
| .call(doCmd('previousSimilarItem')) |
| .expectSpeech('inner', 'Link') |
| .call(doCmd('nextHeading')) |
| .expectSpeech('outer2', 'Heading 3') |
| .call(doCmd('previousSimilarItem')) |
| .expectSpeech('inner', 'Heading 3'); |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'InvalidItemNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h3><a href="#a">inner</a></h3> |
| <p>some <span aria-invalid="spelling">txet</span></p> |
| <button>button A</button> |
| <p aria-invalid="true">some other reason</p> |
| <p>no error text 1</P> |
| <p aria-invalid=false>no error text 2</P> |
| <p><span aria-invalid="grammar">this are</span> a test</span></p> |
| <p aria-invalid="unknown">error is this</p> |
| <a href="#b">outer1</a> |
| <h3>outer2</h3> |
| `; |
| |
| this.runWithLoadedTree(site, function(root) { |
| assertEquals( |
| RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role); |
| assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name); |
| mockFeedback.call(doCmd('nextInvalidItem')) |
| .expectSpeech('txet', 'misspelled') |
| .call(doCmd('nextInvalidItem')) |
| .expectSpeech('some other reason') |
| .call(doCmd('nextInvalidItem')) |
| .expectSpeech('this are', 'grammar error') |
| .call(doCmd('nextInvalidItem')) |
| .expectSpeech('error is this') |
| // Ensure wrap. |
| .call(doCmd('nextInvalidItem')) |
| .expectSpeech('txet') |
| // Wrap backward. |
| .call(doCmd('previousInvalidItem')) |
| .expectSpeech('error is this') |
| .call(doCmd('previousInvalidItem')) |
| .expectSpeech('this are', 'grammar error'); |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'InvalidItemNavigationNoItem', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h3><a href="#a">inner</a></h3> |
| <p>some text</p> |
| <button>some other text</button> |
| <a href="#b">outer1</a> |
| <h3>outer2</h3> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| assertEquals( |
| RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role); |
| assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name); |
| mockFeedback.call(doCmd('nextInvalidItem')) |
| .expectSpeech('No invalid item') |
| .call(doCmd('previousInvalidItem')) |
| .expectSpeech('No invalid item'); |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TableWithAriaRowCol', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="table"> |
| <div role="row" aria-rowindex=3> |
| <div role="cell">test</div> |
| </div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('fullyDescribe')) |
| .expectSpeech('test', 'row 3 column 1', 'Table , 1 by 1') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NonModalDialogHeadingJump', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h2>Heading outside dialog</h2> |
| <div role="dialog"> |
| <h2>Heading inside dialog</h2> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextHeading')) |
| .expectSpeech('Heading inside dialog') |
| .call(doCmd('previousHeading')) |
| .expectSpeech('Heading outside dialog') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'LevelEndsForNestedLists', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div> |
| <ul> |
| <li>Berries |
| <ul> |
| <li>Strawberries</li> |
| <li>Blueberries</li> |
| <li>Raspberries</li> |
| </ul> |
| </li> |
| <li>Citruses |
| <ul> |
| <li>Oranges |
| <ul> |
| <li>Grapefruits</li> |
| <li>Mandarins</li> |
| </ul> |
| </li> |
| </ul> |
| </li> |
| <li>Bananas</li> |
| </ul> |
| </div> |
| `; |
| |
| this.runWithLoadedTree(site, function(root) { |
| const blueberries = root.find({attributes: {name: 'Blueberries'}}); |
| const grapefruits = root.find({attributes: {name: 'Grapefruits'}}); |
| |
| mockFeedback |
| .call(() => { |
| ChromeVoxState.instance.setCurrentRange( |
| cursors.Range.fromNode(blueberries)); |
| }) |
| .call(doCmd('nextObject')) |
| .expectSpeech( |
| 'â—¦ Raspberries', 'List item', 'List end', 'nested level 2') |
| .call(() => { |
| ChromeVoxState.instance.setCurrentRange( |
| cursors.Range.fromNode(grapefruits)); |
| }) |
| .call(doCmd('nextObject')) |
| .expectSpeech('â– Mandarins', 'List item', 'List end', 'nested level 3') |
| .call(doCmd('nextObject')) |
| // Nested level is not mentioned for level 1. |
| .expectSpeech('• Bananas', 'List item', 'List end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NestedListNavigationSimple', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.nestedListDoc, function(root) { |
| mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items') |
| .call(doCmd('nextObject')) |
| .expectSpeech('• Oranges', 'List item') |
| .call(doCmd('nextObject')) |
| .expectSpeech('• ', 'Berries', 'List item') |
| .expectBraille('• Berries lstitm') |
| .call(doCmd('nextObject')) |
| .expectSpeech('â—¦ Strawberries', 'List item', 'List', 'with 2 items') |
| .call(doCmd('nextObject')) |
| .expectSpeech('â—¦ Raspberries', 'List item', 'List end') |
| .call(doCmd('nextObject')) |
| .expectSpeech('• Bananas', 'List item', 'List end') |
| .expectBraille('• Bananas lstitm lst end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NestedListNavigationMixed', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(this.nestedListDoc, function(root) { |
| mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items') |
| .call(doCmd('nextObject')) |
| .expectSpeech('• Oranges', 'List item') |
| .call(doCmd('nextLine')) |
| .expectSpeech('• ', 'Berries', 'List item') |
| .call(doCmd('nextLine')) |
| .expectSpeech('â—¦ Strawberries', 'List item', 'List', 'with 2 items') |
| .call(doCmd('previousLine')) |
| .expectSpeech('• ', 'Berries') |
| .call(doCmd('nextWord')) |
| .expectSpeech('â—¦ Strawberries') |
| .call(doCmd('nextWord')) |
| .expectSpeech('â—¦ Raspberries') |
| .call(doCmd('previousObject')) |
| .call(doCmd('previousObject')) |
| .expectSpeech('• ', 'Berries') |
| .call(doCmd('previousCharacter')) |
| .call(doCmd('previousCharacter')) |
| .call(doCmd('previousCharacter')) |
| .expectSpeech('g') // For Oranges |
| .call(doCmd('nextGroup')) |
| .expectSpeech('â—¦ Strawberries', 'â—¦ Raspberries') |
| .clearPendingOutput() |
| .call(doCmd('previousGroup')) |
| .expectSpeech('• Oranges') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigationByList', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start here</p> |
| <ul> |
| Drinks |
| <li>Coffee</li> |
| <li>Tea</li> |
| </ul> |
| <p>A random paragraph</p> |
| <ul></ul> |
| <ol> |
| Lunch |
| <li>Burgers</li> |
| <li>Fries</li> |
| <li>Soda</li> |
| <ul> |
| Nested list |
| <li>Element</li> |
| </ul> |
| </ol> |
| <p>Another random paragraph</p> |
| <dl> |
| Colors |
| <dt>Red</dt> |
| <dd>Description for red</dd> |
| <dt>Blue</dt> |
| <dd>Description for blue</dd> |
| <dt>Green</dt> |
| <dd>Description for green</dd> |
| </dl> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('jumpToTop')) |
| .call(doCmd('nextList')) |
| .expectSpeech('Drinks', 'List', 'with 2 items') |
| .call(doCmd('nextList')) |
| .expectSpeech('List', 'with 0 items') |
| .call(doCmd('nextList')) |
| .expectSpeech('Lunch', 'List', 'with 3 items') |
| .call(doCmd('nextList')) |
| .expectSpeech('Nested list', 'List', 'with 1 item') |
| .call(doCmd('nextList')) |
| .expectSpeech('Colors', 'Description list', 'with 3 items') |
| .call(doCmd('nextList')) |
| // Ensure we wrap correctly. |
| .expectSpeech('Drinks', 'List', 'with 2 items') |
| .call(doCmd('nextObject')) |
| .call(doCmd('nextObject')) |
| .expectSpeech('\u2022 Coffee') |
| // Ensure we wrap correctly and go to previous list, not top of |
| // current list. |
| .call(doCmd('previousList')) |
| .expectSpeech('Colors') |
| .call(doCmd('previousObject')) |
| .expectSpeech('Another random paragraph') |
| // Ensure we dive into the nested list. |
| .call(doCmd('previousList')) |
| .expectSpeech('Nested list', 'List', 'with 1 item') |
| .call(doCmd('previousList')) |
| .expectSpeech('Lunch') |
| .call(doCmd('nextObject')) |
| .call(doCmd('nextObject')) |
| .expectSpeech('1. Burgers') |
| // Ensure we go to the previous list, not the top of the current |
| // list. |
| .call(doCmd('previousList')) |
| .expectSpeech('List', 'with 0 items') |
| .call(doCmd('previousObject')) |
| .expectSpeech('A random paragraph') |
| .call(doCmd('previousList')) |
| .expectSpeech('Drinks', 'List', 'with 2 items') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NoListTest', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<button>Click me</button>', function(root) { |
| mockFeedback.call(doCmd('nextList')) |
| .expectSpeech('No next list') |
| .call(doCmd('previousList')) |
| .expectSpeech('No previous list'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigateToLastHeading', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <h1>First</h1> |
| <h1>Second</h1> |
| <h1>Third</h1> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('jumpToTop')) |
| .expectSpeech('First', 'Heading 1') |
| .call(doCmd('previousHeading')) |
| .expectSpeech('Third', 'Heading 1') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ReadLinkURLTest', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <a href="https://www.google.com/">A popular link</a> |
| <button>Not a link</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextLink')) |
| .expectSpeech( |
| 'A popular link', 'Link', 'Press Search+Space to activate') |
| .call(doCmd('readLinkURL')) |
| .expectSpeech('Link URL: https://www.google.com/') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Not a link', 'Button', 'Press Search+Space to activate') |
| .call(doCmd('readLinkURL')) |
| .expectSpeech('No URL found') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NoRepeatTitle', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="button" aria-label="title" title="title"></div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('title') |
| .expectSpeech('Button') |
| .expectNextSpeechUtteranceIsNot('title') |
| .expectSpeech('Press Search+Space to activate') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PhoneticsAndCommands', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>some sample text</p> |
| <button>ok</button> |
| <p>A</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const noPhonetics = {phoneticCharacters: undefined}; |
| const phonetics = {phoneticCharacters: true}; |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeechWithProperties(noPhonetics, 'ok') |
| .call(doCmd('previousObject')) |
| .expectSpeechWithProperties(noPhonetics, 'some sample text') |
| .call(doCmd('nextWord')) |
| .expectSpeechWithProperties(noPhonetics, 'sample') |
| .call(doCmd('previousWord')) |
| .expectSpeechWithProperties(noPhonetics, 'some') |
| .call(doCmd('nextCharacter')) |
| .expectSpeechWithProperties(phonetics, 'o') |
| .call(doCmd('nextCharacter')) |
| .expectSpeechWithProperties(phonetics, 'm') |
| .call(doCmd('previousCharacter')) |
| .expectSpeechWithProperties(phonetics, 'o') |
| .call(doCmd('jumpToBottom')) |
| .expectSpeechWithProperties(noPhonetics, 'A'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ToggleScreen', function() { |
| const mockFeedback = this.createMockFeedback(); |
| // Pretend we've already accepted the confirmation dialog once. |
| localStorage['acceptToggleScreen'] = 'true'; |
| this.runWithLoadedTree('<div>Unimportant web content</div>', function() { |
| mockFeedback.call(doCmd('toggleScreen')) |
| .expectSpeech('Screen off') |
| .call(doCmd('toggleScreen')) |
| .expectSpeech('Screen on') |
| .call(doCmd('toggleScreen')) |
| .expectSpeech('Screen off') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackDisabled', function() { |
| // Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is |
| // off for the current app. |
| this.dispatchOnCustomSpokenFeedbackToggledEvent(false); |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>Test document</p>', function() { |
| ChromeVoxState.instance.setCurrentRange(null); |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech( |
| 'No current ChromeVox focus. Press Alt+Shift+L to go to the ' + |
| 'launcher.') |
| .call(doCmd('previousObject')) |
| .expectSpeech( |
| 'No current ChromeVox focus. Press Alt+Shift+L to go to the ' + |
| 'launcher.'); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackEnabled', function() { |
| // Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is |
| // on for the current app. We don't want to announce the no-focus hint message |
| // when TalkBack is on because we expect ChromeVox to have no focus in that |
| // case. If we announce the hint message, TalkBack and ChromeVox will |
| // try to speak at the same time. |
| this.dispatchOnCustomSpokenFeedbackToggledEvent(true); |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>Start here</p>', function() { |
| ChromeVoxState.instance.setCurrentRange(null); |
| mockFeedback.call(doCmd('nextObject')) |
| .call( |
| () => assertFalse(mockFeedback.utteranceInQueue( |
| 'No current ChromeVox focus. ' + |
| 'Press Alt+Shift+L to go to the launcher.'))) |
| .call(doCmd('previousObject')) |
| .call( |
| () => assertFalse(mockFeedback.utteranceInQueue( |
| 'No current ChromeVox focus. ' + |
| 'Press Alt+Shift+L to go to the launcher.'))); |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NavigateOutOfMultiline', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <p>before</p> |
| <div contenteditable> |
| Testing testing<br>one two three |
| </div> |
| <p>after</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const contentEditable = |
| root.find({attributes: {nonAtomicTextFieldRoot: true}}); |
| mockFeedback.call(contentEditable.focus.bind(contentEditable)) |
| .expectSpeech(/Testing testing\s+one two three/) |
| .call(doCmd('nextLine')) |
| .expectSpeech('one two three') |
| .call(doCmd('nextLine')) |
| .expectSpeech('after') |
| |
| // In reverse (explicitly focus, instead of moving to previous |
| // line, because all subsequent commands require the content |
| // editable to be focused first): |
| .clearPendingOutput() |
| .call(contentEditable.focus.bind(contentEditable)) |
| .expectSpeech(/Testing testing\s+one two three/) |
| .call(doCmd('nextLine')) |
| .expectSpeech('one two three') |
| .call(doCmd('previousLine')) |
| .expectSpeech('Testing testing') |
| .call(doCmd('previousLine')) |
| .expectSpeech('before') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ReadWindowTitle', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <button id="click"></button> |
| <script> |
| const button = document.getElementById('click'); |
| button.addEventListener('click', _ => document.title = 'bar'); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const clickButtonThenReadCurrentTitle = () => { |
| const desktop = root.parent.root; |
| desktop.addEventListener(EventType.TREE_CHANGED, (evt) => { |
| if (evt.target.role === RoleType.WINDOW && |
| /bar/.test(evt.target.name)) { |
| doCmd('readCurrentTitle')(); |
| } |
| }); |
| const button = root.find({role: RoleType.BUTTON}); |
| button.doDefault(); |
| }; |
| |
| mockFeedback.clearPendingOutput() |
| .call(clickButtonThenReadCurrentTitle) |
| |
| // This test may run against official builds, so match against |
| // utterances starting with 'bar'. This should exclude any other |
| // utterances that contain 'bar' e.g. data:...bar.. or the data url. |
| .expectSpeech(/^bar*/) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'OutputEmptyQueueMode', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>unused</p>', function(root) { |
| const output = new Output(); |
| Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH); |
| output.append_( |
| output.speechBuffer_, new Spannable(''), |
| {annotation: [new OutputAction()]}); |
| output.withString('test'); |
| mockFeedback.clearPendingOutput() |
| .call(output.go.bind(output)) |
| .expectSpeechWithQueueMode('', QueueMode.CATEGORY_FLUSH) |
| .expectSpeechWithQueueMode('test', QueueMode.CATEGORY_FLUSH) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SetAccessibilityFocus', function() { |
| this.runWithLoadedTree('<p>Text.</p><button>Button</button>', function(root) { |
| const node = root.find({role: RoleType.BUTTON}); |
| |
| node.addEventListener(EventType.FOCUS, this.newCallback(function() { |
| chrome.automation.getAccessibilityFocus((focusedNode) => { |
| assertEquals(node, focusedNode); |
| }); |
| })); |
| |
| node.focus(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MenuItemRadio', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <ul role="menu" tabindex="0" autofocus> |
| <li role="menuitemradio" aria-checked="true">Small</li> |
| <li role="menuitemradio" aria-checked="false">Medium</li> |
| <li role="menuitemradio" aria-checked="false">Large</li> |
| </ul> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('Menu', 'with 3 items') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Small, menu item radio button selected', ' 1 of 3 ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Medium, menu item radio button unselected', ' 2 of 3 ') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F( |
| 'ChromeVoxBackgroundTest', 'ButtonNavigationIgnoresRadioButtons', |
| function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <button>Action 1</button> |
| <fieldset> |
| <p><label> <input type=radio>Radio 1</label></p> |
| <p><label> <input type=radio>Radio 2</label></p> |
| </fieldset> |
| <button>Action 2</button> |
| `; |
| |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextButton')) |
| .expectSpeech('Action 1', 'Button') |
| .call(doCmd('nextButton')) |
| .expectSpeech('Action 2', 'Button'); |
| |
| mockFeedback.replay(); |
| }); |
| }); |
| |
| TEST_F( |
| 'ChromeVoxBackgroundTest', 'FocusableNamedDivIsNotContainer', function() { |
| const site = ` |
| <div aria-label="hello world" tabindex="0">hello world</div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const genericContainer = root.find({role: RoleType.GENERIC_CONTAINER}); |
| assertTrue(AutomationPredicate.object(genericContainer)); |
| assertFalse(AutomationPredicate.container(genericContainer)); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'HitTestOnExoSurface', function() { |
| const site = ` |
| <button></button> |
| <input type="text"</input> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const fakeWindow = root.find({role: RoleType.BUTTON}); |
| const realTextField = root.find({role: RoleType.TEXT_FIELD}); |
| |
| // Fake the role and className to imitate a ExoSurface. |
| Object.defineProperty(fakeWindow, 'role', {get: () => RoleType.WINDOW}); |
| Object.defineProperty( |
| fakeWindow, 'className', {get: () => 'ExoSurface-40'}); |
| |
| // Mock and expect a call for the fake window. |
| chrome.accessibilityPrivate.sendSyntheticMouseEvent = |
| this.newCallback(evt => { |
| assertEquals(fakeWindow.location.left, evt.x); |
| assertEquals(fakeWindow.location.top, evt.y); |
| }); |
| |
| // Fake a mouse explore event on the real text field. This should not |
| // trigger the above mouse path. |
| GestureCommandHandler.pointerHandler_.onMouseMove( |
| realTextField.location.left, realTextField.location.top); |
| |
| // Fake a touch explore gesture event on the fake window which should |
| // trigger a mouse move. |
| GestureCommandHandler.onAccessibilityGesture_( |
| Gesture.TOUCH_EXPLORE, fakeWindow.location.left, |
| fakeWindow.location.top); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PointerSkipsContainers', function() { |
| PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = -1; |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="grouparia-label="test" " tabindex=0> |
| <div role=button><p></p></div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| ChromeVoxState.addObserver(new class { |
| onCurrentRangeChanged(range) { |
| if (!range) { |
| ChromeVox.tts.speak('range cleared!'); |
| } |
| } |
| }()); |
| |
| const button = root.find({role: RoleType.BUTTON}); |
| assertNotNullNorUndefined(button); |
| const group = button.parent; |
| assertNotNullNorUndefined(group); |
| mockFeedback.call(simulateHitTestResult(button)) |
| .expectSpeech('Button') |
| .call(() => { |
| // Override the role to simulate panes which are only found in |
| // views. |
| Object.defineProperty(group, 'role', { |
| get() { |
| return chrome.automation.RoleType.PANE; |
| } |
| }); |
| }) |
| .call(simulateHitTestResult(group)) |
| .expectSpeech('range cleared!') |
| .expectEarcon(Earcon.NO_POINTER_ANCHOR) |
| .call(simulateHitTestResult(button)) |
| .expectSpeech('Button') |
| .call(simulateHitTestResult(group)) |
| .expectSpeech('range cleared!') |
| .expectEarcon(Earcon.NO_POINTER_ANCHOR) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'FocusOnUnknown', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div role="group" tabindex=0> |
| <p>hello<p> |
| </div> |
| <div role="group" tabindex=0></div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const [group1, group2] = root.findAll({role: RoleType.GROUP}); |
| assertNotNullNorUndefined(group1); |
| assertNotNullNorUndefined(group2); |
| Object.defineProperty(group1, 'role', { |
| get() { |
| return chrome.automation.RoleType.UNKNOWN; |
| } |
| }); |
| Object.defineProperty(group2, 'role', { |
| get() { |
| return chrome.automation.RoleType.UNKNOWN; |
| } |
| }); |
| |
| const evt2 = new CustomAutomationEvent(EventType.FOCUS, group2); |
| const currentRange = ChromeVoxState.instance.currentRange; |
| DesktopAutomationHandler.instance.onFocus(evt2); |
| assertEquals(currentRange, ChromeVoxState.instance.currentRange); |
| |
| const evt1 = new CustomAutomationEvent(EventType.FOCUS, group1); |
| mockFeedback |
| .call(DesktopAutomationHandler.instance.onFocus.bind( |
| DesktopAutomationHandler.instance, evt1)) |
| .expectSpeech('hello') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TimeDateCommand', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p></p>', function(root) { |
| mockFeedback.call(doCmd('speakTimeAndDate')) |
| .expectSpeech(/(AM|PM)*(2)/) |
| .expectBraille(/(AM|PM)*(2)/) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SwipeToScrollByPage', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| <p style="font-size: 200pt">This is a test</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doGesture(Gesture.SWIPE_UP3)) |
| .expectSpeech(/Page 2 of/) |
| .call(doGesture(Gesture.SWIPE_UP3)) |
| .expectSpeech(/Page 3 of/) |
| .call(doGesture(Gesture.SWIPE_DOWN3)) |
| .expectSpeech(/Page 2 of/) |
| .call(doGesture(Gesture.SWIPE_DOWN3)) |
| .expectSpeech(/Page 1 of/) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PointerOnOffOnRepeatsNode', function() { |
| PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = -1; |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<button>hi</button>', function(root) { |
| ChromeVoxState.addObserver(new class { |
| onCurrentRangeChanged(range) { |
| if (!range) { |
| ChromeVox.tts.speak('range cleared!'); |
| } |
| } |
| }()); |
| |
| const button = root.find({role: RoleType.BUTTON}); |
| assertNotNullNorUndefined(button); |
| mockFeedback.call(simulateHitTestResult(button)) |
| .expectSpeech('hi', 'Button') |
| |
| // Touch slightly off of the button. |
| .call(GestureCommandHandler.onAccessibilityGesture_.bind( |
| null, Gesture.TOUCH_EXPLORE, button.location.left, |
| button.location.top + 60)) |
| .expectSpeech('range cleared!') |
| .expectEarcon(Earcon.NO_POINTER_ANCHOR) |
| .clearPendingOutput() |
| .call(simulateHitTestResult(button)) |
| .expectSpeech('hi', 'Button') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PopupButtonCollapsed', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <select id="button"> |
| <option value="Apple">Apple</option> |
| <option value="Banana">Banana</option> |
| </select> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('jumpToTop')) |
| .expectSpeech( |
| 'Apple', 'Button', 'has pop up', 'Collapsed', |
| 'Press Search+Space to activate') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'PopupButtonExpanded', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <button id="button" aria-haspopup="true" aria-expanded="true" |
| aria-controls="menu"> |
| Click me |
| </button> |
| <ul id="menu" |
| role="menu" |
| aria-labelledby="button"> |
| <li role="menuitem">Item 1</li> |
| <li role="menuitem">Item 2</li> |
| <li role="menuitem">Item 3</li> |
| </ul> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback |
| .call(doCmd('jumpToTop')) |
| // SetSize is only reported if popup button is expanded. |
| .expectSpeech( |
| 'Click me', 'Button', 'has pop up', 'with 3 items', 'Expanded', |
| 'Press Search+Space to activate') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SortDirection', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <table border="1"> |
| <th aria-sort="ascending"><button id="sort">Date</button></th> |
| <tr><td>1/2/20</td></tr> |
| <tr><td>2/2/20</td></tr> |
| </table> |
| <script> |
| let ascending = true; |
| const sortButton = document.getElementById('sort'); |
| sortButton.addEventListener('click', () => { |
| ascending = !ascending; |
| sortButton.parentElement.setAttribute( |
| 'aria-sort', ascending ? 'ascending' : 'descending'); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const sortButton = root.find({role: RoleType.BUTTON}); |
| mockFeedback.expectSpeech('Button', 'Ascending sort') |
| .call(sortButton.doDefault.bind(sortButton)) |
| .expectSpeech('Descending sort') |
| .call(sortButton.doDefault.bind(sortButton)) |
| .expectSpeech('Ascending sort') |
| .call(sortButton.doDefault.bind(sortButton)) |
| .expectSpeech('Descending sort') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'InlineLineNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <p><strong>This</strong><b>is</b>a <em>test</em></p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextLine')) |
| .expectSpeech('This', 'is', 'a ', 'test') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AudioVideo', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <button></button> |
| <button></button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const [audio, video] = root.findAll({role: RoleType.BUTTON}); |
| |
| assertNotNullNorUndefined(audio); |
| assertNotNullNorUndefined(video); |
| |
| assertEquals(undefined, audio.name); |
| assertEquals(undefined, video.name); |
| assertEquals(undefined, audio.firstChild); |
| assertEquals(undefined, video.firstChild); |
| |
| // Fake the roles. |
| Object.defineProperty(audio, 'role', { |
| get() { |
| return chrome.automation.RoleType.AUDIO; |
| } |
| }); |
| |
| Object.defineProperty(video, 'role', { |
| get() { |
| return chrome.automation.RoleType.VIDEO; |
| } |
| }); |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Video') |
| .call(doCmd('previousObject')) |
| .expectSpeech('Audio') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AlertNoAnnouncement', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<button></button>', function(root) { |
| ChromeVoxState.addObserver(new class { |
| onCurrentRangeChanged(range) { |
| assertNotReached('Range was changed unexpectedly.'); |
| } |
| }()); |
| const button = root.find({role: RoleType.BUTTON}); |
| const alertEvt = new CustomAutomationEvent(EventType.ALERT, button); |
| mockFeedback |
| .call(DesktopAutomationHandler.instance.onAlert.bind( |
| DesktopAutomationHandler.instance, alertEvt)) |
| .call(() => assertFalse(mockFeedback.utteranceInQueue('Alert'))) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AlertAnnouncement', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<button>hello world</button>', function(root) { |
| ChromeVoxState.addObserver(new class { |
| onCurrentRangeChanged(range) { |
| assertNotReached('Range was changed unexpectedly.'); |
| } |
| }()); |
| |
| const button = root.find({role: RoleType.BUTTON}); |
| const alertEvt = new CustomAutomationEvent(EventType.ALERT, button); |
| mockFeedback |
| .call(DesktopAutomationHandler.instance.onAlert.bind( |
| DesktopAutomationHandler.instance, alertEvt)) |
| .expectNextSpeechUtteranceIsNot('Alert') |
| .expectSpeech('hello world') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SwipeLeftRight4ByContainers', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree(`<p>test</p>`, function(root) { |
| mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT4)) |
| .expectSpeech('Launcher', 'Button', 'Shelf', 'Tool bar', ', window') |
| .call(doGesture(Gesture.SWIPE_RIGHT4)) |
| .expectSpeech('Shelf', 'Tool bar') |
| .call(doGesture(Gesture.SWIPE_RIGHT4)) |
| .expectSpeech(/Status tray*/) |
| .call(doGesture(Gesture.SWIPE_RIGHT4)) |
| .expectSpeech(/Address and search bar*/) |
| |
| .call(doGesture(Gesture.SWIPE_LEFT4)) |
| .expectSpeech(/Status tray*/) |
| .call(doGesture(Gesture.SWIPE_LEFT4)) |
| .expectSpeech('Shelf', 'Tool bar') |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SwipeLeftRight2', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p id="live" aria-live="polite"</p> |
| <script> |
| document.body.addEventListener('keydown', (evt) => { |
| document.getElementById('live').textContent = evt.key; |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT2)).expectSpeech('Enter'); |
| mockFeedback.call(doGesture(Gesture.SWIPE_LEFT2)) |
| .expectSpeech('Escape') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'DialogAutoSummaryTextContent', function() { |
| this.resetContextualOutput(); |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div role="dialog" aria-label="Setup"> |
| <h1>Welcome</h1> |
| <p>This is some introductory text<p> |
| <button>Exit</button> |
| <button>Let's go</button> |
| </div> |
| <p>end</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Setup', 'Dialog') |
| .expectSpeech(`Welcome This is some introductory text Exit Let's go`) |
| .expectSpeech('Welcome', 'Heading 1') |
| .call(doCmd('nextObject')) |
| .expectSpeech('This is some introductory text') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Exit', 'Button') |
| .call(doCmd('nextObject')) |
| .expectSpeech(`Let's go`, 'Button') |
| .call(doCmd('nextObject')) |
| .expectSpeech('end') |
| |
| .call(doCmd('previousObject')) |
| .expectSpeech(`Let's go`, 'Button') |
| .expectSpeech('Setup', 'Dialog') |
| .expectSpeech(`Welcome This is some introductory text Exit Let's go`) |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ImageAnnotations', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <img alt="bar" src="data:image/png;base64,iVBORw0KGgoAAAANS"> |
| <img src="data:image/png;base64,iVBORw0KGgoAAAANS"> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const [namedImg, unnamedImg] = root.findAll({role: RoleType.IMAGE}); |
| |
| assertNotNullNorUndefined(namedImg); |
| assertNotNullNorUndefined(unnamedImg); |
| |
| assertEquals('bar', namedImg.name); |
| assertEquals(undefined, unnamedImg.name); |
| |
| // Fake the image annotation. |
| Object.defineProperty(namedImg, 'imageAnnotation', { |
| get() { |
| return 'foo'; |
| } |
| }); |
| Object.defineProperty(unnamedImg, 'imageAnnotation', { |
| get() { |
| return 'foo'; |
| } |
| }); |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('start') |
| .expectNextSpeechUtteranceIsNot('foo') |
| .expectSpeech('bar', 'Image') |
| .call(doCmd('nextObject')) |
| .expectNextSpeechUtteranceIsNot('bar') |
| .expectSpeech('foo', 'Image') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'VolumeChanges', function() { |
| const mockFeedback = this.createMockFeedback(); |
| this.runWithLoadedTree('<p>test</p>', function() { |
| const bounds = ChromeVoxState.instance.getFocusBounds(); |
| mockFeedback.call(press(KeyCode.VOLUME_UP)) |
| .expectSpeech('Volume', 'Slider', /\d+%/) |
| .call(() => { |
| // The bounds should not have changed. |
| assertEquals( |
| JSON.stringify(bounds), |
| JSON.stringify(ChromeVoxState.instance.getFocusBounds())); |
| }) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'WrapContentEditableAtEndOfDoc', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = `<p>start</p> |
| <div role="textbox" contenteditable aria-multiline="false"></div>`; |
| this.runWithLoadedTree(site, function() { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Edit text') |
| .call(doCmd('nextObject')) |
| .expectEarcon(Earcon.WRAP) |
| .expectSpeech('Web Content') |
| .call(doCmd('nextObject')) |
| .expectSpeech('start') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ReadFromHereBlankNodes', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = `<a tabindex=0></a><p>start</p><a tabindex=0></a><p>end</p>`; |
| this.runWithLoadedTree(site, function(root) { |
| assertEquals( |
| RoleType.STATIC_TEXT, |
| ChromeVoxState.instance.currentRange.start.node.role); |
| |
| // "start" is uttered twice, once for the initial focus as the page loads, |
| // and once during the 'read from here' command. |
| mockFeedback.expectSpeech('start') |
| .call(doCmd('readFromHere')) |
| .expectSpeech('start', 'end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ContainerButtons', function() { |
| const mockFeedback = this.createMockFeedback(); |
| |
| // This pattern can be found in ARC++/YouTube. |
| const site = ` |
| <p>videos</p> |
| <div aria-label="Cat Video" role="button"> |
| <div role="group">4 minutes, Cat Video</div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const group = root.find({role: RoleType.GROUP}); |
| |
| Object.defineProperty(group, 'clickable', { |
| get() { |
| return true; |
| } |
| }); |
| |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Cat Video', 'Button') |
| .call(doCmd('nextObject')) |
| .expectSpeech('4 minutes, Cat Video') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'FocusOnWebAreaIgnoresEvents', function() { |
| const site = ` |
| <div role="application" tabindex=0 aria-label="container"> |
| <select> |
| <option>apple</option> |
| <option>grape</option> |
| <option>pear</option> |
| </select> |
| </div> |
| <p>go</p> |
| <script> |
| let counter = 0; |
| document.body.getElementsByTagName('p')[0].addEventListener('click', |
| e => { |
| document.body.getElementsByTagName('select')[0].selectedIndex = |
| ++counter % 3; |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, async function(root) { |
| const application = root.find({role: RoleType.APPLICATION}); |
| const popUpButton = root.find({role: RoleType.POP_UP_BUTTON}); |
| const p = root.find({role: RoleType.PARAGRAPH}); |
| |
| // Move focus to the select, which honors value changes through |
| // FocusAutomationHandler. |
| popUpButton.focus(); |
| await TestUtils.waitForSpeech('apple'); |
| |
| // Clicking the paragraph programmatically changes the select value. |
| p.doDefault(); |
| await TestUtils.waitForSpeech('grape'); |
| assertEquals( |
| RoleType.POP_UP_BUTTON, |
| ChromeVoxState.instance.currentRange.start.node.role); |
| |
| // Now, move focus to the application which is a parent of the select. |
| application.focus(); |
| await TestUtils.waitForSpeech('container'); |
| |
| // Hook into the speak call, to see what comes next. |
| let nextSpeech; |
| ChromeVox.tts.speak = textString => { |
| nextSpeech = textString; |
| }; |
| |
| // Trigger another value update for the select. |
| p.doDefault(); |
| |
| // This comes when the <select>'s value changes. |
| await TestUtils.waitForEvent(application, EventType.SELECTED_VALUE_CHANGED); |
| |
| // Nothing should have been spoken. |
| assertEquals(undefined, nextSpeech); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AriaLeaves', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="radio"><p>PM</p></div> |
| <div role="switch"><p>Agree</p></div> |
| <div role="checkbox"><p>Agree</p></div> |
| <script> |
| const p = document.getElementsByTagName('p')[0]; |
| p.addEventListener('click', () => {}); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('PM, radio button unselected') |
| .call(doCmd('nextObject')) |
| .expectSpeech('PM') |
| .call( |
| () => assertEquals( |
| RoleType.STATIC_TEXT, |
| ChromeVoxState.instance.currentRange.start.node.role)) |
| |
| .call(doCmd('nextObject')) |
| .expectSpeech('Agree, switch off') |
| .call( |
| () => assertEquals( |
| RoleType.SWITCH, |
| ChromeVoxState.instance.currentRange.start.node.role)) |
| |
| .call(doCmd('nextObject')) |
| .expectSpeech('Agree', 'Check box') |
| .call( |
| () => assertEquals( |
| RoleType.CHECK_BOX, |
| ChromeVoxState.instance.currentRange.start.node.role)) |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MarkedContent', function() { |
| this.resetContextualOutput(); |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <span>This is </span><span role="mark">my</span><span> text.</span> |
| <br> |
| <span>This is </span><span role="mark" |
| aria-roledescription="Comment">your</span><span> text.</span> |
| <br> |
| <span>This is </span><span role="suggestion"><span |
| role="insertion">their</span></span><span> text.</span> |
| <br> |
| <span>This is </span><span role="suggestion"><span |
| role="deletion">everyone's</span></span><span> text.</span> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| mockFeedback.expectSpeech('Start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('This is ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Marked content', 'my', 'Marked content end') |
| .expectBraille('Marked content my Marked content end') |
| .call(doCmd('nextObject')) |
| .expectSpeech(' text.') |
| .expectBraille(' text.') |
| .call(doCmd('nextObject')) |
| .expectSpeech('This is ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Comment', 'your', 'Comment end') |
| .expectBraille('Comment your Comment end') |
| .call(doCmd('nextObject')) |
| .expectSpeech(' text.') |
| .expectBraille(' text.') |
| .call(doCmd('nextObject')) |
| .expectSpeech('This is ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Suggest', 'Insert', 'their', 'Insert end', 'Suggest end') |
| .expectBraille('Suggest Insert their Insert end Suggest end') |
| .call(doCmd('nextObject')) |
| .expectSpeech(' text.') |
| .expectBraille(' text.') |
| .call(doCmd('nextObject')) |
| .expectSpeech('This is ') |
| .call(doCmd('nextObject')) |
| .expectSpeech( |
| 'Suggest', 'Delete', `everyone's`, 'Delete end', 'Suggest end') |
| .expectBraille(`Suggest Delete everyone's Delete end Suggest end`) |
| .call(doCmd('nextObject')) |
| .expectSpeech(' text.') |
| .expectBraille(' text.') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ClickAncestorAreNotActionable', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <div id="button1" role="button" aria-label="OK"> |
| <div role="group">OK</div> |
| </div> |
| <div id="button2" role="button" aria-label="cancel"> |
| <a href="#cancel">more info</a> |
| </div> |
| <p>end</p> |
| <script> |
| document.getElementById('button1').addEventListener('click', () => {}); |
| document.getElementById('button2').addEventListener('click', () => {}); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| mockFeedback.expectSpeech('Start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('OK') |
| .call(doCmd('nextObject')) |
| .expectSpeech('cancel') |
| .call(doCmd('nextObject')) |
| .expectSpeech('more info') |
| .call(doCmd('nextObject')) |
| .expectSpeech('end') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TouchEditingState', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <input type="text"></input> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| const bounds = rootNode.find({role: RoleType.TEXT_FIELD}).location; |
| mockFeedback.expectSpeech('Start') |
| .call(doGesture( |
| chrome.accessibilityPrivate.Gesture.TOUCH_EXPLORE, bounds.left, |
| bounds.top)) |
| .expectSpeech('Edit text', 'Double tap to start editing') |
| .call(doGesture( |
| chrome.accessibilityPrivate.Gesture.CLICK, bounds.left, bounds.top)) |
| .expectSpeech('Edit text', 'is editing') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'TouchGesturesProducesEarcons', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <button>ok</button> |
| <a href="chromevox.com">cancel</a> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| mockFeedback.expectSpeech('Start') |
| .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1)) |
| .expectSpeech('ok', 'Button') |
| .expectEarcon(Earcon.BUTTON) |
| .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1)) |
| .expectSpeech('cancel', 'Link') |
| .expectEarcon(Earcon.LINK) |
| .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_LEFT1)) |
| .expectSpeech('ok', 'Button') |
| .expectEarcon(Earcon.BUTTON) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'Separator', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <p><span>Hello</span></p> |
| <p><span role="separator">Separator content should be read</span></p> |
| <p><span>World</span></p> |
| `; |
| this.runWithLoadedTree(site, function(rootNode) { |
| mockFeedback.expectSpeech('Start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Hello') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Separator content should be read', 'Separator') |
| .expectBraille('Separator content should be read seprtr') |
| .call(doCmd('nextObject')) |
| .expectSpeech('World') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'FocusAfterClick', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <button id="clickMe">Click me</button> |
| <h1 id="focusMe" tabindex=-1>Focus me</h1> |
| <script> |
| document.getElementById('clickMe').addEventListener('click', function() { |
| document.getElementById('focusMe').focus(); |
| }, false); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| DesktopAutomationHandler.announceActions = false; |
| mockFeedback.expectSpeech('Start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Click me') |
| .call(doCmd('forceClickOnCurrentItem')) |
| .expectSpeech('Focus me') |
| .call(() => { |
| assertEquals( |
| 'Focus me', |
| ChromeVoxState.instance.getCurrentRange().start.node.name); |
| }) |
| .replay(); |
| }); |
| }); |
| |
| SYNC_TEST_F('ChromeVoxBackgroundTest', 'EarconPlayback', function() { |
| const engine = ChromeVoxState.instance.earcons_.engine_; |
| assertTrue(engine !== undefined); |
| |
| // We only test a few earcons here. Not all earcons prevent parallel playback |
| // or have mappings into the earcon engine. |
| |
| // There are no tracked sources yet. |
| assertEquals(0, Object.keys(engine.lastEarconSources_).length); |
| |
| // Note that alert modal vs nonmodal would be allowed to play in parallel (as |
| // do wrap / wrap edge) because they are different events even though they |
| // really play the same sound. |
| ChromeVox.earcons.playEarcon(Earcon.ALERT_MODAL); |
| assertEquals(1, Object.keys(engine.lastEarconSources_).length); |
| const lastAlertSource = engine.lastEarconSources_[Earcon.ALERT_MODAL]; |
| assertTrue(lastAlertSource !== undefined); |
| |
| ChromeVox.earcons.playEarcon(Earcon.ALERT_MODAL); |
| assertEquals(1, Object.keys(engine.lastEarconSources_).length); |
| |
| // The earcon for this stayed the same (above), so there's no duplicate |
| // playback of two alerts. |
| assertEquals(lastAlertSource, engine.lastEarconSources_[Earcon.ALERT_MODAL]); |
| |
| // This simulates a parallel playback of the button earcon which is allowed. |
| ChromeVox.earcons.playEarcon(Earcon.BUTTON); |
| assertEquals(2, Object.keys(engine.lastEarconSources_).length); |
| assertTrue(engine.lastEarconSources_[Earcon.BUTTON] !== undefined); |
| |
| // This gets called by web audio when the earcon finishes. |
| lastAlertSource.onended(); |
| |
| // The button earcon is still playing. |
| assertEquals(1, Object.keys(engine.lastEarconSources_).length); |
| assertTrue(engine.lastEarconSources_[Earcon.BUTTON] !== undefined); |
| |
| // Finish up the button earcon, too. |
| engine.lastEarconSources_[Earcon.BUTTON].onended(); |
| assertEquals(0, Object.keys(engine.lastEarconSources_).length); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MixedNavWithRangeInvalidation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>Start</p> |
| <button>apple</button> |
| <a href="google.com">grape</a> |
| <button>banana</button> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| // Different ways to navigate to the next object. |
| const keyboardHandler = ChromeVoxState.instance.keyboardHandler_; |
| const nextObjectKeyboard = keyboardHandler.onKeyDown.bind(keyboardHandler, { |
| keyCode: KeyCode.RIGHT, |
| metaKey: true, |
| preventDefault: () => {}, |
| stopPropagation: () => {} |
| }); |
| const nextObjectBraille = BrailleCommandHandler.onBrailleKeyEvent.bind( |
| BrailleCommandHandler, {command: BrailleKeyCommand.PAN_RIGHT}); |
| const nextObjectGesture = |
| GestureCommandHandler.onAccessibilityGesture_.bind( |
| GestureCommandHandler, Gesture.SWIPE_RIGHT1); |
| const clearCurrentRange = ChromeVoxState.instance.setCurrentRange.bind( |
| ChromeVoxState.instance, null); |
| const toggleTalkBack = () => { |
| ChromeVoxState.instance.talkBackEnabled = |
| !ChromeVoxState.instance.talkBackEnabled; |
| }; |
| |
| mockFeedback |
| .expectSpeech('Start') |
| |
| .call(clearCurrentRange) |
| .call(nextObjectKeyboard) |
| .expectSpeech('apple', 'Button') |
| |
| .call(clearCurrentRange) |
| .call(nextObjectBraille) |
| .expectSpeech('grape', 'Link') |
| |
| .call(clearCurrentRange) |
| .call(nextObjectGesture) |
| .expectSpeech('banana', 'Button') |
| |
| .call(clearCurrentRange) |
| .call(nextObjectKeyboard) |
| .expectSpeech('Web Content') |
| |
| .call(clearCurrentRange) |
| .call(toggleTalkBack) |
| .call(nextObjectKeyboard) |
| .call(() => assertFalse(!!ChromeVoxState.instance.currentRange)) |
| |
| .call(nextObjectBraille) |
| .call(() => assertFalse(!!ChromeVoxState.instance.currentRange)) |
| |
| .call(nextObjectGesture) |
| .call(() => assertFalse(!!ChromeVoxState.instance.currentRange)) |
| |
| .call(toggleTalkBack) |
| .call(nextObjectKeyboard) |
| .call(() => assertTrue(!!ChromeVoxState.instance.currentRange)) |
| |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'DetailsChanged', function() { |
| const mockFeedback = this.createMockFeedback(); |
| |
| // Make sure we're not testing reading of the hint from the button's output |
| // below. |
| localStorage['useVerboseMode'] = false; |
| const site = ` |
| <button id="click">ok</button> |
| <p id="details">hello</p> |
| <script> |
| const button = document.getElementById('click'); |
| button.addEventListener('click', () => { |
| button.setAttribute('aria-details', 'details'); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const button = root.find({role: RoleType.BUTTON}); |
| mockFeedback.expectSpeech('ok') |
| .call(button.doDefault.bind(button)) |
| .expectSpeech('Press Search+A, J to jump to details') |
| .replay(); |
| }); |
| }); |
| |
| SYNC_TEST_F('ChromeVoxBackgroundTest', 'PageLoadEarcons', async function() { |
| const sawEarcons = []; |
| const fakeEarcons = {playEarcon: (earcon) => sawEarcons.push(earcon)}; |
| Object.defineProperty(ChromeVox, 'earcons', {get: () => fakeEarcons}); |
| AutomationUtil.getTopLevelRoot = (node) => node; |
| |
| const module = await import('./page_load_sound_handler.js'); |
| const PageLoadSoundHandler = module.PageLoadSoundHandler; |
| |
| // Use this specific object to control the load environment. |
| const handler = new PageLoadSoundHandler(); |
| |
| // Build up a fake automation node with a parent and root. |
| const fakeNode = {}; |
| fakeNode.docUrl = 'foo'; |
| fakeNode.root = fakeNode; |
| fakeNode.parent = {state: {focused: true}}; |
| |
| handler.onLoadStart({target: fakeNode}); |
| assertEqualStringArrays([Earcon.PAGE_START_LOADING], sawEarcons); |
| handler.onLoadComplete({target: fakeNode}); |
| assertEqualStringArrays( |
| [Earcon.PAGE_START_LOADING, Earcon.PAGE_FINISH_LOADING], sawEarcons); |
| |
| // No extra earcons. |
| handler.onLoadComplete({target: fakeNode}); |
| assertEqualStringArrays( |
| [Earcon.PAGE_START_LOADING, Earcon.PAGE_FINISH_LOADING], sawEarcons); |
| |
| // Try a range change that finishes the load sound. |
| sawEarcons.length = 0; |
| handler.onLoadStart({target: fakeNode}); |
| fakeNode.docLoadingProgress = 1; |
| handler.onCurrentRangeChanged({start: {node: fakeNode}}); |
| assertEqualStringArrays( |
| [Earcon.PAGE_START_LOADING, Earcon.PAGE_FINISH_LOADING], sawEarcons); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NewTabRead', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = `<p>start</p><p>end</p>`; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('end') |
| .call(press(KeyCode.T, {ctrl: true})) |
| .expectSpeech(/New Tab/) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NestedMenuHints', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <div role="menu" aria-orientation="vertical"> |
| <div role="menu" aria-orientation="horizontal"> |
| <div tabindex="0" role="menuitem">hello</div> |
| <div tabindex="0" role="menuitem">bro</div> |
| </div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback |
| .expectSpeech( |
| 'Press left or right arrow to navigate; enter to activate') |
| .call( |
| () => assertFalse(mockFeedback.utteranceInQueue( |
| 'Press up or down arrow to navigate; enter to activate'))) |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SkipLabelDescriptionFor', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <label> |
| <input type="checkbox" name="enableSpeechLogging"> |
| <span>Enable speech logging</span> |
| </label> |
| <p>end</p> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('start') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Enable speech logging', 'Check box') |
| .call(doCmd('nextObject')) |
| .expectSpeech('end') |
| .call(doCmd('previousObject')) |
| .expectSpeech('Enable speech logging', 'Check box') |
| .call(doCmd('previousObject')) |
| .expectSpeech('start') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'Abbreviation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <abbr title="uniform resource locator">URL</abbr> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.expectSpeech('URL', 'uniform resource locator', 'Abbreviation') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'EndOfText', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div tabindex=0 role="textbox" contenteditable><p>abc</p><p>123</p></div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const contentEditable = root.find({role: RoleType.TEXT_FIELD}); |
| |
| this.listenOnce(contentEditable, EventType.FOCUS, function() { |
| mockFeedback.call(press(KeyCode.RIGHT)) |
| .expectSpeech('b') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('c') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('\n') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('1') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('2') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('3') |
| .call(press(KeyCode.RIGHT)) |
| .expectSpeech('End of text') |
| |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('3') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('2') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('1') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('\n') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('c') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('b') |
| .call(press(KeyCode.LEFT)) |
| .expectSpeech('a') |
| |
| .replay(); |
| }.bind(this)); |
| contentEditable.focus(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenuOnViewsTab', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = `<p>test</p>`; |
| this.runWithLoadedTree(site, function(root) { |
| const tabs = root.findAll({Role: RoleType.TAB}); |
| assertTrue(tabs.length > 0); |
| tabs[0].showContextMenu(); |
| mockFeedback.expectSpeech(/menu opened/).replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'SelectWithOptGroup', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <select> |
| <optgroup label="Theropods"> |
| <option>Tyrannosaurus</option> |
| <option>Velociraptor</option> |
| <option>Deinonychus</option> |
| </optgroup> |
| </select> |
| `; |
| this.runWithLoadedTree(site, function() { |
| mockFeedback.expectSpeech('Tyrannosaurus', 'has pop up', 'Collapsed') |
| .call(doCmd('forceClickOnCurrentItem')) |
| .expectSpeech('Tyrannosaurus') |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('Velociraptor') |
| .call(press(KeyCode.DOWN)) |
| .expectSpeech('Deinonychus') |
| .call(press(KeyCode.UP)) |
| .expectSpeech('Velociraptor') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'GroupNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p><span>hello</span><a href="a.com">hi</a><a href="a.com">hey</a></p> |
| <p><span>goodbye</span><a href="a.com">bye</a><a href="a.com">chow</a></p> |
| `; |
| this.runWithLoadedTree(site, function() { |
| mockFeedback.call(doCmd('nextGroup')) |
| .expectSpeech('goodbye', 'bye', 'Link', 'chow', 'Link') |
| .call(doCmd('nextObject')) |
| .expectSpeech('goodbye') |
| .call(doCmd('nextObject')) |
| .expectSpeech('bye', 'Link') |
| .call(doCmd('previousGroup')) |
| .expectSpeech('hello', 'hi', 'Link', 'hey', 'Link') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'AllowIframeToBeFocused', function(root) { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>hello</p> |
| <iframe id="frame" tabindex=-1 |
| srcdoc="<title>test title</title><p>world</p>"></iframe> |
| <button id="click"></button> |
| <script> |
| const button = document.getElementById('click'); |
| button.addEventListener('click', () => { |
| document.getElementById('frame').focus(); |
| }); |
| </script> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| const button = root.find({role: RoleType.BUTTON}); |
| mockFeedback.expectSpeech('hello') |
| .call(button.doDefault.bind(button)) |
| .expectSpeech('test title') |
| .replay(); |
| }); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'NewWindowWebSpeech', function() { |
| this.newCallback(async () => { |
| const speech = []; |
| let onSpeech; |
| ChromeVox.tts.speak = (textString) => { |
| speech.push(textString); |
| if (onSpeech) { |
| onSpeech(textString); |
| } |
| }; |
| |
| chrome.runtime.openOptionsPage(); |
| |
| await new Promise(resolve => { |
| onSpeech = (textString) => { |
| if (textString === 'ChromeVox Options') { |
| resolve(); |
| } |
| }; |
| }); |
| |
| press(KeyCode.TAB)(); |
| |
| await new Promise(resolve => { |
| onSpeech = resolve; |
| }); |
| |
| // Check to ensure there are no duplicate announcements. |
| assertEquals( |
| speech.indexOf('ChromeVox Options'), |
| speech.lastIndexOf('ChromeVox Options')); |
| |
| // Ensure there are no announcements about the Tab role. |
| assertTrue(speech.every(text => { |
| return text.indexOf('Tab') !== 0; |
| })); |
| })(); |
| }); |
| |
| TEST_F('ChromeVoxBackgroundTest', 'MultipleListBoxes', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = ` |
| <p>start</p> |
| <div role="listbox" aria-expanded="false" aria-label="Configuration 1"> |
| <div role="presentation"> |
| <div role="presentation"> |
| <div aria-selected="true" role="option" tabindex="0"> |
| <span>Listbox item 1</span> |
| </div> |
| <div aria-selected="false" role="option" tabindex="-1"> |
| <span>Listbox item 2</span> |
| </div> |
| <div aria-selected="false" role="option" tabindex="-2"> |
| <span>Listbox item 3</span> |
| </div> |
| </div> |
| <div role="presentation"></div> |
| </div> |
| </div> |
| |
| <div role="listbox" aria-expanded="false" aria-label="Configuration 2"> |
| <div role="presentation"> |
| <div role="presentation"> |
| <div aria-selected="false" role="option" tabindex="-1"> |
| <span>Listbox item 1</span> |
| </div> |
| <div aria-selected="true" role="option" tabindex="0"> |
| <span>Listbox item 2</span> |
| </div> |
| <div aria-selected="false" role="option" tabindex="-2"> |
| <span>Listbox item 3</span> |
| </div> |
| </div> |
| <div role="presentation"></div> |
| </div> |
| </div> |
| |
| <div role="listbox" aria-expanded="false" aria-label="Configuration 3"> |
| <div role="presentation"> |
| <div role="presentation"> |
| <div aria-selected="false" role="option" tabindex="-1"> |
| <span>Listbox item 1</span> |
| </div> |
| <div aria-selected="false" role="option" tabindex="-2"> |
| <span>Listbox item 2</span> |
| </div> |
| <div aria-selected="true" role="option" tabindex="0"> |
| <span>Listbox item 3</span> |
| </div> |
| </div> |
| <div role="presentation"></div> |
| </div> |
| </div> |
| `; |
| this.runWithLoadedTree(site, function(root) { |
| mockFeedback.call(press(KeyCode.TAB)) |
| .expectSpeech( |
| 'Listbox item 1', ' 1 of 3 ', 'Configuration 1', 'List box') |
| .call(press(KeyCode.TAB)) |
| .expectSpeech( |
| 'Listbox item 2', ' 2 of 3 ', 'Configuration 2', 'List box') |
| .call(press(KeyCode.TAB)) |
| .expectSpeech( |
| 'Listbox item 3', ' 3 of 3 ', 'Configuration 3', 'List box') |
| .replay(); |
| }); |
| }); |
| |
| // Make sure linear navigation does not go inside ListBox's options. |
| TEST_F('ChromeVoxBackgroundTest', 'ListBoxLinearNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| const site = |
| |
| this.runWithLoadedTree(this.listBoxDoc, function(root) { |
| mockFeedback.call(doCmd('nextObject')) |
| .expectSpeech('Select an item', 'List box') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Click', 'Button') |
| .call(doCmd('previousObject')) |
| .expectSpeech('Select an item', 'List box') |
| .replay(); |
| }); |
| }); |
| |
| // Make sure navigation with Tab to ListBox lands on options. |
| TEST_F('ChromeVoxBackgroundTest', 'ListBoxItemsNavigation', function() { |
| const mockFeedback = this.createMockFeedback(); |
| |
| this.runWithLoadedTree(this.listBoxDoc, function(root) { |
| mockFeedback.call(press(KeyCode.TAB)) |
| .expectSpeech( |
| 'Listbox item one', ' 1 of 3 ', 'Select an item', 'List box') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Listbox item two', ' 2 of 3 ') |
| .call(doCmd('nextObject')) |
| .expectSpeech('Listbox item three', ' 3 of 3 ') |
| .replay(); |
| }); |
| }); |