Files
dikte/tests/render_ui.py
T

170 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Capture native Qt client areas with isolated settings and sample data.
Run with python -m tests.render_ui --output DIRECTORY. CI explicitly selects
xcb, cocoa or windows before importing the test package; the default local
test backend remains offscreen. No audio, API request or input is generated.
Window decorations, native file dialogs and compositor effects are excluded.
"""
import argparse
import html
import json
import platform
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
from PyQt6.QtCore import QCoreApplication, QEvent, QT_VERSION_STR, Qt
from PyQt6.QtGui import QFontInfo
from PyQt6.QtWidgets import QApplication, QLineEdit, QScrollArea
from tests.test_ui import Settings
from dikte import config as cfg, i18n, overlay, settings_ui, theme
from dikte.home_ui import HomeWindow
SAMPLE = "Bir sonraki sürüm için kayıt kontrollerini tamamlayalım. Ayarları gözden geçirip uygulamayı üç platformda da deneyelim."
CASES = (
("dictation", 620, 560), ("dictation-wide", 1900, 1000),
("file", 620, 640), ("meeting", 680, 760), ("ask", 680, 640),
("settings-general", 720, 760), ("settings-display", 720, 640),
("settings-api", 720, 760), ("settings-assistant", 720, 760),
("settings-shortcuts", 720, 640), ("overlay", 220, 48),
)
SETTINGS_PAGES = {"general": 0, "display": 1, "api": 2, "assistant": 4, "shortcuts": 6}
def settle():
for _ in range(5):
QApplication.processEvents()
def capture(widget, path, width, height):
# Native styles and fonts still render; monitor size does not constrain
# wide-window cases, and the runner's other windows cannot cover them.
widget.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
widget.resize(width, height)
widget.show()
settle()
pixmap = widget.grab()
if pixmap.isNull() or not pixmap.save(str(path), "PNG"):
raise RuntimeError(f"Could not capture {path.name}")
font = QFontInfo(widget.font())
result = {
"file": path.name, "requested_size": [width, height],
"logical_size": [widget.width(), widget.height()],
"pixel_size": [pixmap.width(), pixmap.height()],
"device_pixel_ratio": pixmap.devicePixelRatio(),
"font": font.family(), "font_points": font.pointSizeF(),
"horizontal_overflow": [],
}
for area in widget.findChildren(QScrollArea):
if area.isVisible() and area.horizontalScrollBar().maximum() > 0:
result["horizontal_overflow"].append(area.horizontalScrollBar().maximum())
widget.hide()
print(f"Captured {path.name}: {result['logical_size']}, {font.family()}", flush=True)
return result
def capture_theme(name, output):
harness = Settings("runTest")
# Keep the actual platform's application branches as well as Qt's style.
harness.platform = sys.platform
harness.setUp()
try:
harness.enterContext(mock.patch.object(settings_ui.SettingsWindow, "_sources_once", return_value=[]))
conf = harness.config(
theme=name, ui_language="tr", transcribe_provider="openrouter",
openrouter_api_key="screenshot-only", openrouter_transcribe_model="openai/whisper-1",
cleanup_provider="openrouter", assistant_provider="openrouter",
)
i18n.set_language("tr")
cfg.append_history({"ts": "2026-09-09 14:32:00", "duration": 18,
"elapsed": 2, "text": SAMPLE, "raw": SAMPLE})
settings = harness.window(conf)
settings.keep_audio.setText("Ses kayıtlarını sakla (örnek kayıt klasörü)")
settings.assistant_dir.setPlaceholderText("Proje klasörü")
for field in settings.findChildren(QLineEdit):
if field.isReadOnly() and "__main__.py toggle" in field.text():
field.setText("dikte toggle")
settings._saved_form = settings._form_values()
settings._show_dirty()
controller = SimpleNamespace(
conf=conf, state="idle", ask_state="idle", meeting_state="idle",
recording=False, paused=False, home_messages={}, meeting_message="",
paste_override={}, meeting_elapsed=SimpleNamespace(elapsed=lambda: 12000),
_recorded_seconds=lambda: 12, open_settings=settings.show,
)
for method in ("reset_conversation", "_toggle_pause", "_cancel", "cancel_ask",
"cancel_meeting", "_toggle_meeting", "start", "stop", "start_ask", "stop_ask"):
setattr(controller, method, mock.Mock())
home = HomeWindow(controller, settings)
harness.addCleanup(home.deleteLater)
harness.addCleanup(home.close)
home._timer.stop()
settings.file_label.setText("örnek-kayıt.wav")
settings.file_output.setPlainText(SAMPLE)
home.ask_output.setPlainText("Örnek yanıt: Önce arayüzü doğrulayalım, ardından sürümü hazırlayalım.")
indicator = overlay.Overlay(theme_name=name)
harness.addCleanup(indicator.deleteLater)
harness.addCleanup(indicator.close)
images = []
for case, width, height in CASES:
if case.startswith("settings-"):
settings.tabs.setCurrentIndex(SETTINGS_PAGES[case.removeprefix("settings-")])
widget = settings
elif case == "overlay":
indicator.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
indicator.show_recording()
indicator._anim.stop()
indicator.set_seconds(12)
indicator.levels = [0.15, 0.3, 0.7, 0.4] * (overlay.BARS // 4) + [0.2] * (overlay.BARS % 4)
widget = indicator
width, height = indicator.width(), indicator.height()
else:
home.show_mode("dictation" if case == "dictation-wide" else case)
widget = home
images.append(capture(widget, output / f"{name}-{case}.png", width, height))
return images
finally:
harness.doCleanups()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--expect-platform")
args = parser.parse_args()
app = QApplication.instance()
backend = app.platformName()
if args.expect_platform and backend != args.expect_platform:
parser.error(f"Expected {args.expect_platform}, got {backend}")
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
manifest = {
"system": platform.system(), "python": platform.python_version(),
"qt": QT_VERSION_STR, "qpa_backend": backend,
"qt_style": app.style().objectName(), "language": "tr",
"scope": "Native Qt client-area renders with sample data and isolated settings. No real recording, API call, window decorations, native file dialogs or compositor validation.",
"images": [],
}
for name in theme.NAMES:
manifest["images"].extend(capture_theme(name, output))
(output / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
title = f"Dikte: {manifest['system']} / {backend} / Qt {QT_VERSION_STR}"
cards = "".join(
f'<figure><a href="{entry["file"]}"><img loading="lazy" src="{entry["file"]}"></a><figcaption>{html.escape(entry["file"])}</figcaption></figure>'
for entry in manifest["images"]
)
(output / "index.html").write_text(
'<!doctype html><meta charset="utf-8"><title>' + html.escape(title) + '</title>'
'<style>body{font:16px system-ui;background:#eee;color:#222;margin:24px}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px}figure{margin:0}img{width:100%;height:340px;object-fit:contain;object-position:top}figcaption{padding:8px}</style>'
'<h1>' + html.escape(title) + '</h1><p>' + html.escape(manifest["scope"]) + '</p><main>' + cards + '</main>',
encoding="utf-8",
)
if __name__ == "__main__":
main()