Skip to content

Commit 026081f

Browse files
codeofduskseanbudd
andcommitted
Add system tests
Co-authored-by: Sean Budd <[email protected]>
1 parent df1f83a commit 026081f

File tree

5 files changed

+252
-8
lines changed

5 files changed

+252
-8
lines changed

.github/workflows/testAndPublish.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ jobs:
393393
fail-fast: false
394394
matrix:
395395
# To skip tests, and just run install, replace with [excluded_from_build]
396-
testSuite: [chrome, installer, startupShutdown, symbols]
396+
testSuite: [chrome, installer, startupShutdown, symbols, vscode]
397397
runner: ${{ fromJson(needs.matrix.outputs.supportedRunners) }}
398398
arch: ${{ fromJson(needs.matrix.outputs.supportedArchitectures) }}
399399
pythonVersion: ${{ fromJson(needs.matrix.outputs.supportedPythonVersions) }}
@@ -430,6 +430,9 @@ jobs:
430430
run: ci/scripts/installNVDA.ps1
431431
env:
432432
nvdaLauncherDir: ${{ steps.getLauncher.outputs.download-path }}
433+
- name: Download and Install Visual Studio Code
434+
if: ${{ matrix.testSuite == 'vscode' }}
435+
run: choco install -y vscode
433436
- name: Run system tests
434437
run: ci/scripts/tests/systemTests.ps1
435438
env:

tests/system/libraries/SystemTestSpy/windows.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2021-2025 NV Access Limited, Łukasz Golonka
2+
# Copyright (C) 2021-2025 NV Access Limited, Łukasz Golonka, Bill Dengler
33
# This file may be used under the terms of the GNU General Public License, version 2 or later.
44
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
55

@@ -75,14 +75,14 @@ def _GetVisibleWindows() -> list[Window]:
7575

7676
def CloseWindow(window: Window) -> bool:
7777
"""
78-
@return: True if the window exists and the message was sent.
78+
Request that the target window close gracefully (WM_CLOSE).
79+
:return: True if the window exists and the message was posted.
7980
"""
8081
if windowWithHandleExists(window.hwndVal):
81-
return bool(
82-
windll.user32.CloseWindow(
83-
window.hwndVal,
84-
),
85-
)
82+
# We do not use user32.CloseWindow, which minimizes (rather than
83+
# closing) the window.
84+
WM_CLOSE: int = 0x0010
85+
return bool(windll.user32.PostMessageW(window.hwndVal, WM_CLOSE, 0, 0))
8686
return False
8787

