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..6cbfd1b30ee0b18b98baadaa4cbae5dde8c029c9
|
--- /dev/null
|
+++ b/tools/accessibility/nvda/nvda_chrome_tests.py
|
@@ -0,0 +1,229 @@
|
+#!/usr/bin/env python
|
+# Copyright 2014 The Chromium Authors. All rights reserved.
|
+# 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
|
+ 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
|
+ 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
|
+ 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
|
+
|
+ 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):
|
+ 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()
|
+
|
|