Clock_out_Time_Calculator/_i18n_gui_test.py
KINDNICK bedbb1e9ec
Some checks failed
CI / test (push) Has been cancelled
Initial release v2.2.0
핵심 기능:
- 단축근무·표준·반일 등 다양한 근무 패턴 (5개 프리셋 + 사용자 정의)
- Windows 이벤트 뷰어 자동 출퇴근 감지
- 30분 단위 연장근무 적립/사용 시스템
- 1.0/0.5/0.25일 연차·반차·반반차
- 자동 점심·저녁·외출·자동 백업·화면 잠금 자동 외출
- 한국 공휴일 자동 등록 (음력 포함, holidays 패키지)
- matplotlib 차트 기반 주간/월간/패턴 통계
- 미니 위젯 + 시스템 트레이 통합
- 한국어/English i18n
- 자가 업데이트 (updater.exe + Gitea Releases)

아키텍처:
- core/ (db, time_calculator, notifier, i18n, version, settings_keys)
- ui/ (main_window + 9 dialogs + 3 controllers)
- utils/ (backup, lock_detector, debug_log, updater_client, time_format)
- tests/ (66 pytest 단위) + 통합/i18n GUI 검증

CI/CD:
- .gitea/workflows/ci.yml: push 시 pytest + 통합 테스트
- .gitea/workflows/release.yml: v* 태그 push 시 두 .exe 자동 빌드 + Releases 첨부

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:54:40 +09:00

158 lines
4.9 KiB
Python

"""
i18n GUI 검증: 언어 전환 시 UI 라벨이 실제로 바뀌는지.
동일 SettingsView를 한국어로 만들고 확인 → 영어로 만들고 확인.
(런타임 언어 전환은 위젯 재생성을 요구하므로 새 인스턴스로 검증)
"""
import os
import sys
import tempfile
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PyQt5.QtWidgets import QApplication, QPushButton, QGroupBox
app = QApplication.instance() or QApplication(sys.argv)
from core.database import Database
from core.i18n import set_language
db_path = os.path.join(tempfile.gettempdir(), 'clockout_i18n.db')
if os.path.exists(db_path):
os.remove(db_path)
db = Database(db_path)
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}")
def find_button_text(widget, text_substring):
for btn in widget.findChildren(QPushButton):
if text_substring in btn.text():
return btn
return None
def find_groupbox_title(widget, title_substring):
for gb in widget.findChildren(QGroupBox):
if title_substring in gb.title():
return gb
return None
def test_settings_korean_labels():
"""한국어 모드에서 SettingsView 라벨 검증."""
set_language('ko')
from ui.settings_view import SettingsView
dlg = SettingsView(db=db)
# 윈도우 제목
assert '설정' in dlg.windowTitle(), f"title: {dlg.windowTitle()}"
# 그룹박스
assert find_groupbox_title(dlg, '근무 시간') is not None
assert find_groupbox_title(dlg, '연장근무') is not None
assert find_groupbox_title(dlg, '데이터') is not None
# 저장 버튼
save_btn = find_button_text(dlg, '저장')
assert save_btn is not None
dlg.deleteLater()
def test_settings_english_labels():
"""영어 모드에서 SettingsView 라벨 검증."""
set_language('en')
from ui.settings_view import SettingsView
dlg = SettingsView(db=db)
assert 'Settings' in dlg.windowTitle()
assert find_groupbox_title(dlg, 'Work Time') is not None
assert find_groupbox_title(dlg, 'Overtime') is not None
assert find_groupbox_title(dlg, 'Data') is not None
save_btn = find_button_text(dlg, 'Save')
assert save_btn is not None
dlg.deleteLater()
set_language('ko') # 원복
def test_main_window_language():
"""MainWindow 라벨 — 한국어 / 영어"""
set_language('ko')
from ui.main_window import MainWindow
w = MainWindow()
assert '퇴근시간' in w.windowTitle()
# 메뉴 버튼: 통계/캘린더/일일보고/도움말/설정/퇴근하기
assert find_button_text(w, '통계') is not None
assert find_button_text(w, '캘린더') is not None
assert find_button_text(w, '도움말') is not None
assert find_button_text(w, '점심') is not None
w.deleteLater()
def test_mini_widget_language():
set_language('ko')
from ui.mini_widget import MiniWidget
w = MiniWidget()
assert '남은 시간' in w.title_label.text()
w.update_remaining('+00:30:00')
assert '추가' in w.title_label.text()
w.deleteLater()
set_language('en')
w2 = MiniWidget()
assert 'Remaining' in w2.title_label.text()
w2.update_remaining('+00:30:00')
assert 'Overtime' in w2.title_label.text()
w2.deleteLater()
set_language('ko')
def test_notifier_language():
"""알림 메시지가 언어에 맞춰 나오는지"""
set_language('ko')
from core.notifier import Notifier
from datetime import datetime, timedelta
n = Notifier(db=db)
fired = []
n.notification_signal.connect(lambda t, m: fired.append((t, m)))
n.check_clock_out_soon(datetime.now() + timedelta(minutes=20), datetime.now())
assert len(fired) == 1
assert '퇴근' in fired[0][0]
set_language('en')
n2 = Notifier(db=db)
fired2 = []
n2.notification_signal.connect(lambda t, m: fired2.append((t, m)))
n2.check_clock_out_soon(datetime.now() + timedelta(minutes=20), datetime.now())
assert len(fired2) == 1
assert 'Clock-out' in fired2[0][0]
set_language('ko')
case("I18N-1. SettingsView 한국어 라벨", test_settings_korean_labels)
case("I18N-2. SettingsView 영어 라벨", test_settings_english_labels)
case("I18N-3. MainWindow 한국어 (메뉴/버튼)", test_main_window_language)
case("I18N-4. MiniWidget ko/en 전환 후 라벨", test_mini_widget_language)
case("I18N-5. Notifier 알림 메시지 ko/en", test_notifier_language)
print()
print("=" * 60)
print(f"PASS: {len(PASS)} FAIL: {len(FAIL)}")
if FAIL:
print("\nFAILED:")
for label, err in FAIL:
print(f" - {label}: {err}")
if os.path.exists(db_path):
try: os.remove(db_path)
except: pass
sys.exit(1 if FAIL else 0)