OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Semi-automated tests of Chrome with NVDA. |
| 7 |
| 8 This file performs (semi) automated tests of Chrome with NVDA |
| 9 (NonVisual Desktop Access), a popular open-source screen reader for |
| 10 visually impaired users on Windows. It works by launching Chrome in a |
| 11 subprocess, then launching NVDA in a special environment that simulates |
| 12 speech rather than actually speaking, and ignores all events coming from |
| 13 processes other than a specific Chrome process ID. Each test automates |
| 14 Chrome with a series of actions and asserts that NVDA gives the expected |
| 15 feedback in response. |
| 16 |
| 17 The tests are "semi" automated in the sense that they are not intended to be |
| 18 run from any developer machine, or on a buildbot - it requires setting up the |
| 19 environment according to the instructions in README.txt, then running the |
| 20 test script, then filing bugs for any potential failures. If the environment |
| 21 is set up correctly, the actual tests should run automatically and unattended. |
| 22 """ |
| 23 |
| 24 import os |
| 25 import pywinauto |
| 26 import re |
| 27 import shutil |
| 28 import signal |
| 29 import subprocess |
| 30 import sys |
| 31 import tempfile |
| 32 import time |
| 33 import unittest |
| 34 |
| 35 CHROME_PROFILES_PATH = os.path.join(os.getcwd(), 'chrome_profiles') |
| 36 CHROME_PATH = os.path.join(os.environ['USERPROFILE'], |
| 37 'AppData', |
| 38 'Local', |
| 39 'Google', |
| 40 'Chrome SxS', |
| 41 'Application', |
| 42 'chrome.exe') |
| 43 NVDA_PATH = os.path.join(os.getcwd(), |
| 44 'nvdaPortable', |
| 45 'nvda_noUIAccess.exe') |
| 46 NVDA_PROCTEST_PATH = os.path.join(os.getcwd(), |
| 47 'nvda-proctest') |
| 48 NVDA_LOGPATH = os.path.join(os.getcwd(), |
| 49 'nvda_log.txt') |
| 50 WAIT_FOR_SPEECH_TIMEOUT_SECS = 3.0 |
| 51 |
| 52 class NvdaChromeTest(unittest.TestCase): |
| 53 @classmethod |
| 54 def setUpClass(cls): |
| 55 print 'user data: %s' % CHROME_PROFILES_PATH |
| 56 print 'chrome: %s' % CHROME_PATH |
| 57 print 'nvda: %s' % NVDA_PATH |
| 58 print 'nvda_proctest: %s' % NVDA_PROCTEST_PATH |
| 59 |
| 60 print |
| 61 print 'Clearing user data directory and log file from previous runs' |
| 62 if os.access(NVDA_LOGPATH, os.F_OK): |
| 63 os.remove(NVDA_LOGPATH) |
| 64 if os.access(CHROME_PROFILES_PATH, os.F_OK): |
| 65 shutil.rmtree(CHROME_PROFILES_PATH) |
| 66 os.mkdir(CHROME_PROFILES_PATH, 0777) |
| 67 |
| 68 def handler(signum, frame): |
| 69 print 'Test interrupted, attempting to kill subprocesses.' |
| 70 self.tearDown() |
| 71 sys.exit() |
| 72 signal.signal(signal.SIGINT, handler) |
| 73 |
| 74 def setUp(self): |
| 75 user_data_dir = tempfile.mkdtemp(dir = CHROME_PROFILES_PATH) |
| 76 args = [CHROME_PATH, |
| 77 '--user-data-dir=%s' % user_data_dir, |
| 78 '--no-first-run', |
| 79 'about:blank'] |
| 80 print |
| 81 print ' '.join(args) |
| 82 self._chrome_proc = subprocess.Popen(args) |
| 83 self._chrome_proc.poll() |
| 84 if self._chrome_proc.returncode is None: |
| 85 print 'Chrome is running' |
| 86 else: |
| 87 print 'Chrome exited with code', self._chrome_proc.returncode |
| 88 sys.exit() |
| 89 print 'Chrome pid: %d' % self._chrome_proc.pid |
| 90 |
| 91 os.environ['NVDA_SPECIFIC_PROCESS'] = str(self._chrome_proc.pid) |
| 92 |
| 93 args = [NVDA_PATH, |
| 94 '-m', |
| 95 '-c', |
| 96 NVDA_PROCTEST_PATH, |
| 97 '-f', |
| 98 NVDA_LOGPATH] |
| 99 self._nvda_proc = subprocess.Popen(args) |
| 100 self._nvda_proc.poll() |
| 101 if self._nvda_proc.returncode is None: |
| 102 print 'NVDA is running' |
| 103 else: |
| 104 print 'NVDA exited with code', self._nvda_proc.returncode |
| 105 sys.exit() |
| 106 print 'NVDA pid: %d' % self._nvda_proc.pid |
| 107 |
| 108 app = pywinauto.application.Application() |
| 109 app.connect_(process = self._chrome_proc.pid) |
| 110 self._pywinauto_window = app.top_window_() |
| 111 |
| 112 try: |
| 113 self._WaitForSpeech(['Address and search bar edit', 'about:blank']) |
| 114 except: |
| 115 self.tearDown() |
| 116 |
| 117 def tearDown(self): |
| 118 print |
| 119 print 'Shutting down' |
| 120 |
| 121 self._chrome_proc.poll() |
| 122 if self._chrome_proc.returncode is None: |
| 123 print 'Killing Chrome subprocess' |
| 124 self._chrome_proc.kill() |
| 125 else: |
| 126 print 'Chrome already died.' |
| 127 |
| 128 self._nvda_proc.poll() |
| 129 if self._nvda_proc.returncode is None: |
| 130 print 'Killing NVDA subprocess' |
| 131 self._nvda_proc.kill() |
| 132 else: |
| 133 print 'NVDA already died.' |
| 134 |
| 135 def _GetSpeechFromNvdaLogFile(self): |
| 136 """Return everything NVDA would have spoken as a list of strings. |
| 137 |
| 138 Parses lines like this from NVDA's log file: |
| 139 Speaking [LangChangeCommand ('en'), u'Google Chrome', u'window'] |
| 140 Speaking character u'slash' |
| 141 |
| 142 Returns a single list of strings like this: |
| 143 [u'Google Chrome', u'window', u'slash'] |
| 144 """ |
| 145 if not os.access(NVDA_LOGPATH, os.F_OK): |
| 146 return [] |
| 147 lines = open(NVDA_LOGPATH).readlines() |
| 148 regex = re.compile(r"u'((?:[^\'\\]|\\.)*)\'") |
| 149 result = [] |
| 150 for line in lines: |
| 151 for m in regex.finditer(line): |
| 152 speech_with_whitespace = m.group(1) |
| 153 speech_stripped = re.sub(r'\s+', ' ', speech_with_whitespace).strip() |
| 154 result.append(speech_stripped) |
| 155 return result |
| 156 |
| 157 def _WaitForSpeech(self, expected): |
| 158 """Block until the last speech in NVDA's log file is the given string(s). |
| 159 |
| 160 Repeatedly parses the log file until the last speech line(s) in the |
| 161 log file match the given strings, or it times out. |
| 162 |
| 163 Args: |
| 164 expected: string or a list of string - only succeeds if these are the last |
| 165 strings spoken, in order. |
| 166 |
| 167 Returns when those strings are spoken, or throws an error if it times out |
| 168 waiting for those strings. |
| 169 """ |
| 170 if type(expected) is type(''): |
| 171 expected = [expected] |
| 172 start_time = time.time() |
| 173 while True: |
| 174 lines = self._GetSpeechFromNvdaLogFile() |
| 175 if (lines[-len(expected):] == expected): |
| 176 break |
| 177 |
| 178 if time.time() - start_time >= WAIT_FOR_SPEECH_TIMEOUT_SECS: |
| 179 print '** Speech from NVDA so far:' |
| 180 for line in lines: |
| 181 print '"%s"' % line |
| 182 print '** Was waiting for:' |
| 183 for line in expected: |
| 184 print '"%s"' % line |
| 185 raise Exception('Timed out') |
| 186 time.sleep(0.1) |
| 187 |
| 188 # |
| 189 # Tests |
| 190 # |
| 191 |
| 192 def testTypingInOmnibox(self): |
| 193 # Ctrl+A: Select all. |
| 194 self._pywinauto_window.TypeKeys('^A') |
| 195 self._WaitForSpeech('selecting about:blank') |
| 196 |
| 197 # Type three characters. |
| 198 self._pywinauto_window.TypeKeys('xyz') |
| 199 self._WaitForSpeech(['x', 'y', 'z']) |
| 200 |
| 201 # Arrow back over two characters. |
| 202 self._pywinauto_window.TypeKeys('{LEFT}') |
| 203 self._WaitForSpeech(['z', 'z', 'unselecting']) |
| 204 |
| 205 self._pywinauto_window.TypeKeys('{LEFT}') |
| 206 self._WaitForSpeech('y') |
| 207 |
| 208 def testFocusToolbarButton(self): |
| 209 # Alt+Shift+T. |
| 210 self._pywinauto_window.TypeKeys('%+T') |
| 211 self._WaitForSpeech('Reload button Reload this page') |
| 212 |
| 213 def testReadAllOnPageLoad(self): |
| 214 # Ctrl+A: Select all |
| 215 self._pywinauto_window.TypeKeys('^A') |
| 216 self._WaitForSpeech('selecting about:blank') |
| 217 |
| 218 # Load data url. |
| 219 self._pywinauto_window.TypeKeys('data:text/html,Hello<p>World.') |
| 220 self._WaitForSpeech('dot') |
| 221 self._pywinauto_window.TypeKeys('{ENTER}') |
| 222 self._WaitForSpeech( |
| 223 ['document', |
| 224 'Hello', |
| 225 'World.']) |
| 226 |
| 227 if __name__ == '__main__': |
| 228 unittest.main() |
| 229 |
OLD | NEW |