from __future__ import annotations """Windows 온보딩 앱 디자인 RED. 근거: - designpaca: 상태는 색 단독이 아니라 텍스트·기호·다음 행동으로 전달한다. - Microsoft Windows/Fluent guidance: 설정 화면은 단순 그룹, 상태와 다음 행동, 접근 가능한 컨트롤을 제공한다. - 사용자 제약: 비개발자가 더블클릭 후 막힌 항목을 직접 복구해야 한다. """ import time from typing import Iterator import pytest from dmf_crawler.models import CheckResult, Severity def _walk_widgets(widget: object) -> Iterator[object]: children = getattr(widget, "winfo_children", lambda: [])() for child in children: yield child yield from _walk_widgets(child) def _all_texts(widget: object) -> list[str]: texts: list[str] = [] for child in _walk_widgets(widget): cget = getattr(child, "cget", None) if cget is None: continue try: text = str(cget("text") or "") except Exception: # noqa: BLE001 - Tk 위젯 종류별 cget 차이 방어 continue if text: texts.append(text) return texts def _new_onboarding_app(gui_app: object, *, mode: str = "inspect") -> object: """Windows Python 3.14 의 Tk 초기화가 연속 root 생성에서 흔들리면 짧게 재시도한다.""" import tkinter as tk last: tk.TclError | None = None for _ in range(3): try: return gui_app.OnboardingApp(mode=mode) except tk.TclError as exc: last = exc time.sleep(0.08) assert last is not None raise last def _result( key: str, title: str, *, ok: bool, severity: Severity, detail: str, fix_hint: str = "다음 행동 안내", fix_action: str | None = "enter_api_key", ) -> CheckResult: return CheckResult( key=key, title=title, ok=ok, detail=detail, fix_hint=fix_hint, fix_action=fix_action, severity=severity, ) def test_onboarding_app_groups_checks_into_status_summary_and_task_sections(monkeypatch: pytest.MonkeyPatch) -> None: """12개 체크를 평평하게 나열하면 비개발자는 무엇부터 고칠지 모른다.""" tkinter = pytest.importorskip("tkinter") from dmf_crawler.gui import app as gui_app monkeypatch.setattr(gui_app.OnboardingApp, "refresh", lambda self: None) win = _new_onboarding_app(gui_app, mode="inspect") try: win._render( [ _result( "api_key_present", "공공데이터포털 인증키", ok=False, severity=Severity.CRITICAL, detail="키가 없어 오늘 수집을 시작할 수 없습니다.", fix_hint="키 입력 버튼을 누르세요.", fix_action="enter_api_key", ), _result( "agy_auth", "AI 요약 로그인(선택)", ok=False, severity=Severity.WARN, detail="AI 요약을 켠 경우에만 로그인하면 됩니다.", fix_hint="기본 수집과 xlsx 생성은 계속 가능합니다.", fix_action="login_agy", ), _result( "database", "자료 보관소", ok=True, severity=Severity.CRITICAL, detail="DB가 준비됐습니다.", fix_action=None, ), _result( "recent_runs", "최근 실행 상태", ok=True, severity=Severity.WARN, detail="최근 기준선 리포트가 있습니다.", fix_action=None, ), ] ) win.update_idletasks() text_blob = "\n".join(_all_texts(win)) assert "필수 1" in text_blob, "필수 실패 개수는 상단에서 바로 보여야 한다" assert "권장 1" in text_blob, "선택/권장 실패 개수도 실패와 구분돼야 한다" assert "정상 2" in text_blob, "통과 항목도 사용자가 안심할 수 있게 요약돼야 한다" assert "필수 설정" in text_blob assert "선택 기능" in text_blob assert "운영 상태" in text_blob assert "AI 요약은 선택" in text_blob, "Google/agy가 기본 수집을 막지 않는다는 문구가 필요하다" finally: win.destroy() def test_onboarding_check_rows_use_badges_and_readable_action_buttons(monkeypatch: pytest.MonkeyPatch) -> None: """행 내부 액션도 clipped/색 단독/좁은 버튼 회귀를 막는다.""" tkinter = pytest.importorskip("tkinter") from tkinter import ttk from dmf_crawler.gui import app as gui_app from dmf_crawler.gui.widgets import StatusBadge monkeypatch.setattr(gui_app.OnboardingApp, "refresh", lambda self: None) win = _new_onboarding_app(gui_app, mode="inspect") try: win._render( [ _result( "api_key_present", "공공데이터포털 인증키", ok=False, severity=Severity.CRITICAL, detail="키가 없어 오늘 수집을 시작할 수 없습니다.", fix_hint="키 입력 버튼을 누르세요.", fix_action="enter_api_key", ) ] ) win.update_idletasks() widgets = list(_walk_widgets(win)) badges = [w for w in widgets if isinstance(w, StatusBadge)] assert badges, "상태는 텍스트 옆의 전용 badge로도 보여야 색 단독 회귀를 막을 수 있다" buttons = [w for w in widgets if isinstance(w, ttk.Button)] labels = {str(btn.cget("text")): btn for btn in buttons} assert "키 입력" in labels action = labels["키 입력"] assert int(str(action.cget("width") or "0")) >= 14 assert int(action.winfo_reqwidth()) >= 104 assert int(action.winfo_reqheight()) >= 34 finally: win.destroy()