Skip to content

Commit 301f989

Browse files
lukischclaude
andcommitted
fix: Bugsweep #12 — A11y-Fixes + Mechanismus-Korrektur im Regressionstest
- settings_dialog.py: Browse-Buttons ohne accessibleName/accessibleDescription; _create_browse_button()-Factory eingeführt, Buttons als Instanzvariablen gespeichert - explorer_panel.py: Refresh- und Collapse-Button sowie Filter-Input ohne accessibleName/accessibleDescription — Screenreader-Nutzer ohne Kontext - test_settings_application.py: Regressionstest für Browse-Button-A11y - test_explorer_panel_accessibility.py: Regressionstest für Explorer-Toolbar-A11y - test_bugsweep_regressions.py: Mechanismus-Kommentar korrigiert (Race = stale Qt-Queue-Event, nicht Doppel-connect über separate QObjects) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 97023fc commit 301f989

5 files changed

Lines changed: 136 additions & 30 deletions

File tree

src/gui/dialogs/settings_dialog.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -203,22 +203,26 @@ def _create_build_tab(self) -> QWidget:
203203
self.pyinstaller_path = QLineEdit()
204204
self.pyinstaller_path.setPlaceholderText("(System-Standard)")
205205
pyinstaller_row.addWidget(self.pyinstaller_path)
206-
207-
browse_btn = QPushButton("...")
208-
browse_btn.setMaximumWidth(40)
209-
browse_btn.clicked.connect(lambda: self._browse_file(self.pyinstaller_path, "PyInstaller auswählen"))
210-
pyinstaller_row.addWidget(browse_btn)
206+
207+
self.pyinstaller_browse_btn = self._create_browse_button(
208+
accessible_name="PyInstaller-Datei auswählen",
209+
description="Öffnet die Dateiauswahl für den PyInstaller-Pfad.",
210+
callback=lambda: self._browse_file(self.pyinstaller_path, "PyInstaller auswählen"),
211+
)
212+
pyinstaller_row.addWidget(self.pyinstaller_browse_btn)
211213
pi_layout.addRow("PyInstaller:", pyinstaller_row)
212-
214+
213215
output_row = QHBoxLayout()
214216
self.output_dir = QLineEdit()
215217
self.output_dir.setText("dist")
216218
output_row.addWidget(self.output_dir)
217-
218-
browse_out_btn = QPushButton("...")
219-
browse_out_btn.setMaximumWidth(40)
220-
browse_out_btn.clicked.connect(lambda: self._browse_folder(self.output_dir, "Ausgabeverzeichnis"))
221-
output_row.addWidget(browse_out_btn)
219+
220+
self.output_dir_browse_btn = self._create_browse_button(
221+
accessible_name="Ausgabeverzeichnis auswählen",
222+
description="Öffnet die Ordnerauswahl für das Standard-Ausgabeverzeichnis.",
223+
callback=lambda: self._browse_folder(self.output_dir, "Ausgabeverzeichnis"),
224+
)
225+
output_row.addWidget(self.output_dir_browse_btn)
222226
pi_layout.addRow("Ausgabe:", output_row)
223227

224228
layout.addWidget(pi_group)
@@ -307,11 +311,13 @@ def _create_sync_tab(self) -> QWidget:
307311
self.backup_path = QLineEdit()
308312
self.backup_path.setPlaceholderText("Backup-Verzeichnis")
309313
backup_row.addWidget(self.backup_path)
310-
311-
browse_backup = QPushButton("...")
312-
browse_backup.setMaximumWidth(40)
313-
browse_backup.clicked.connect(lambda: self._browse_folder(self.backup_path, "Backup-Verzeichnis"))
314-
backup_row.addWidget(browse_backup)
314+
315+
self.backup_browse_btn = self._create_browse_button(
316+
accessible_name="Backup-Verzeichnis auswählen",
317+
description="Öffnet die Ordnerauswahl für den Backup-Pfad.",
318+
callback=lambda: self._browse_folder(self.backup_path, "Backup-Verzeichnis"),
319+
)
320+
backup_row.addWidget(self.backup_browse_btn)
315321
backup_layout.addRow("Backup-Pfad:", backup_row)
316322

317323
self.auto_backup = QCheckBox("Automatische Backups")
@@ -373,9 +379,19 @@ def _create_appearance_tab(self) -> QWidget:
373379

