Skip to content

Commit 6d05096

Browse files
authored
Merge b407058 into 6fdabfe
2 parents 6fdabfe + b407058 commit 6d05096

File tree

12 files changed

+414
-121
lines changed

12 files changed

+414
-121
lines changed

appveyor/scripts/tests/beforeTests.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@ New-Item -ItemType directory -Path testOutput
22
New-Item -ItemType directory -Path testOutput\unit
33
New-Item -ItemType directory -Path testOutput\system
44
New-Item -ItemType directory -Path testOutput\lint
5+
# The first system test to run that requires chrome occasionally fails.
6+
# This has been replicated on developer machines after chrome updates,
7+
# however it is difficult to reproduce, and difficult to detect/recover-from
8+
# in an automated way.
9+
# It may be possible to handle this better within NVDA, however it seems to actually affect users
10+
# infrequently, and they seem to be able to recover by waiting a few seconds and de/refocus chrome.
11+
# In the mean time, the system tests failing delays development.
12+
#
13+
# Theory: Chrome is busy with post install tasks, so start chrome in the background ahead of the tests.
14+
cmd /c start /min chrome.exe
515
Set-AppveyorBuildVariable "testFailExitCode" 0

tests/system/libraries/ChromeLib.py

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
# imported methods start with underscore (_) so they don't get imported into robot files as keywords
11+
import datetime as _datetime
1112
from os.path import join as _pJoin
1213
import tempfile as _tempfile
1314
from typing import Optional as _Optional
@@ -18,7 +19,6 @@
1819
from SystemTestSpy.windows import (
1920
CloseWindow,
2021
GetWindowWithTitle,
21-
GetForegroundWindowTitle,
2222
Window,
2323
)
2424
import re
@@ -29,11 +29,13 @@
2929
from robot.libraries.Process import Process as _ProcessLib
3030
from AssertsLib import AssertsLib as _AssertsLib
3131
import NvdaLib as _NvdaLib
32+
import WindowsLib as _WindowsLib
3233

3334
builtIn: BuiltIn = BuiltIn()
3435
opSys: _OpSysLib = _getLib('OperatingSystem')
3536
process: _ProcessLib = _getLib('Process')
3637
assertsLib: _AssertsLib = _getLib('AssertsLib')
38+
windowsLib: _WindowsLib = _getLib('WindowsLib')
3739

3840

