fix(ui): sections 17 and 18 were checked against the wrong text

check_design_parity read its checklist from build_audit_page.ANALYSIS. Eight
sections of the audit page are hand-written, and for those the generator's
text is NOT what the page says — so Settings and the Task editor were reported
as matching a design they had never been compared against. The Task editor was
in fact built backwards.

  * The checker now reads the "Thay đổi" bullets straight out of
    docs/ui-audit.html, and reports how many bullets on the page still have no
    probe (57 on the page, 32 probed) instead of implying full coverage.

  * Task editor: reverted from three step tabs to a left list + right panel,
    five rows matching the five real group boxes — which is what the page asks
    for, in as many words ("thay vì chia tab"), for consistency with Settings.

  * Settings now switches panels rather than scrolling, so both dialogs are
    navigated identically and neither is a long scroll any more.

  * Settings field presentation, as the page's second bullet asks: the six
    checkboxes became switches, and Language/Theme became segmented controls.
    ToggleSwitch subclasses QCheckBox and SegmentedControl exposes the slice
    of the QComboBox API this dialog uses, so no save/load path changed —
    verified by round-tripping language/theme/tray through _save().

All 22 task-editor fields and 14 settings fields verified present after the
move; 7 suites green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 12:49:52 +09:00
co-authored by Claude Opus 5
parent cb68f87b35
commit 62bec1f984
7 changed files with 365 additions and 132 deletions
+59 -71
View File
@@ -1,8 +1,9 @@
"""Check the section index added to Settings and the Task editor, offscreen.
"""Check the left-list + right-panel navigation in Settings and Task editor.
The index is navigation only, so the test is again a subtraction test: every
input control must still be there, and every index row must actually scroll to
its section. Both dialogs are built with .show(), never .exec() — exec() blocks.
Both dialogs are navigated the same way, as the audit page asks: a list of the
real group boxes on the left, one panel shown at a time on the right. So the
test is that picking a row swaps the panel, that the rows match the groups, and
— since this is a rearrangement — that no input control went missing.
Run: python tools/check_dialogs.py
"""
@@ -19,69 +20,55 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from capture_screens import _isolate_home, _load_fonts # noqa: E402
def controls(dlg):
from PySide6.QtWidgets import (QCheckBox, QComboBox, QLineEdit, QListWidget,
QPlainTextEdit, QPushButton, QSpinBox)
n = 0
for cls in (QComboBox, QLineEdit, QCheckBox, QSpinBox, QPlainTextEdit,
QPushButton, QListWidget):
n += len(dlg.findChildren(cls))
return n
# Every field each dialog must still offer after the move.
SETTINGS_FIELDS = [
"language_combo", "theme_combo", "tray_chk", "notify_chk",
"provider_combo", "prov_base", "prov_key", "prov_model",
"sandbox_pw_edit", "sandbox_unlock_btn", "sandbox_confirm",
"sandbox_block_network", "sec_enabled", "ai_check",
]
TASK_FIELDS = [
"title_edit", "desc_edit", "gen_desc_btn", "priority_combo", "status_combo",
"workspace_combo", "provider_combo", "model_combo", "skill_combo",
"sched_enabled", "run_at_edit", "files_list", "files_add_btn", "links_list",
"links_add_btn", "next_combo", "run_next_combo", "pass_output_chk",
"depends_list", "retry_spin", "timeout_spin", "approval_chk",
]
def check_tabs(name, dlg, app, expect):
"""The Task editor uses step TABS, not an index — same goal, different
control, so it gets its own check."""
def check(name, dlg, app, expect_rows, fields):
fails = []
print(f"--- {name} ---")
n_ctl = controls(dlg)
tabs = dlg.step_tabs
names = [tabs.tabText(i) for i in range(tabs.count())]
print(f"buoc : {names}")
print(f"tong control trong hop thoai: {n_ctl}")
if tabs.count() != expect:
fails.append(f"{name}: cho {expect} buoc, thay {tabs.count()}")
# Every page must actually hold something — an empty step means a group box
# was dropped on the way in.
for i in range(tabs.count()):
page = tabs.widget(i).widget()
kids = [w for w in page.findChildren(type(dlg)) ] or page.children()
n = len([c for c in page.findChildren(__import__(
"PySide6.QtWidgets", fromlist=["QWidget"]).QWidget) if c.parent() is page])
print(f" buoc {i + 1} co {n} khoi")
if n == 0:
fails.append(f"{name}: buoc {i + 1} rong")
return n_ctl, fails
def check(name, dlg, app, expect_rows):
fails = []
print(f"--- {name} ---")
n_ctl = controls(dlg)
idx = dlg.section_list
idx, stack = dlg.section_list, dlg.section_stack
rows = [idx.item(i).text() for i in range(idx.count())]
print(f"muc luc : {rows}")
print(f"tong control trong hop thoai: {n_ctl}")
print(f"muc : {rows}")
if len(rows) != expect_rows:
fails.append(f"{name}: cho {expect_rows} muc, thay {len(rows)}")
if any(not r or r.endswith(".g_basic") or r.startswith("settings.") for r in rows):
fails.append(f"{name}: co muc chua dich")
if idx.count() != stack.count():
fails.append(f"{name}: {idx.count()} muc nhung {stack.count()} panel")
# Each row must scroll somewhere different (and the last one furthest down).
from PySide6.QtWidgets import QScrollArea
scroll = dlg.findChildren(QScrollArea)[0]
positions = []
# Picking a row must swap the panel — and each panel must hold something.
swapped, empty = 0, []
for i in range(idx.count()):
idx.itemClicked.emit(idx.item(i))
app.processEvents()
positions.append(scroll.verticalScrollBar().value())
print(f"vi tri cuon theo tung muc : {positions}")
if positions != sorted(positions):
fails.append(f"{name}: muc luc nhay khong theo thu tu tren xuong")
if len(set(positions)) < 2:
fails.append(f"{name}: bam muc nao cung dung mot cho — muc luc khong chay")
return n_ctl, fails
idx.setCurrentRow(i)
for _ in range(3):
app.processEvents()
if stack.currentIndex() == i:
swapped += 1
page = stack.widget(i).widget()
if not page.findChildren(type(page)):
empty.append(rows[i])
print(f"chon muc -> doi panel : {swapped}/{idx.count()}")
if swapped != idx.count():
fails.append(f"{name}: chon muc khong doi panel")
if empty:
fails.append(f"{name}: panel rong {empty}")
missing = [f for f in fields if getattr(dlg, f, None) is None]
print(f"field con nguyen : {len(fields) - len(missing)}/{len(fields)}")
if missing:
fails.append(f"{name}: mat field {missing}")
return fails
def main() -> int:
@@ -101,31 +88,32 @@ def main() -> int:
set_language("vi")
ctx = AppContext(AppConfig.load())
fails = []
s = SettingsDialog(ctx)
s.resize(900, 600)
s.resize(900, 640)
s.show()
app.processEvents()
n_s, f = check("Cai dat", s, app, 5)
fails += f
fails += check("Cai dat", s, app, 5, SETTINGS_FIELDS)
t = TaskEditorDialog(ctx=ctx) # task=None → a new task, all fields present
t.resize(900, 600)
t = TaskEditorDialog(ctx=ctx)
t.resize(900, 640)
t.show()
app.processEvents()
n_t, f = check_tabs("Task editor", t, app, 3)
fails += f
fails += check("Task editor", t, app, 5, TASK_FIELDS)
# Translations for the two names that had to be invented for the index.
# Both dialogs must be navigated the SAME way — that is the stated point.
same = (type(s.section_list) is type(t.section_list)
and type(s.section_stack) is type(t.section_stack))
print()
print(f"hai hop thoai cung kieu dieu huong: {same}")
if not same:
fails.append("hai hop thoai dieu huong khac kieu")
for lang in ("vi", "en", "ja"):
set_language(lang)
print(f" {lang}: general={tr('settings.group.general')!r} "
f"basic={tr('schedtask.g_basic')!r}")
for key in ("settings.group.general", "schedtask.g_basic"):
if tr(key) == key:
fails.append(f"thieu ban dich {key} cho {lang}")
set_language("vi")
print()
@@ -134,7 +122,7 @@ def main() -> int:
for x in fails:
print(" " + x)
return 1
print("KET QUA: hai hop thoai co muc luc, khong mat control nao")
print("KET QUA: hai hop thoai dung danh sach trai + panel phai, khong mat field")
return 0