File: nvda_chrome_tests.py

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (219 lines) | stat: -rwxr-xr-x 7,332 bytes parent folder | download | duplicates (10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors
# 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.
"""

from __future__ import print_function

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)

    tasklist = subprocess.Popen("tasklist", shell=True, stdout=subprocess.PIPE)
    tasklist_output = tasklist.communicate()[0].decode('utf8').split('\r\n')
    for task in tasklist_output:
      if (task.split(' ', 1)[0] == "nvda.exe"):
        print("nvda.exe is running!  Please kill it before running these tests")
        sys.exit()

    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, 0o777)

    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()
    self.last_nvda_log_line = 0

  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()[self.last_nvda_log_line:]
    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)
    self.last_nvda_log_line = len(lines) - 1
    return result

  def _ArrayInArray(self, lines, expected):
    positions = len(lines) - len(expected) + 1
    if (positions >= 0):
      # loop through the number of positions that the subset can hold
      for index in range(positions):
        if (lines[index:index + len(expected)] == expected):
          return True
    return False

  def _TestForSpeech(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.
    """
    if type(expected) is type(''):
      expected = [expected]
    start_time = time.time()
    while True:
      lines = self._GetSpeechFromNvdaLogFile()

      if self._ArrayInArray(lines, expected):
        return True

      if time.time() - start_time >= WAIT_FOR_SPEECH_TIMEOUT_SECS:
        self.fail("Test for expected speech failed.\n\nExpected:\n" +
                  str(expected) + ".\n\nActual:\n" + str(lines))
        return False
      time.sleep(0.1)

  #
  # Tests
  #

  def testTypingInOmnibox(self):
    # Ctrl+A: Select all.
    self._pywinauto_window.TypeKeys('^l')
    self._TestForSpeech(["main tool bar"])

    self._pywinauto_window.TypeKeys('xyz')
    self._pywinauto_window.TypeKeys('^a')
    self._TestForSpeech(["selected about:blank"])

  def testFocusToolbarButton(self):
    # Alt+Shift+T.
    self._pywinauto_window.TypeKeys('%+T')
    self._TestForSpeech('Reload button Reload this page')
    # this is flakey because sometimes this will be a stop button too.

  def testReadAllOnPageLoad(self):
    # Load data url.

    # Focus the url bar with control-L
    self._pywinauto_window.TypeKeys('^l')

    self._pywinauto_window.TypeKeys('^a')

    self._pywinauto_window.TypeKeys('data:text/html,Hello<p>World.')
    self._pywinauto_window.TypeKeys('{ENTER}')

    self._TestForSpeech(['Hello', 'World.'])


if __name__ == '__main__':
  unittest.main()