Home | History | Annotate | Download | only in nvda
      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.
      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
    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):
    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 
    230