3941
# In Robot libraries, class name must match the name of the module. Use caps for both.
@@ -159,88 +161,88 @@ def _waitForStartMarker(self) -> bool:
159161
@return: False on failure
160162
"""
161163
spy = _NvdaLib.getSpyLib()
162-
spy.emulateKeyPress('alt+d') # focus the address bar, chrome shortcut
163164
spy.wait_for_speech_to_finish()
164-
addressSpeechIndex = spy.get_last_speech_index()
165+
expectedAddressBarSpeech = "Address and search bar"
166+
moveToAddressBarSpeech = _NvdaLib.getSpeechAfterKey('nvda+tab') # report current focus.
167+
if expectedAddressBarSpeech not in moveToAddressBarSpeech:
168+
moveToAddressBarSpeech = _NvdaLib.getSpeechAfterKey('alt+d') # focus the address bar, chrome shortcut
169+
if expectedAddressBarSpeech not in moveToAddressBarSpeech:
170+
builtIn.log(
171+
f"Didn't read '{expectedAddressBarSpeech}' after alt+d, instead got: {moveToAddressBarSpeech}"
172+
)
173+
return False
165174

166-
spy.emulateKeyPress('control+F6') # focus web content, chrome shortcut.
167-
spy.wait_for_speech_to_finish()
168-
afterControlF6Speech = spy.get_speech_at_index_until_now(addressSpeechIndex)
169-
if f"document\n{ChromeLib._beforeMarker}" not in afterControlF6Speech:
170-
builtIn.log(afterControlF6Speech, level="DEBUG")
175+
afterControlF6Speech = _NvdaLib.getSpeechAfterKey('control+F6') # focus web content, chrome shortcut.
176+
documentDescriptor = f"document\n{ChromeLib._beforeMarker}"
177+
if documentDescriptor not in afterControlF6Speech:
178+
builtIn.log(
179+
f"Didn't get '{documentDescriptor}' after moving to document, instead got: {afterControlF6Speech}"
180+
)
171181
return False
172182

173-
spy.emulateKeyPress('control+home') # ensure we start at the top of the document
174-
controlHomeSpeechIndex = spy.get_last_speech_index()
175-
spy.emulateKeyPress('numpad8') # report current line
183+
# ensure we start at the top of the document
184+
_NvdaLib.getSpeechAfterKey('control+home')
176185
spy.wait_for_speech_to_finish()
177-
afterNumPad8Speech = spy.get_speech_at_index_until_now(controlHomeSpeechIndex)
186+
187+
afterNumPad8Speech = _NvdaLib.getSpeechAfterKey('numpad8') # report current line
178188
if ChromeLib._beforeMarker not in afterNumPad8Speech:
179-
builtIn.log(afterNumPad8Speech, level="DEBUG")
189+
builtIn.log(
190+
f"Didn't get {ChromeLib._beforeMarker} after reporting the current line"
191+
f", instead got: {afterNumPad8Speech}"
192+
)
180193
return False
181194
return True
182195

183-
def toggleFocusChrome(self) -> None:
184-
"""Remove focus, then refocus chrome
185-
Attempt to work around NVDA missing focus / foreground events when chrome first opens.
186-
Forcing chrome to send another foreground event by focusing the desktop, then using alt+tab to return
187-
chrome to the foreground.
188-
@remarks If another application raises to the foreground after chrome, this approach won't resolve that
189-
situation.
190-
We don't have evidence that another application taking focus is a cause of failure yet.
191-
"""
192-
spy = _NvdaLib.getSpyLib()
193-
spy.emulateKeyPress('windows+d')
194-
_blockUntilConditionMet(
195-
giveUpAfterSeconds=5,
196-
getValue=GetForegroundWindowTitle,
197-
shouldStopEvaluator=lambda _title: self.chromeWindow.title != _title,
198-
errorMessage="Chrome didn't lose focus"
199-
)
200-
spy.emulateKeyPress('alt+tab')
201-
_blockUntilConditionMet(
202-
giveUpAfterSeconds=5,
203-
getValue=GetForegroundWindowTitle,
204-
shouldStopEvaluator=lambda _title: self.chromeWindow.title == _title,
205-
errorMessage="Chrome didn't gain focus"
196+
def canChromeTitleBeReported(self, chromeTitleSpeechPattern: re.Pattern) -> bool:
197+
speech = _NvdaLib.getSpeechAfterKey('NVDA+t')
198+
return bool(
199+
chromeTitleSpeechPattern.search(speech)
206200
)
207-
spy.wait_for_speech_to_finish()
208-
209-
def ensureChromeTitleCanBeReported(self, applicationTitle: str) -> int:
210-
spy = _NvdaLib.getSpyLib()
211-
afterFocusToggleIndex = spy.get_last_speech_index()
212-
spy.emulateKeyPress('NVDA+t')
213-
appTitleIndex = spy.wait_for_specific_speech(applicationTitle, afterIndex=afterFocusToggleIndex)
214-
return appTitleIndex
215201

216-
def prepareChrome(self, testCase: str, _doToggleFocus: bool = False) -> None:
202+
def prepareChrome(self, testCase: str, _alwaysDoToggleFocus: bool = False) -> None:
217203
"""
218204
Starts Chrome opening a file containing the HTML sample
219205
@param testCase - The HTML sample to test.
220-
@param _doToggleFocus - When True, Chrome will be intentionally de-focused and re-focused
206+
@param _alwaysDoToggleFocus - When True, Chrome will be intentionally de-focused and re-focused
221207
"""
208+
testCase = testCase + (
209+
"\n<!-- " # new line, start a HTML comment
210+
"Sample generation time, to ensure that the test case title is reproducibly unique purely from"
211+
" this test case string: \n"
212+
f"{ _datetime.datetime.now().isoformat()} "
213+
f" -->" # end HTML comment
214+
)
222215
spy = _NvdaLib.getSpyLib()
223216
_chromeLib: "ChromeLib" = _getLib('ChromeLib') # using the lib gives automatic 'keyword' logging.
224217
path = self._writeTestFile(testCase)
225218

226219
spy.wait_for_speech_to_finish()
227-
lastSpeechIndex = spy.get_last_speech_index()
228220
_chromeLib.start_chrome(path, testCase)
221+
windowsLib.logForegroundWindowTitle()
222+
229223
applicationTitle = ChromeLib.getUniqueTestCaseTitle(testCase)
224+
# application title will be something like "NVDA Browser Test Case (499078752)"
225+
# the parentheses could be escaped, instead we can just replace them with "match any char".
226+
patternSafeTitleString = applicationTitle.replace('(', '.').replace(')', '.')
227+
chromeTitleSpeechPattern = re.compile(patternSafeTitleString)
230228

231-
_chromeLib.ensureChromeTitleCanBeReported(applicationTitle)
232-
spy.wait_for_speech_to_finish()
229+
if (
230+
_alwaysDoToggleFocus # may work around focus/foreground event missed issues for tests.
231+
or not _chromeLib.canChromeTitleBeReported(chromeTitleSpeechPattern)
232+
):
233+
windowsLib.taskSwitchToItemMatching(targetWindowNamePattern=chromeTitleSpeechPattern)
234+
windowsLib.logForegroundWindowTitle()
233235

234-
if _doToggleFocus: # may work around focus/foreground event missed issues for tests.
235-
_chromeLib.toggleFocusChrome()
236-
spy.wait_for_speech_to_finish()
236+
if not _chromeLib.canChromeTitleBeReported(chromeTitleSpeechPattern):
237+
raise AssertionError("NVDA unable to report chrome title")
238+
spy.wait_for_speech_to_finish()
237239

238240
if not self._waitForStartMarker():
239241
builtIn.fail(
240242
"Unable to locate 'before sample' marker."
241243
" See NVDA log for full speech."
242244
)
243-
# Move to the loading status line, and wait fore it to become complete
245+
# Move to the loading status line, and wait for it to become complete
244246
# the page has fully loaded.
245247
spy.emulateKeyPress('downArrow')
246248
for x in range(10):

tests/system/libraries/NotepadLib.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
from SystemTestSpy.windows import (
1919
GetForegroundWindowTitle,
2020
GetVisibleWindowTitles,
21-
GetForegroundHwnd,
21+
GetForegroundHwnd as _getForegroundHwnd,
2222
GetWindowWithTitle,
23+
Window,
2324
)
2425
import re
2526
from robot.libraries.BuiltIn import BuiltIn
@@ -29,40 +30,77 @@
2930
from robot.libraries.Process import Process as _ProcessLib
3031
from AssertsLib import AssertsLib as _AssertsLib
3132
import NvdaLib as _NvdaLib
33+
import WindowsLib as _WindowsLib
3234

3335
builtIn: BuiltIn = BuiltIn()
3436
opSys: _OpSysLib = _getLib('OperatingSystem')
3537
process: _ProcessLib = _getLib('Process')
3638
assertsLib: _AssertsLib = _getLib('AssertsLib')
39+
windowsLib: _WindowsLib = _getLib('WindowsLib')
3740

3841

3942
# In Robot libraries, class name must match the name of the module. Use caps for both.
4043
class NotepadLib:
4144
_testFileStagingPath = _tempfile.mkdtemp()
4245
_testCaseTitle = "test"
4346

44-
def __init__(self):
45-
self.notepadHandle: _Optional[int] = None
47+
# Use class variables for state that should be tied to the RF library instance.
48+
# These variables will be available in the teardown
49+
notepadWindow: _Optional[Window] = None
50+
processRFHandleForStart: _Optional[int] = None
4651

4752
@staticmethod
4853
def _getTestCasePath(filename):
4954
return _pJoin(NotepadLib._testFileStagingPath, filename)
5055

5156
def exit_notepad(self):
5257
spy = _NvdaLib.getSpyLib()
53-
spy.emulateKeyPress('alt+f4')
54-
process.wait_for_process(self.notepadHandle, timeout="1 minute", on_timeout="continue")
58+
builtIn.log(
59+
# True is expected due to /wait argument.
60+
"Is Start process still running (True expected): "
61+
f"{process.is_process_running(NotepadLib.processRFHandleForStart)}"
62+
)
63+
64+
if _getForegroundHwnd() == NotepadLib.notepadWindow.hwndVal:
65+
builtIn.log("Test case in foreground, trying to close")
66+
spy.emulateKeyPress('alt+f4')
67+
process.wait_for_process(
68+
NotepadLib.processRFHandleForStart,
69+
timeout="10 seconds",
70+
on_timeout="continue"
71+
)
72+
else:
73+
builtIn.log("Test case not in foreground, can't close it.")
74+
builtIn.log(
75+
# False is expected, notepad should have allowed "Start" to exit.
76+
"Is Start process still running (False expected): "
77+
f"{process.is_process_running(NotepadLib.processRFHandleForStart)}"
78+
)
5579

56-
def start_notepad(self, filePath):
80+
def start_notepad(self, filePath: str, expectedTitlePattern: re.Pattern) -> Window:
5781
builtIn.log(f"starting notepad: {filePath}")
58-
self.notepadHandle = process.start_process(
59-
"start notepad"
82+
NotepadLib.processRFHandleForStart = process.start_process(
83+
"start" # windows utility to start a process
84+
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/start
85+
" /wait" # Starts an application and waits for it to end.
86+
" notepad"
6087
f' "{filePath}"',
6188
shell=True,
6289
alias='NotepadAlias',
6390
)
64-
process.process_should_be_running(self.notepadHandle)
65-
return self.notepadHandle
91+
process.process_should_be_running(NotepadLib.processRFHandleForStart)
92+
93+
success, NotepadLib.notepadWindow = _blockUntilConditionMet(
94+
getValue=lambda: GetWindowWithTitle(expectedTitlePattern, lambda message: builtIn.log(message, "DEBUG")),
95+
giveUpAfterSeconds=3,
96+
shouldStopEvaluator=lambda _window: _window is not None,
97+
intervalBetweenSeconds=0.5,
98+
errorMessage="Unable to get notepad window"
99+
)
100+
101+
if not success or NotepadLib.notepadWindow is None:
102+
builtIn.fatal_error("Unable to get notepad window")
103+
return NotepadLib.notepadWindow
66104

67105
@staticmethod
68106
def getUniqueTestCaseTitle(testCase: str) -> str:
@@ -91,7 +129,7 @@ def _isNotepadInForeground() -> bool:
91129
notepadWindow = GetWindowWithTitle(startsWithTestCaseTitle, builtIn.log)
92130
if notepadWindow is None:
93131
return False
94-
return notepadWindow.hwndVal == GetForegroundHwnd()
132+
return notepadWindow.hwndVal == _getForegroundHwnd()
95133

96134
success, _success = _blockUntilConditionMet(
97135
getValue=_isNotepadInForeground,
@@ -111,6 +149,12 @@ def _isNotepadInForeground() -> bool:
111149
f"{windowInformation}"
112150
)
113151

152+
def canNotepadTitleBeReported(self, notepadTitleSpeechPattern: re.Pattern) -> bool:
153+
titleSpeech = _NvdaLib.getSpeechAfterKey('NVDA+t')
154+
return bool(
155+
notepadTitleSpeechPattern.search(titleSpeech)
156+
)
157+
114158
def prepareNotepad(self, testCase: str) -> None:
115159
"""
116160
Starts Notepad opening a file containing the plaintext sample.
@@ -123,8 +167,19 @@ def prepareNotepad(self, testCase: str) -> None:
123167
path = self._writeTestFile(testCase)
124168

