Clock_out_Time_Calculator/_gui_smoke_test.py

181 lines
5.9 KiB
Python

"""
GUI smoke 테스트 — QApplication을 띄우지 않고 UI 다이얼로그 instantiation만 확인.
실제 .show()는 헤드리스 환경에서 위험하므로 생성 단계까지만 검증.
"""
import os
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Qt platform plugin: offscreen으로 실제 창 안 뜨게
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
# 백그라운드 휴일 동기화 스레드 비활성화 (DB lock / segfault 방지)
os.environ['CLOCKOUT_DISABLE_HOLIDAY_SYNC'] = '1'
from PyQt5.QtWidgets import QApplication
app = QApplication.instance() or QApplication(sys.argv)
PASS, FAIL = [], []
def case(label, fn):
try:
fn()
PASS.append(label)
print(f"[PASS] {label}")
except Exception as e:
FAIL.append((label, f"{type(e).__name__}: {e}"))
print(f"[FAIL] {label}: {type(e).__name__}: {e}")
# Fresh DB
from core.database import Database
db_path = os.path.join(tempfile.gettempdir(), 'clockout_gui_test.db')
if os.path.exists(db_path):
os.remove(db_path)
db = Database(db_path)
def test_settings_view():
from ui.settings_view import SettingsView
dlg = SettingsView(db=db)
# 단축근무 7h30m 프리셋 적용
dlg.work_preset_combo.setCurrentIndex(1) # "단축근무 7시간 30분"
assert dlg.work_hours_spin.value() == 7
assert dlg.work_minutes_spin.value() == 30
assert dlg.lunch_spin.value() == 30
dlg.deleteLater()
def test_help_view():
from ui.help_view import HelpView
dlg = HelpView()
# 6개 탭 존재
tabs = dlg.findChild(type(dlg).__mro__[0])
from PyQt5.QtWidgets import QTabWidget
tab_widget = dlg.findChild(QTabWidget)
assert tab_widget is not None
assert tab_widget.count() == 6
dlg.deleteLater()
def test_mini_widget():
from ui.mini_widget import MiniWidget
w = MiniWidget()
w.update_remaining("01:30:00")
assert w.time_label.text() == "01:30:00"
w.update_remaining("+00:30:00")
assert "추가" in w.title_label.text()
w.deleteLater()
def test_chart_widget():
from ui.chart_widget import make_chart_widget, draw_daily_hours, draw_weekday_avg
w = make_chart_widget()
# 빈 records로도 안전하게 그려져야
draw_daily_hours(w, [])
# 데이터가 있을 때
records = [
{'date': '2026-04-25', 'total_hours': 8.5, 'overtime_minutes': 30},
{'date': '2026-04-26', 'total_hours': 9.0, 'overtime_minutes': 60},
]
draw_daily_hours(w, records)
draw_weekday_avg(w, records)
w.deleteLater()
def test_stats_view():
from ui.stats_view import StatsView
dlg = StatsView(db=db)
# 데이터 없어도 정상 로드
assert dlg.weekly_total_card is not None
dlg.deleteLater()
def test_calendar_view():
from ui.calendar_view import CalendarView
dlg = CalendarView(db=db)
dlg.deleteLater()
def test_main_window_init():
"""MainWindow 초기화 — 가장 무거운 케이스"""
# QLocalServer 충돌 방지: 프로세스 ID 기반 이름 변경 어려움 → init만 확인
from ui.main_window import MainWindow
from datetime import date as _date
# MainWindow load_today_data에서 QMessageBox를 띄우지 않도록 오늘 출근 기록을 미리 삽입
today = _date.today().isoformat()
conn = db.get_connection()
try:
conn.execute("DELETE FROM work_records WHERE date = ?", (today,))
conn.execute(
"INSERT INTO work_records (date, clock_in, clock_out, lunch_break, dinner_break) VALUES (?, ?, ?, ?, ?)",
(today, '09:00:00', '18:00:00', 0, 0)
)
conn.commit()
finally:
conn.close()
w = MainWindow(db=db)
# 기본 상태
assert w.is_clocked_in == False # 퇴근 완료 기록이므로 False
assert w.lunch_break_enabled == False
# auto_lunch 캐시 초기 None (AutoLunchManager 낶)
assert w._auto_lunch._enabled_cache is None
# 단축키 7개 등록되었는지
from PyQt5.QtWidgets import QShortcut
shortcuts = w.findChildren(QShortcut)
assert len(shortcuts) >= 7, f"shortcuts: {len(shortcuts)}"
w.deleteLater()
def test_settings_save_load_round_trip():
"""설정 저장→로드 라운드트립"""
from ui.settings_view import SettingsView
dlg = SettingsView(db=db)
# 단축근무 프리셋 → 저장
dlg.work_preset_combo.setCurrentIndex(1)
# save 호출 시 메시지박스가 뜨므로 save_settings 직접 호출 회피
# 대신 저장 로직과 동일한 dict 만들기
work_min = dlg.work_hours_spin.value() * 60 + dlg.work_minutes_spin.value()
db.save_settings({
'work_minutes': work_min,
'lunch_duration_minutes': dlg.lunch_spin.value(),
})
# 새 다이얼로그에 다시 로드
dlg2 = SettingsView(db=db)
assert dlg2.work_hours_spin.value() == 7
assert dlg2.work_minutes_spin.value() == 30
assert dlg2.lunch_spin.value() == 30
# 프리셋이 자동 매칭되었는지
assert dlg2.work_preset_combo.currentIndex() == 1
dlg.deleteLater()
dlg2.deleteLater()
# 실행
case("UI-1. SettingsView: 단축근무 프리셋 적용", test_settings_view)
case("UI-2. HelpView: 6개 탭 생성 확인", test_help_view)
case("UI-3. MiniWidget: 시간 업데이트 + 추가근무 표시", test_mini_widget)
case("UI-4. ChartWidget: 빈/유효 데이터 그리기", test_chart_widget)
case("UI-5. StatsView: 데이터 없이 로드", test_stats_view)
case("UI-6. CalendarView: 인스턴스화", test_calendar_view)
case("UI-7. MainWindow: 단축키 7개 등록 + 초기 상태", test_main_window_init)
case("UI-8. Settings 저장→재로드: 단축근무 7h30m round-trip", test_settings_save_load_round_trip)
print()
print("=" * 60)
print(f"PASS: {len(PASS)} FAIL: {len(FAIL)}")
if FAIL:
print("\nFAILED:")
for label, err in FAIL:
print(f" - {label}: {err}")
# Cleanup
if os.path.exists(db_path):
try: os.remove(db_path)
except: pass
sys.exit(1 if FAIL else 0)