Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / accessibility / nvda / nvda_chrome_tests.py
blob6cbfd1b30ee0b18b98baadaa4cbae5dde8c029c9
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.
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
15 feedback in response.
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 """
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
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
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
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)
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)
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
91 os.environ['NVDA_SPECIFIC_PROCESS'] = str(self._chrome_proc.pid)
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
108 app = pywinauto.application.Application()
109 app.connect_(process = self._chrome_proc.pid)
110 self._pywinauto_window = app.top_window_()
112 try:
113 self._WaitForSpeech(['Address and search bar edit', 'about:blank'])
114 except:
115 self.tearDown()
117 def tearDown(self):
118 print
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()
125 else:
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()
132 else:
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):
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
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.
163 Args:
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()
173 while True:
174 lines = self._GetSpeechFromNvdaLogFile()
175 if (lines[-len(expected):] == expected):
176 break
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)
189 # Tests
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):
209 # Alt+Shift+T.
210 self._pywinauto_window.TypeKeys('%+T')
211 self._WaitForSpeech('Reload button Reload this page')
213 def testReadAllOnPageLoad(self):
214 # Ctrl+A: Select all
215 self._pywinauto_window.TypeKeys('^A')
216 self._WaitForSpeech('selecting about:blank')
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.'])
227 if __name__ == '__main__':
228 unittest.main()