8888

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2025 Bill Dengler
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
6+
import os as _os
7+
import re as _re
8+
import shutil as _shutil
9+
import tempfile as _tempfile
10+
import json as _json
11+
from robot.libraries.BuiltIn import BuiltIn as _BuiltIn
12+
from SystemTestSpy import _blockUntilConditionMet, _getLib
13+
from SystemTestSpy.windows import (
14+
CloseWindow as _CloseWindow,
15+
GetWindowWithTitle as _GetWindowWithTitle,
16+
SetForegroundWindow as _SetForegroundWindow,
17+
Window as _Window,
18+
windowWithHandleExists as _windowWithHandleExists,
19+
)
20+
import NvdaLib as _NvdaLib
21+
22+
from robot.libraries.Process import Process as _ProcessLib
23+
import WindowsLib as _WindowsLib
24+
25+
_builtIn: _BuiltIn = _BuiltIn()
26+
_process: _ProcessLib = _getLib("Process")
27+
_windowsLib: _WindowsLib = _getLib("WindowsLib")
28+
29+
_LAUNCHER_PATHS = [
30+
_os.path.expandvars(r"%LOCALAPPDATA%\Programs\Microsoft VS Code\Code.exe"),
31+
_os.path.expandvars(r"C:\Program Files\Microsoft VS Code\Code.exe"),
32+
_os.path.expandvars(r"%ProgramFiles%\Microsoft VS Code\Code.exe"),
33+
_os.path.expandvars(r"%ProgramFiles(x86)%\Microsoft VS Code\Code.exe"),
34+
]
35+
_LAUNCHER_CMDS = [
36+
_os.path.expandvars(r"%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd"),
37+
"code",
38+
]
39+
_WINDOW_TITLE_PATTERN = _re.compile(r"Visual Studio Code", _re.IGNORECASE)
40+
41+
42+
class VSCodeLib:
43+
_testTempDir: str | None = _tempfile.mkdtemp(prefix="nvdatest")
44+
_codeWindow: _Window | None = None
45+
_processRFHandleForStart: int | None = None
46+
47+
@staticmethod
48+
def _findCodeLauncher() -> str:
49+
for p in _LAUNCHER_PATHS:
50+
if _os.path.isfile(p):
51+
return f'"{p}"'
52+
for candidate in _LAUNCHER_CMDS:
53+
resolved = _shutil.which(candidate)
54+
if resolved:
55+
return f'"{resolved}"'
56+
raise AssertionError("Visual Studio Code launcher not found. Is it installed?")
57+
58+
def start_vscode(self) -> _Window:
59+
launcher = self._findCodeLauncher()
60+
userDataDir = _os.path.join(self._testTempDir, "vscodeUserData")
61+
_os.makedirs(userDataDir, exist_ok=True)
62+
63+
env = dict(_os.environ)
64+
env["VSCODE_DISABLE_UPDATES"] = "1"
65+
env["VSCODE_TELEMETRY_LEVEL"] = "off"
66+
67+
# Prepare user settings to suppress welcome/startup screen
68+
userSettingsDir = _os.path.join(userDataDir, "User")
69+
_os.makedirs(userSettingsDir, exist_ok=True)
70+
settingsPath = _os.path.join(userSettingsDir, "settings.json")
71+
try:
72+
if not _os.path.isfile(settingsPath):
73+
with open(settingsPath, "w", encoding="utf-8") as cam:
74+
_json.dump(
75+
{
76+
"editor.accessibilitySupport": "on",
77+
"workbench.startupEditor": "none",
78+
"update.showReleaseNotes": False,
79+
"workbench.tips.enabled": False,
80+
"update.mode": "none",
81+
"telemetry.telemetryLevel": "off",
82+
},
83+
cam,
84+
ensure_ascii=False,
85+
indent=2,
86+
)
87+
except Exception as e:
88+
_builtIn.log(
89+
f"Failed to prepare Visual Studio Code settings to skip welcome screen: {e!r}",
90+
level="WARN",
91+
)
92+
93+
cmd = (
94+
f'start "" /wait {launcher} '
95+
f'--user-data-dir "{userDataDir}" '
96+
f"--disable-gpu "
97+
f"--disable-extensions "
98+
f"--disable-workspace-trust "
99+
f"--skip-add-to-recently-opened "
100+
f"-n "
101+
f"--wait"
102+
)
103+
_builtIn.log(f"Starting Visual Studio Code: {cmd}", level="DEBUG")
104+
VSCodeLib._processRFHandleForStart = _process.start_process(
105+
cmd,
106+
shell=True,
107+
alias="vscodeStartAlias",
108+
env=env,
109+
)
110+
111+
success, VSCodeLib._codeWindow = _blockUntilConditionMet(
112+
getValue=lambda: _GetWindowWithTitle(
113+
_WINDOW_TITLE_PATTERN,
114+
lambda m: _builtIn.log(m, level="DEBUG"),
115+
),
116+
giveUpAfterSeconds=15.0,
117+
shouldStopEvaluator=lambda w: w is not None,
118+
intervalBetweenSeconds=0.5,
119+
errorMessage="Unable to get Visual Studio Code window",
120+
)
121+
if not success or VSCodeLib._codeWindow is None:
122+
_builtIn.fatal_error("Unable to get Visual Studio Code window")
123+
124+
_windowsLib.taskSwitchToItemMatching(targetWindowNamePattern=_WINDOW_TITLE_PATTERN)
125+
_windowsLib.logForegroundWindowTitle()
126+
_NvdaLib.getSpyLib().wait_for_speech_to_finish()
127+
return VSCodeLib._codeWindow
128+
129+
def close_vscode(self):
130+
window = VSCodeLib._codeWindow or _GetWindowWithTitle(
131+
_WINDOW_TITLE_PATTERN,
132+
lambda m: _builtIn.log(m, level="DEBUG"),
133+
)
134+
if not window:
135+
_builtIn.log("No Visual Studio Code window handle to close.", level="WARN")
136+
return
137+
138+
_CloseWindow(window)
139+
success, _ = _blockUntilConditionMet(
140+
getValue=lambda: not _windowWithHandleExists(window.hwndVal),
141+
giveUpAfterSeconds=10.0,
142+
shouldStopEvaluator=lambda x: bool(x),
143+
intervalBetweenSeconds=0.5,
144+
)
145+
146+
if not success:
147+
_builtIn.log("Window still present after WM_CLOSE, trying Alt+F4.", level="WARN")
148+
try:
149+
_SetForegroundWindow(window, lambda m: _builtIn.log(m, level="DEBUG"))
150+
except Exception:
151+
pass
152+
try:
153+
_NvdaLib.getSpyLib().emulateKeyPress("alt+F4")
154+
except Exception:
155+
pass
156+
# Now wait again, failing the test if the window is still present.
157+
_blockUntilConditionMet(
158+
getValue=lambda: not _windowWithHandleExists(window.hwndVal),
159+
giveUpAfterSeconds=15.0,
160+
shouldStopEvaluator=lambda x: bool(x),
161+
intervalBetweenSeconds=0.5,
162+
errorMessage="Visual Studio Code window did not close after sending close events.",
163+
)
164+
165+
if VSCodeLib._processRFHandleForStart:
166+
_process.wait_for_process(
167+
VSCodeLib._processRFHandleForStart,
168+
timeout="10 seconds",
169+
on_timeout="continue",
170+
)
171+
VSCodeLib._processRFHandleForStart = None
172+
173+
try:
174+
if VSCodeLib._testTempDir and _os.path.isdir(VSCodeLib._testTempDir):
175+
_builtIn.log(
176+
f"Cleaning up Visual Studio Code temp dir: {VSCodeLib._testTempDir}",
177+
level="DEBUG",
178+
)
179+
_shutil.rmtree(VSCodeLib._testTempDir)
180+
VSCodeLib._testTempDir = None
181+
except Exception as e:
182+
_builtIn.log(
183+
f"Failed to remove temp dir '{VSCodeLib._testTempDir}': {e!r}",
184+
level="WARN",
185+
)
186+
VSCodeLib._codeWindow = None