374380
layout.addWidget(accent_group)
375381
layout.addStretch()
376-
382+
377383
return widget
378-
384+
385+
def _create_browse_button(self, accessible_name: str, description: str, callback) -> QPushButton:
386+
"""Erzeugt einen kompakten Browse-Button mit sprechendem A11y-Kontext."""
387+
button = QPushButton("...")
388+
button.setMaximumWidth(40)
389+
button.setToolTip(accessible_name)
390+
button.setAccessibleName(accessible_name)
391+
button.setAccessibleDescription(description)
392+
button.clicked.connect(callback)
393+
return button
394+
379395
def _browse_file(self, line_edit: QLineEdit, title: str):
380396
"""Datei auswählen"""
381397
path, _ = QFileDialog.getOpenFileName(self, title)

src/gui/panels/explorer_panel.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,20 @@ def _setup_ui(self):
5151

5252
self.refresh_btn = QPushButton("🔄")
5353
self.refresh_btn.setToolTip("Aktualisieren")
54+
self.refresh_btn.setAccessibleName("Explorer aktualisieren")
55+
self.refresh_btn.setAccessibleDescription(
56+
"Lädt die aktuelle Projektstruktur im Explorer neu."
57+
)
5458
self.refresh_btn.setMaximumWidth(30)
5559
self.refresh_btn.clicked.connect(self._refresh)
5660
header.addWidget(self.refresh_btn)
57-
61+
5862
self.collapse_btn = QPushButton("⬆")
5963
self.collapse_btn.setToolTip("Alle zuklappen")
64+
self.collapse_btn.setAccessibleName("Explorer zuklappen")
65+
self.collapse_btn.setAccessibleDescription(
66+
"Klappt alle aktuell geöffneten Ordner im Explorer zu."
67+
)
6068
self.collapse_btn.setMaximumWidth(30)
6169
self.collapse_btn.clicked.connect(self._collapse_all)
6270
header.addWidget(self.collapse_btn)
@@ -66,6 +74,11 @@ def _setup_ui(self):
6674
# Filter
6775
self.filter_input = QLineEdit()
6876
self.filter_input.setPlaceholderText("🔍 Dateien filtern...")
77+
self.filter_input.setToolTip("Dateien im Explorer filtern")
78+
self.filter_input.setAccessibleName("Dateifilter")
79+
self.filter_input.setAccessibleDescription(
80+
"Filtert die im Explorer angezeigten Projektdateien."
81+
)
6982
self.filter_input.textChanged.connect(self._apply_filter)
7083
self.filter_input.setStyleSheet("""
7184
QLineEdit {

tests/test_bugsweep_regressions.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"""
22
Bugsweep Run #12 — Regressionstests (2026-06-20)
33
4-
Bug #12-1: ai_panel.py — Zombie-Signal-Verbindung beim Neustart des Workers.
5-
_start_request() ersetzte self._worker ohne den alten response_received-
6-
Signal-Slot zu trennen. Nach Worker-Beendigung (isRunning=False) liegt
7-
das Signal noch in der Qt-Event-Queue; ein neuer Worker + erneutes connect()
8-
erzeugt zwei aktive Verbindungen → _on_response wird doppelt aufgerufen
9-
(doppelte Chat-Nachricht, falscher Loading-State).
10-
Fix Teil 1: disconnect() vor dem Ersetzen des Workers (RuntimeError abfangen
11-
falls kein Slot mehr verbunden) — verhindert neue doppelte Connections.
12-
Fix Teil 2: Identitäts-Guard in _on_response (self.sender() is not self._worker
13-
→ return) — verwirft Events veralteter Worker, die bereits in der Qt-Event-Queue
14-
lagen, bevor disconnect() aufgerufen werden konnte.
4+
Bug #12-1: ai_panel.py — Stale-Response-Race beim Neustart des Workers.
5+
Jeder AIWorker besitzt ein eigenes response_received-Signal (separate QObjects,
6+
keine gemeinsame Connection). Das eigentliche Problem: Worker1 hat isRunning()=False
7+
zurückgegeben, aber sein emittiertes Signal liegt noch in der Qt-Event-Queue.
8+
Wird danach ein neuer Worker2 gestartet, kann _on_response Worker1's verspätete
9+
Antwort als Worker2-Antwort behandeln — falsche Chat-Nachricht, falscher Loading-State.
10+
Fix Teil 1: disconnect() vor dem Ersetzen des Workers (RuntimeError abfangen) —
11+
defensive Maßnahme, verkürzt das Race-Fenster aber eliminiert es nicht vollständig.
12+
Fix Teil 2 (der robuste Teil): Identitäts-Guard in _on_response
13+
(self.sender() is not self._worker → return) — verwirft Events veralteter Worker
14+
die noch in der Qt-Event-Queue lagen; dieser Guard schließt die Race vollständig.
1515
"""
1616
import os
1717
import re
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# -*- coding: utf-8 -*-
2+
"""Regressionstests für die Explorer-Barrierefreiheit."""
3+
4+
import importlib.util
5+
import os
6+
import sys
7+
import unittest
8+
from pathlib import Path
9+
10+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
11+
12+
from PySide6.QtWidgets import QApplication
13+
14+
EXPLORER_PANEL_PATH = Path(__file__).parent.parent / "src" / "gui" / "panels" / "explorer_panel.py"
15+
EXPLORER_PANEL_SPEC = importlib.util.spec_from_file_location(
16+
"devcenter_explorer_panel",
17+
EXPLORER_PANEL_PATH,
18+
)
19+
EXPLORER_PANEL_MODULE = importlib.util.module_from_spec(EXPLORER_PANEL_SPEC)
20+
assert EXPLORER_PANEL_SPEC.loader is not None
21+
EXPLORER_PANEL_SPEC.loader.exec_module(EXPLORER_PANEL_MODULE)
22+
ExplorerPanel = EXPLORER_PANEL_MODULE.ExplorerPanel
23+
24+
25+
class ExplorerPanelAccessibilityTests(unittest.TestCase):
26+
"""Sichert sprechende Namen für die kompakte Explorer-Toolbar."""
27+
28+
@classmethod
29+
def setUpClass(cls):
30+
cls.app = QApplication.instance() or QApplication([])
31+
32+
def test_compact_explorer_controls_expose_accessible_context(self):
33+
panel = ExplorerPanel()
34+
35+
self.assertEqual(panel.refresh_btn.toolTip(), "Aktualisieren")
36+
self.assertEqual(panel.refresh_btn.accessibleName(), "Explorer aktualisieren")
37+
self.assertIn("Projektstruktur", panel.refresh_btn.accessibleDescription())
38+
39+
self.assertEqual(panel.collapse_btn.toolTip(), "Alle zuklappen")
40+
self.assertEqual(panel.collapse_btn.accessibleName(), "Explorer zuklappen")
41+
self.assertIn("geöffneten Ordner", panel.collapse_btn.accessibleDescription())
42+
43+
self.assertEqual(panel.filter_input.toolTip(), "Dateien im Explorer filtern")
44+
self.assertEqual(panel.filter_input.accessibleName(), "Dateifilter")
45+
self.assertIn("Projektdateien", panel.filter_input.accessibleDescription())
46+
47+
48+
if __name__ == "__main__":
49+
unittest.main(verbosity=2)

tests/test_settings_application.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,34 @@ def test_settings_dialog_saves_highlight_current_line(self):
4545

4646
self.assertFalse(settings.get("editor.highlight_current_line"))
4747

48+
def test_settings_dialog_browse_buttons_expose_accessible_context(self):
49+
settings = self._temp_settings()
50+
dialog = SettingsDialog(settings)
51+
52+
browse_buttons = [
53+
(
54+
dialog.pyinstaller_browse_btn,
55+
"PyInstaller-Datei auswählen",
56+
"Öffnet die Dateiauswahl für den PyInstaller-Pfad.",
57+
),
58+
(
59+
dialog.output_dir_browse_btn,
60+
"Ausgabeverzeichnis auswählen",
61+
"Öffnet die Ordnerauswahl für das Standard-Ausgabeverzeichnis.",
62+
),
63+
(
64+
dialog.backup_browse_btn,
65+
"Backup-Verzeichnis auswählen",
66+
"Öffnet die Ordnerauswahl für den Backup-Pfad.",
67+
),
68+
]
69+
70+
for button, accessible_name, description in browse_buttons:
71+
self.assertEqual(button.text(), "...")
72+
self.assertEqual(button.toolTip(), accessible_name)
73+
self.assertEqual(button.accessibleName(), accessible_name)
74+
self.assertEqual(button.accessibleDescription(), description)
75+
4876
def test_main_window_applies_editor_settings_to_open_tabs(self):
4977
settings = self._temp_settings()
5078
settings.set("editor.font_family", "Courier New")

0 commit comments

Comments
 (0)