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 230