diff options
Diffstat (limited to 'tools/accessibility')
-rw-r--r-- | tools/accessibility/nvda/OWNERS | 3 | ||||
-rw-r--r-- | tools/accessibility/nvda/README.txt | 58 | ||||
-rw-r--r-- | tools/accessibility/nvda/nvda_chrome_tests.py | 229 |
3 files changed, 290 insertions, 0 deletions
diff --git a/tools/accessibility/nvda/OWNERS b/tools/accessibility/nvda/OWNERS new file mode 100644 index 0000000..c50d5b8 --- /dev/null +++ b/tools/accessibility/nvda/OWNERS @@ -0,0 +1,3 @@ +aboxhall@chromium.org +dmazzoni@chromium.org +dtseng@chromium.org diff --git a/tools/accessibility/nvda/README.txt b/tools/accessibility/nvda/README.txt new file mode 100644 index 0000000..ce34d93 --- /dev/null +++ b/tools/accessibility/nvda/README.txt @@ -0,0 +1,58 @@ +This directory contains semi-automated tests of Chrome with +NVDA (NonVisual Desktop Access), a popular open-source screen reader for +visually impaired users on Windows. It works by launching Chrome in a +subprocess, then launching NVDA in a special environment that simulates +speech rather than actually speaking, and ignores all events coming from +processes other than a specific Chrome process ID. Each test automates +Chrome with a series of actions and asserts that NVDA gives the expected +feedback in response. + +Instructions for running these tests: + +1. Install Python 2.7, 32-bit: http://www.python.org/ + + Note - the version of Python installed by Chrome's depot_tools will not + work, it's 64-bit. + +2. Download pywinauto here: + https://code.google.com/p/pywinauto/downloads/list + + Unzip it, then install it by running this from a cmd shell in that directory: + python setup.py install + + If you get an error, make sure you're using the 32-bit version of Python. + +3. Install the latest NVDA "next" snapshot from: + http://community.nvda-project.org/wiki/Snapshots + + In the installer, choose "Create Portable copy" rather than "Install...". + From the Browse dialog, create an new folder called nvdaPortable inside + this folder. + + Note: after NVDA 2014.3 stable is released, just use the stable version + instead, from http://www.nvaccess.org/download/ + - if you do this, you need to run NVDA, then from the NVDA menu, choose + Tools > Create Portable Copy. + From the Browse dialog, create an new folder called nvdaPortable inside + this folder. + You should now have something like this: + d:\src\nvda_chrome_tests\nvdaPortable\nvda.exe + You can now exit NVDA. + +4. Install Chrome Canary. The binary is typically installed in: + c:\Users\USERNAME\AppData\Local\Google\Chrome SxS\Application\chrome.exe + ...if not, edit nvda_chrome_tests.py to point to it. + +5. Clone the nvda-proctest environment into this directory: + git clone https://bitbucket.org/nvaccess/nvda-proctest.git + +6. Run the tests: + + First make sure NVDA is not already running. + + Open a cmd console, change to the nvda_chrome_tests directory, and run: + python nvda_chrome_tests.py + + If you get an error, open the Windows task manager and make sure NVDA + isn't running, kill it if necessary. + diff --git a/tools/accessibility/nvda/nvda_chrome_tests.py b/tools/accessibility/nvda/nvda_chrome_tests.py new file mode 100644 index 0000000..6cbfd1b --- /dev/null +++ b/tools/accessibility/nvda/nvda_chrome_tests.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# 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. + +"""Semi-automated tests of Chrome with NVDA. + +This file performs (semi) automated tests of Chrome with NVDA +(NonVisual Desktop Access), a popular open-source screen reader for +visually impaired users on Windows. It works by launching Chrome in a +subprocess, then launching NVDA in a special environment that simulates +speech rather than actually speaking, and ignores all events coming from +processes other than a specific Chrome process ID. Each test automates +Chrome with a series of actions and asserts that NVDA gives the expected +feedback in response. + +The tests are "semi" automated in the sense that they are not intended to be +run from any developer machine, or on a buildbot - it requires setting up the +environment according to the instructions in README.txt, then running the +test script, then filing bugs for any potential failures. If the environment +is set up correctly, the actual tests should run automatically and unattended. +""" + +import os +import pywinauto +import re +import shutil +import signal +import subprocess +import sys +import tempfile +import time +import unittest + +CHROME_PROFILES_PATH = os.path.join(os.getcwd(), 'chrome_profiles') +CHROME_PATH = os.path.join(os.environ['USERPROFILE'], + 'AppData', + 'Local', + 'Google', + 'Chrome SxS', + 'Application', + 'chrome.exe') +NVDA_PATH = os.path.join(os.getcwd(), + 'nvdaPortable', + 'nvda_noUIAccess.exe') +NVDA_PROCTEST_PATH = os.path.join(os.getcwd(), + 'nvda-proctest') +NVDA_LOGPATH = os.path.join(os.getcwd(), + 'nvda_log.txt') +WAIT_FOR_SPEECH_TIMEOUT_SECS = 3.0 + +class NvdaChromeTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + print 'user data: %s' % CHROME_PROFILES_PATH + print 'chrome: %s' % CHROME_PATH + print 'nvda: %s' % NVDA_PATH + print 'nvda_proctest: %s' % NVDA_PROCTEST_PATH + + print + print 'Clearing user data directory and log file from previous runs' + if os.access(NVDA_LOGPATH, os.F_OK): + os.remove(NVDA_LOGPATH) + if os.access(CHROME_PROFILES_PATH, os.F_OK): + shutil.rmtree(CHROME_PROFILES_PATH) + os.mkdir(CHROME_PROFILES_PATH, 0777) + + def handler(signum, frame): + print 'Test interrupted, attempting to kill subprocesses.' + self.tearDown() + sys.exit() + signal.signal(signal.SIGINT, handler) + + def setUp(self): + user_data_dir = tempfile.mkdtemp(dir = CHROME_PROFILES_PATH) + args = [CHROME_PATH, + '--user-data-dir=%s' % user_data_dir, + '--no-first-run', + 'about:blank'] + print + print ' '.join(args) + self._chrome_proc = subprocess.Popen(args) + self._chrome_proc.poll() + if self._chrome_proc.returncode is None: + print 'Chrome is running' + else: + print 'Chrome exited with code', self._chrome_proc.returncode + sys.exit() + print 'Chrome pid: %d' % self._chrome_proc.pid + + os.environ['NVDA_SPECIFIC_PROCESS'] = str(self._chrome_proc.pid) + + args = [NVDA_PATH, + '-m', + '-c', + NVDA_PROCTEST_PATH, + '-f', + NVDA_LOGPATH] + self._nvda_proc = subprocess.Popen(args) + self._nvda_proc.poll() + if self._nvda_proc.returncode is None: + print 'NVDA is running' + else: + print 'NVDA exited with code', self._nvda_proc.returncode + sys.exit() + print 'NVDA pid: %d' % self._nvda_proc.pid + + app = pywinauto.application.Application() + app.connect_(process = self._chrome_proc.pid) + self._pywinauto_window = app.top_window_() + + try: + self._WaitForSpeech(['Address and search bar edit', 'about:blank']) + except: + self.tearDown() + + def tearDown(self): + print + print 'Shutting down' + + self._chrome_proc.poll() + if self._chrome_proc.returncode is None: + print 'Killing Chrome subprocess' + self._chrome_proc.kill() + else: + print 'Chrome already died.' + + self._nvda_proc.poll() + if self._nvda_proc.returncode is None: + print 'Killing NVDA subprocess' + self._nvda_proc.kill() + else: + print 'NVDA already died.' + + def _GetSpeechFromNvdaLogFile(self): + """Return everything NVDA would have spoken as a list of strings. + + Parses lines like this from NVDA's log file: + Speaking [LangChangeCommand ('en'), u'Google Chrome', u'window'] + Speaking character u'slash' + + Returns a single list of strings like this: + [u'Google Chrome', u'window', u'slash'] + """ + if not os.access(NVDA_LOGPATH, os.F_OK): + return [] + lines = open(NVDA_LOGPATH).readlines() + regex = re.compile(r"u'((?:[^\'\\]|\\.)*)\'") + result = [] + for line in lines: + for m in regex.finditer(line): + speech_with_whitespace = m.group(1) + speech_stripped = re.sub(r'\s+', ' ', speech_with_whitespace).strip() + result.append(speech_stripped) + return result + + def _WaitForSpeech(self, expected): + """Block until the last speech in NVDA's log file is the given string(s). + + Repeatedly parses the log file until the last speech line(s) in the + log file match the given strings, or it times out. + + Args: + expected: string or a list of string - only succeeds if these are the last + strings spoken, in order. + + Returns when those strings are spoken, or throws an error if it times out + waiting for those strings. + """ + if type(expected) is type(''): + expected = [expected] + start_time = time.time() + while True: + lines = self._GetSpeechFromNvdaLogFile() + if (lines[-len(expected):] == expected): + break + + if time.time() - start_time >= WAIT_FOR_SPEECH_TIMEOUT_SECS: + print '** Speech from NVDA so far:' + for line in lines: + print '"%s"' % line + print '** Was waiting for:' + for line in expected: + print '"%s"' % line + raise Exception('Timed out') + time.sleep(0.1) + + # + # Tests + # + + def testTypingInOmnibox(self): + # Ctrl+A: Select all. + self._pywinauto_window.TypeKeys('^A') + self._WaitForSpeech('selecting about:blank') + + # Type three characters. + self._pywinauto_window.TypeKeys('xyz') + self._WaitForSpeech(['x', 'y', 'z']) + + # Arrow back over two characters. + self._pywinauto_window.TypeKeys('{LEFT}') + self._WaitForSpeech(['z', 'z', 'unselecting']) + + self._pywinauto_window.TypeKeys('{LEFT}') + self._WaitForSpeech('y') + + def testFocusToolbarButton(self): + # Alt+Shift+T. + self._pywinauto_window.TypeKeys('%+T') + self._WaitForSpeech('Reload button Reload this page') + + def testReadAllOnPageLoad(self): + # Ctrl+A: Select all + self._pywinauto_window.TypeKeys('^A') + self._WaitForSpeech('selecting about:blank') + + # Load data url. + self._pywinauto_window.TypeKeys('data:text/html,Hello<p>World.') + self._WaitForSpeech('dot') + self._pywinauto_window.TypeKeys('{ENTER}') + self._WaitForSpeech( + ['document', + 'Hello', + 'World.']) + +if __name__ == '__main__': + unittest.main() + |