chore: 저장소 구조 정리 및 문서화, 첫 커밋
- 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 문서 지도 갱신
This commit is contained in:
commit
56a6e2da93
159 changed files with 145825 additions and 0 deletions
173
tests/test_gui_design_contracts.py
Normal file
173
tests/test_gui_design_contracts.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue