Skip to content

Commit 5de71a3

Browse files
committed
Add system test
1 parent b9712c8 commit 5de71a3

File tree

5 files changed

+223
-8
lines changed

5 files changed

+223
-8
lines changed

.github/workflows/testAndPublish.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ jobs:
390390
fail-fast: false
391391
matrix:
392392
# To skip tests, and just run install, replace with [excluded_from_build]
393-
testSuite: [chrome, installer, startupShutdown, symbols]
393+
testSuite: [chrome, installer, startupShutdown, symbols, vscode]
394394
arch: ${{ fromJson(needs.matrix.outputs.supportedArchitectures) }}
395395
pythonVersion: ${{ fromJson(needs.matrix.outputs.supportedPythonVersions) }}
396396
env:
@@ -421,6 +421,15 @@ jobs:
421421
run: ci/scripts/installNVDA.ps1
422422
env:
423423
nvdaLauncherDir: ${{ steps.getLauncher.outputs.download-path }}
424+
- name: Download and Install VS Code
425+
if: ${{ matrix.testSuite == 'vscode' }}
426+
shell: pwsh
427+
run: |
428+
$ErrorActionPreference = 'Stop'
429+
$setup = Join-Path $env:RUNNER_TEMP 'VSCodeSetup.exe'
430+
Invoke-WebRequest -Uri 'https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-user' -OutFile $setup
431+
# Install silently, do not auto-run after install, avoid reboot prompts.
432+
& $setup /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /MERGETASKS=!runcode
424433
- name: Run system tests
425434
run: ci/scripts/tests/systemTests.ps1
426435
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

tests/system/libraries/VSCodeLib.py

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

tests/system/robot/vscodeTests.robot

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)