Index: tools/accessibility/nvda/nvda_chrome_tests.py |
diff --git a/tools/accessibility/nvda/nvda_chrome_tests.py b/tools/accessibility/nvda/nvda_chrome_tests.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..61c0820c7916442972f2b83211657f33e8a941d8 |
--- /dev/null |
+++ b/tools/accessibility/nvda/nvda_chrome_tests.py |
@@ -0,0 +1,229 @@ |
+#!/usr/bin/env python |
+# 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.
|
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Semi-automated tests of Chrome with NVDA. |
+ |
+This file performs (semi) automated tests of Chrome with NVDA |
+(NonVisual Desktop Access), a popular open-source screen reader for |
+visually impaired users on Windows. It works by launching Chrome in a |
+subprocess, then launching NVDA in a special environment that simulates |
+speech rather than actually speaking, and ignores all events coming from |
+processes other than a specific Chrome process ID. Each test automates |
+Chrome with a series of actions and asserts that NVDA gives the expected |
+feedback in response. |
+ |
+The tests are "semi" automated in the sense that they are not intended to be |
+run from any developer machine, or on a buildbot - it requires setting up the |
+environment according to the instructions in README.txt, then running the |
+test script, then filing bugs for any potential failures. If the environment |
+is set up correctly, the actual tests should run automatically and unattended. |
+""" |
+ |
+import os |
+import pywinauto |
+import re |
+import shutil |
+import signal |
+import subprocess |
+import sys |
+import tempfile |
+import time |
+import unittest |
+ |
+CHROME_PROFILES_PATH = os.path.join(os.getcwd(), 'chrome_profiles') |
+CHROME_PATH = os.path.join(os.environ['USERPROFILE'], |
+ 'AppData', |
+ 'Local', |
+ 'Google', |
+ 'Chrome SxS', |
+ 'Application', |
+ 'chrome.exe') |
+NVDA_PATH = os.path.join(os.getcwd(), |
+ 'nvdaPortable', |
+ 'nvda_noUIAccess.exe') |
+NVDA_PROCTEST_PATH = os.path.join(os.getcwd(), |
+ 'nvda-proctest') |
+NVDA_LOGPATH = os.path.join(os.getcwd(), |
+ 'nvda_log.txt') |
+WAIT_FOR_SPEECH_TIMEOUT_SECS = 3.0 |
+ |
+class NvdaChromeTest(unittest.TestCase): |
+ @classmethod |
+ def setUpClass(cls): |
+ print 'user data: %s' % CHROME_PROFILES_PATH |
+ print 'chrome: %s' % CHROME_PATH |
+ print 'nvda: %s' % NVDA_PATH |
+ print 'nvda_proctest: %s' % NVDA_PROCTEST_PATH |
+ |
+ print 'Clearing user data directory and log file from previous runs' |
+ if os.access(NVDA_LOGPATH, os.F_OK): |
+ os.remove(NVDA_LOGPATH) |
+ if os.access(CHROME_PROFILES_PATH, os.F_OK): |
+ shutil.rmtree(CHROME_PROFILES_PATH) |
+ os.mkdir(CHROME_PROFILES_PATH, 0777) |
+ |
+ def handler(signum, frame): |
+ print 'Test interrupted, attempting to kill subprocesses.' |
+ self.tearDown() |
+ sys.exit() |
+ signal.signal(signal.SIGINT, handler) |
+ |
+ def setUp(self): |
+ user_data_dir = tempfile.mkdtemp(dir = CHROME_PROFILES_PATH) |
+ args = [CHROME_PATH, |
+ '--user-data-dir=%s' % user_data_dir, |
+ '--no-first-run', |
+ 'about:blank'] |
+ print ' '.join(args) |
+ self._chrome_proc = subprocess.Popen(args) |
+ self._chrome_proc.poll() |
+ if self._chrome_proc.returncode is None: |
+ print 'Chrome is running' |
+ else: |
+ print 'Chrome exited with code', self._chrome_proc.returncode |
+ sys.exit() |
+ print 'Chrome pid: %d' % self._chrome_proc.pid |
+ |
+ os.environ['NVDA_SPECIFIC_PROCESS'] = str(self._chrome_proc.pid) |
+ |
+ args = [NVDA_PATH, |
+ '-m', |
+ '-c', |
+ NVDA_PROCTEST_PATH, |
+ '-f', |
+ NVDA_LOGPATH] |
+ self._nvda_proc = subprocess.Popen(args) |
+ self._nvda_proc.poll() |
+ if self._nvda_proc.returncode is None: |
+ print 'NVDA is running' |
+ else: |
+ print 'NVDA exited with code', self._nvda_proc.returncode |
+ sys.exit() |
+ print 'NVDA pid: %d' % self._nvda_proc.pid |
+ |
+ app = pywinauto.application.Application() |
+ app.connect_(process = self._chrome_proc.pid) |
+ self._pywinauto_window = app.top_window_() |
+ |
+ try: |
+ self._WaitForSpeech(['Address and search bar edit', 'about:blank']) |
+ except: |
+ self.tearDown() |
+ |
+ def tearDown(self): |
+ print 'Shutting down' |
+ |
+ self._chrome_proc.poll() |
+ if self._chrome_proc.returncode is None: |
+ print 'Killing Chrome subprocess' |
+ self._chrome_proc.kill() |
+ else: |
+ print 'Chrome already died.' |
+ |
+ self._nvda_proc.poll() |
+ if self._nvda_proc.returncode is None: |
+ print 'Killing NVDA subprocess' |
+ self._nvda_proc.kill() |
+ else: |
+ print 'NVDA already died.' |
+ |
+ def _GetSpeechFromNvdaLogFile(self): |
+ """Return everything NVDA would have spoken as a list of strings. |
+ |
+ Parses lines like this from NVDA's log file: |
+ Speaking [LangChangeCommand ('en'), u'Google Chrome', u'window'] |
+ Speaking character u'slash' |
+ |
+ Returns a single list of strings like this: |
+ [u'Google Chrome', u'window', u'slash'] |
+ """ |
+ if not os.access(NVDA_LOGPATH, os.F_OK): |
+ return [] |
+ lines = open(NVDA_LOGPATH).readlines() |
+ regex = re.compile(r"u'((?:[^\'\\]|\\.)*)\'") |
+ result = [] |
+ for line in lines: |
+ for m in regex.finditer(line): |
+ speech_with_whitespace = m.group(1) |
+ speech_stripped = re.sub(r'\s+', ' ', speech_with_whitespace).strip() |
+ result.append(speech_stripped) |
+ 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
|
+ |
+ def _WaitForSpeech(self, expected): |
+ """Block until the last speech in NVDA's log file is the given string(s). |
+ |
+ Repeatedly parses the log file until the last speech line(s) in the |
+ log file match the given strings, or it times out. |
+ |
+ Args: |
+ expected: string or a list of string - only succeeds if these are the last |
+ strings spoken, in order. |
+ |
+ Returns when those strings are spoken, or throws an error if it times out |
+ waiting for those strings. |
+ """ |
+ if type(expected) is type(''): |
+ expected = [expected] |
+ start_time = time.time() |
+ while True: |
+ lines = self._GetSpeechFromNvdaLogFile() |
+ 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
|
+ break |
+ |
+ if time.time() - start_time >= WAIT_FOR_SPEECH_TIMEOUT_SECS: |
+ print '** Speech from NVDA so far:' |
+ for line in lines: |
+ print '"%s"' % line |
+ print '** Was waiting for:' |
+ for line in expected: |
+ print '"%s"' % line |
+ raise Exception('Timed out') |
+ time.sleep(0.1) |
+ |
+ # |
+ # Tests |
+ # |
+ |
+ def testTypingInOmnibox(self): |
+ # Ctrl+A: Select all. |
+ self._pywinauto_window.TypeKeys('^A') |
+ self._WaitForSpeech('selecting about:blank') |
+ |
+ # Type three characters. |
+ self._pywinauto_window.TypeKeys('xyz') |
+ self._WaitForSpeech(['x', 'y', 'z']) |
+ |
+ # Arrow back over two characters. |
+ self._pywinauto_window.TypeKeys('{LEFT}') |
+ self._WaitForSpeech(['z', 'z', 'unselecting']) |
+ |
+ self._pywinauto_window.TypeKeys('{LEFT}') |
+ self._WaitForSpeech('y') |
+ |
+ def testFocusToolbarButton(self): |
+ # Alt+Shift+T. |
+ self._pywinauto_window.TypeKeys('%+T') |
+ self._WaitForSpeech('Reload button Reload this page') |
+ |
+ def testReadAllOnPageLoad(self): |
+ # Ctrl+A: Select all |
+ self._pywinauto_window.TypeKeys('^A') |
+ self._WaitForSpeech('selecting about:blank') |
+ |
+ # Load data url. |
+ self._pywinauto_window.TypeKeys('data:text/html,Hello<p>World.') |
+ self._WaitForSpeech('dot') |
+ self._pywinauto_window.TypeKeys('{ENTER}') |
+ self._WaitForSpeech( |
+ ['document', |
+ 'Hello', |
+ 'World.']) |
+ |
+if __name__ == '__main__': |
+ unittest.main() |
+ |