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.
6 """Semi-automated tests of Chrome with NVDA.
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
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.
35 CHROME_PROFILES_PATH
= os
.path
.join(os
.getcwd(), 'chrome_profiles')
36 CHROME_PATH
= os
.path
.join(os
.environ
['USERPROFILE'],
43 NVDA_PATH
= os
.path
.join(os
.getcwd(),
45 'nvda_noUIAccess.exe')
46 NVDA_PROCTEST_PATH
= os
.path
.join(os
.getcwd(),
48 NVDA_LOGPATH
= os
.path
.join(os
.getcwd(),
50 WAIT_FOR_SPEECH_TIMEOUT_SECS
= 3.0
52 class NvdaChromeTest(unittest
.TestCase
):
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
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)
68 def handler(signum
, frame
):
69 print 'Test interrupted, attempting to kill subprocesses.'
72 signal
.signal(signal
.SIGINT
, handler
)
75 user_data_dir
= tempfile
.mkdtemp(dir = CHROME_PROFILES_PATH
)
77 '--user-data-dir=%s' % user_data_dir
,
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'
87 print 'Chrome exited with code', self
._chrome
_proc
.returncode
89 print 'Chrome pid: %d' % self
._chrome
_proc
.pid
91 os
.environ
['NVDA_SPECIFIC_PROCESS'] = str(self
._chrome
_proc
.pid
)
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'
104 print 'NVDA exited with code', self
._nvda
_proc
.returncode
106 print 'NVDA pid: %d' % self
._nvda
_proc
.pid
108 app
= pywinauto
.application
.Application()
109 app
.connect_(process
= self
._chrome
_proc
.pid
)
110 self
._pywinauto
_window
= app
.top_window_()
113 self
._WaitForSpeech
(['Address and search bar edit', 'about:blank'])
119 print 'Shutting down'
121 self
._chrome
_proc
.poll()
122 if self
._chrome
_proc
.returncode
is None:
123 print 'Killing Chrome subprocess'
124 self
._chrome
_proc
.kill()
126 print 'Chrome already died.'
128 self
._nvda
_proc
.poll()
129 if self
._nvda
_proc
.returncode
is None:
130 print 'Killing NVDA subprocess'
131 self
._nvda
_proc
.kill()
133 print 'NVDA already died.'
135 def _GetSpeechFromNvdaLogFile(self
):
136 """Return everything NVDA would have spoken as a list of strings.
138 Parses lines like this from NVDA's log file:
139 Speaking [LangChangeCommand ('en'), u'Google Chrome', u'window']
140 Speaking character u'slash'
142 Returns a single list of strings like this:
143 [u'Google Chrome', u'window', u'slash']
145 if not os
.access(NVDA_LOGPATH
, os
.F_OK
):
147 lines
= open(NVDA_LOGPATH
).readlines()
148 regex
= re
.compile(r
"u'((?:[^\'\\]|\\.)*)\'")
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
)
157 def _WaitForSpeech(self
, expected
):
158 """Block until the last speech in NVDA's log file is the given string(s).
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.
164 expected: string or a list of string - only succeeds if these are the last
165 strings spoken, in order.
167 Returns when those strings are spoken, or throws an error if it times out
168 waiting for those strings.
170 if type(expected
) is type(''):
171 expected
= [expected
]
172 start_time
= time
.time()
174 lines
= self
._GetSpeechFromNvdaLogFile
()
175 if (lines
[-len(expected
):] == expected
):
178 if time
.time() - start_time
>= WAIT_FOR_SPEECH_TIMEOUT_SECS
:
179 print '** Speech from NVDA so far:'
182 print '** Was waiting for:'
183 for line
in expected
:
185 raise Exception('Timed out')
192 def testTypingInOmnibox(self
):
193 # Ctrl+A: Select all.
194 self
._pywinauto
_window
.TypeKeys('^A')
195 self
._WaitForSpeech
('selecting about:blank')
197 # Type three characters.
198 self
._pywinauto
_window
.TypeKeys('xyz')
199 self
._WaitForSpeech
(['x', 'y', 'z'])
201 # Arrow back over two characters.
202 self
._pywinauto
_window
.TypeKeys('{LEFT}')
203 self
._WaitForSpeech
(['z', 'z', 'unselecting'])
205 self
._pywinauto
_window
.TypeKeys('{LEFT}')
206 self
._WaitForSpeech
('y')
208 def testFocusToolbarButton(self
):
210 self
._pywinauto
_window
.TypeKeys('%+T')
211 self
._WaitForSpeech
('Reload button Reload this page')
213 def testReadAllOnPageLoad(self
):
215 self
._pywinauto
_window
.TypeKeys('^A')
216 self
._WaitForSpeech
('selecting about:blank')
219 self
._pywinauto
_window
.TypeKeys('data:text/html,Hello<p>World.')
220 self
._WaitForSpeech
('dot')
221 self
._pywinauto
_window
.TypeKeys('{ENTER}')
227 if __name__
== '__main__':