tests/system/robot/vscodeTests.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2025 Bill Dengler
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
6+
7+
from robot.libraries.BuiltIn import BuiltIn
8+
from SystemTestSpy import _getLib
9+
import NvdaLib as _NvdaLib
10+
11+
_builtIn: BuiltIn = BuiltIn()
12+
_vscode = _getLib("VSCodeLib")
13+
14+
15+
def vs_code_status_line_is_available():
16+
"""Start Visual Studio Code and ensure NVDA+end does not report "no status line found"."""
17+
_vscode.start_vscode()
18+
speech = _NvdaLib.getSpeechAfterKey("NVDA+end")
19+
_builtIn.should_not_contain(speech, "no status line found")
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2025 Bill Dengler
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
*** Settings ***
6+
Documentation Visual Studio Code tests
7+
Force Tags NVDA smoke test vscode
8+
Library NvdaLib.py
9+
Library WindowsLib.py
10+
Library ScreenCapLibrary
11+
Library VSCodeLib.py
12+
Library vscodeTests.py
13+
14+
Test Setup default setup
15+
Test Teardown default teardown
16+
17+
*** Keywords ***
18+
default setup
19+
logForegroundWindowTitle
20+
start NVDA standard-dontShowWelcomeDialog.ini
21+
logForegroundWindowTitle
22+
enable_verbose_debug_logging_if_requested
23+
24+
default teardown
25+
logForegroundWindowTitle
26+
${screenshotName}= create_preserved_test_output_filename failedTest.png
27+
Run Keyword If Test Failed Take Screenshot ${screenshotName}
28+
dump_speech_to_log
29+
dump_braille_to_log
30+
close vscode
31+
quit NVDA
32+
33+
*** Test Cases ***
34+
VS Code status line is available
35+
[Documentation] Start Visual Studio Code and ensure NVDA+end does not report "no status line found".
36+
vs_code_status_line_is_available

0 commit comments

Comments
 (0)