- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes) - 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에 안내용 README.md 추가 - CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가 - docs/README.md 문서 지도 갱신
173 lines
6.2 KiB
Python
173 lines
6.2 KiB
Python
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()
|