OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
David Tseng
2014/07/15 21:34:45
nit: I think the (c) isn't needed.
dmazzoni
2014/07/17 06:52:43
Done.
| |
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 | |
David Tseng
2014/07/15 21:34:45
Do you want/care to clear the speech log? (so that
dmazzoni
2014/07/17 06:52:43
I'm not sure how to do this easily without potenti
| |
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): | |
David Tseng
2014/07/15 21:34:45
What if some other speech slips in right after you
dmazzoni
2014/07/17 06:52:43
Currently these tests really want to assert that i
| |
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 |