125169
spy.wait_for_speech_to_finish()
126-
self.start_notepad(path)
170+
self.start_notepad(path, expectedTitlePattern=self.getUniqueTestCaseTitleRegex(testCase))
171+
172+
windowsLib.logForegroundWindowTitle()
173+
testCaseNotepadTitleSpeech = re.compile(
174+
# Unlike getUniqueTestCaseTitleRegex, this speech does not have to be at the start of the string.
175+
f"{NotepadLib._testCaseTitle} \\({abs(hash(testCase))}\\)"
176+
)
177+
if not self.canNotepadTitleBeReported(notepadTitleSpeechPattern=testCaseNotepadTitleSpeech):
178+
builtIn.log("Trying to switch to notepad Window")
179+
windowsLib.taskSwitchToItemMatching(targetWindowNamePattern=testCaseNotepadTitleSpeech)
180+
windowsLib.logForegroundWindowTitle()
181+
127182
self._waitForNotepadFocus(NotepadLib.getUniqueTestCaseTitleRegex(testCase))
183+
windowsLib.logForegroundWindowTitle()
128184
# Move to the start of file
129-
spy.emulateKeyPress('home')
130-
spy.wait_for_speech_to_finish()
185+
_NvdaLib.getSpeechAfterKey('home')

tests/system/libraries/SystemTestSpy/blockUntilConditionMet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def _blockUntilConditionMet(
2626
"""Repeatedly tries to get a value up until a time limit expires. Tries are separated by
2727
a time interval. The call will block until shouldStopEvaluator returns True when given the value,
2828
the default evaluator just returns the value converted to a boolean.
29-
@param errorMessage Use 'None' to suppress the exception.
30-
@return A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
29+
@param errorMessage: Use 'None' to suppress the exception.
30+
@returns: Tuple, (True, value) if evaluator condition is met, otherwise (False, None)
3131
@raises RuntimeError if the time limit expires and an errorMessage is given.
3232
"""
3333
assert callable(getValue)

0 commit comments

Comments
 (0)