"""Native workspace navigation, capture boundaries and persisted results.""" from types import SimpleNamespace from unittest import mock from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QApplication, QMessageBox, QPushButton from dikte import config as cfg, home_ui, i18n from dikte.app import Dikte from tests import test_ui from tests.support import DikteTest class Home(DikteTest): def setUp(self): super().setUp() self.fixture = test_ui.Settings("runTest") self.fixture.setUp() self.addCleanup(self.fixture.doCleanups) self.conf = self.fixture.config(transcribe_provider="openai", openai_api_key="test", cleanup_enabled=False) self.settings = self.fixture.window(self.conf) self.controller = SimpleNamespace( conf=self.conf, state="idle", ask_state="idle", meeting_state="idle", meeting_message="", home_messages={}, recording=False, paused=False, paste_override={}, open_settings=mock.Mock(), reset_conversation=mock.Mock(), _recorded_seconds=lambda: 65, meeting_elapsed=SimpleNamespace(elapsed=lambda: 90000), ) for name in ("start", "stop", "start_ask", "stop_ask", "_toggle_pause", "_cancel", "cancel_ask", "_toggle_meeting", "cancel_meeting"): setattr(self.controller, name, mock.Mock()) self.window = home_ui.HomeWindow(self.controller, self.settings) self.addCleanup(self.window.deleteLater) self.addCleanup(self.window.close) self.window.show() QApplication.processEvents() def test_daily_tasks_are_reachable_outside_configuration(self): for mode in ("dictation", "file", "meeting", "ask", "history"): self.window.show_mode(mode) QApplication.processEvents() self.assertEqual(self.window.pages.currentWidget(), self.window.mode_pages[mode]) self.window.show_mode("meeting") self.assertTrue(self.settings.minutes_view.isVisible()) self.window.show_mode("history") self.assertTrue(self.settings.history.isVisible()) self.assertEqual(self.settings.tabs.count(), 7) self.assertFalse(self.settings.tabs.tabBar().isVisible()) def test_native_chrome_and_capture_geometry(self): self.assertFalse(self.window.windowFlags() & Qt.WindowType.FramelessWindowHint) self.assertEqual(self.window.capture_button.width(), self.window.capture_button.height()) self.assertGreaterEqual(self.window.capture_button.height(), 100) def test_theme_preview_discard_and_save_keep_runtime_separate(self): self.assertEqual(self.conf["theme"], "nord") self.settings.theme_choice.setCurrentIndex(self.settings.theme_choice.findData("light")) self.assertEqual(self.conf["theme"], "nord") self.assertIn("#FFFFFF", self.settings.styleSheet()) self.assertNotEqual(self.window.styleSheet(), self.settings.styleSheet()) self.settings._discard_changes() self.assertEqual(self.settings.theme_choice.currentData(), "nord") self.settings.theme_choice.setCurrentIndex(self.settings.theme_choice.findData("dracula")) self.settings._save() self.assertEqual(cfg.Config()["theme"], "dracula") self.assertEqual(self.window._theme_name, "dracula") self.assertIn("#282A36", self.window.styleSheet()) def test_theme_colors_are_per_overlay_and_unknown_name_falls_back(self): from dikte import overlay, theme dark = overlay.Overlay(theme_name="dark") light = overlay.Overlay(theme_name="light") self.addCleanup(dark.deleteLater) self.addCleanup(light.deleteLater) self.assertEqual(light.colors["base"].name(), "#ffffff") self.assertEqual(dark.colors["base"].name(), "#101010") dark.set_theme("dracula") self.assertEqual(light.colors["base"].name(), "#ffffff") self.assertEqual(theme.stylesheet("unknown"), theme.stylesheet("nord")) def test_wide_meeting_page_keeps_actions_compact(self): self.window.resize(900, 700) self.window.show_mode("meeting") QApplication.processEvents() self.assertLess(self.window.meeting_button.width(), 300) self.assertLessEqual(self.settings.minutes_view.width(), 680) def test_wide_windows_center_every_task_and_settings_page(self): self.window.resize(1900, 1000) for mode, area in self.window.mode_pages.items(): self.window.show_mode(mode) QApplication.processEvents() with self.subTest(mode=mode): self.assertLessEqual(area.widget().width(), 680) self.assertAlmostEqual(area.widget().geometry().center().x(), area.viewport().rect().center().x(), delta=1) self.settings.resize(1900, 1000) self.settings.show() for index in range(self.settings.tabs.count()): self.settings.tabs.setCurrentIndex(index) QApplication.processEvents() area = self.settings.tabs.widget(index) with self.subTest(settings=index): self.assertAlmostEqual(area.widget().geometry().center().x(), area.viewport().rect().center().x(), delta=1) def test_meeting_and_assistant_actions_share_one_row(self): self.window.resize(620, 760) for mode, labels in ( ("meeting", ("Copy", "Write it up", "Open the folder", "Delete selected", "Reload")), ("ask", (self.window.ask_button.text(), "Start a new conversation")), ): self.window.show_mode(mode) QApplication.processEvents() page = self.window.mode_pages[mode] buttons = {b.text(): b for b in page.findChildren(QPushButton)} positions = [buttons[label].mapTo(page, buttons[label].rect().center()).y() for label in labels] with self.subTest(mode=mode): self.assertLessEqual(max(positions) - min(positions), 1) def test_empty_state_does_not_invent_a_transcript(self): self.assertEqual(self.window.latest_text.toPlainText(), "") self.assertFalse(self.window.copy_button.isEnabled()) self.assertFalse(self.window.open_button.isEnabled()) def test_clearing_history_removes_the_latest_preview(self): cfg.append_history({"text": "Remove this preview"}) self.window.refresh_results() self.window.show_mode("history") with mock.patch.object(self.settings, "_confirm", return_value=True): self.settings._clear_history() self.window.show_mode("dictation") self.assertEqual(self.window.latest_text.toPlainText(), "") self.assertFalse(self.window.copy_button.isEnabled()) def test_missing_local_model_opens_setup_without_recording(self): self.conf["transcribe_provider"] = "local" self.window.refresh() self.assertEqual(self.window.capture_status.text(), "Set up transcription") self.window._capture() self.controller.start.assert_not_called() self.controller.open_settings.assert_called_once() self.assertEqual(self.settings.tabs.currentIndex(), self.settings.api_tab_index) self.assertEqual(self.controller.paste_override, {}) def test_button_capture_never_automatically_pastes(self): def start(): self.assertIs(self.controller.paste_override["dictation"], False) self.controller.state = "recording" self.controller.recording = True self.controller.start.side_effect = start self.window._capture() self.controller.start.assert_called_once() self.assertFalse(self.controller.paste_override["dictation"]) self.assertIn("01:05", self.window.capture_status.text()) self.assertTrue(self.window.pause_button.isVisible()) self.window._capture() self.controller.stop.assert_called_once() def test_failed_capture_does_not_leak_a_paste_override(self): self.window._capture() self.assertEqual(self.controller.paste_override, {}) def test_busy_capture_can_queue_but_does_not_steal_assistant_microphone(self): self.controller.state = "busy" self.window.refresh() self.assertTrue(self.window.capture_button.isEnabled()) self.controller.ask_state = "recording" self.controller.recording = True self.window.refresh() self.assertFalse(self.window.capture_button.isEnabled()) self.window._capture() self.controller.start.assert_not_called() def test_pause_cancel_and_failures_are_visible(self): self.controller.state = "recording" self.controller.recording = True self.controller.paused = True self.window.refresh() self.assertIn("Paused", self.window.capture_status.text()) self.window._pause() self.controller._toggle_pause.assert_called_once() self.window._cancel_capture() self.controller._cancel.assert_called_once() self.controller.home_messages["dictation"] = "Microphone permission denied" self.window.refresh() self.assertIn("permission denied", self.window.capture_error.text()) def test_real_latest_dictation_is_separate_from_assistant_answer(self): cfg.append_history({"ts": "2026-09-09 12:00:00", "text": "Actual transcript"}) cfg.append_history({"mode": "ask", "text": "Actual answer"}) self.window.refresh_results() self.assertEqual(self.window.latest_text.toPlainText(), "Actual transcript") self.assertEqual(self.window.ask_output.toPlainText(), "Actual answer") cursor = self.window.latest_text.textCursor() cursor.setPosition(3) self.window.latest_text.setTextCursor(cursor) self.window.refresh() self.window.refresh_results() self.assertEqual(self.window.latest_text.textCursor().position(), 3) cfg.clear_history() self.window.refresh_results() self.assertEqual(self.window.latest_text.toPlainText(), "") def test_processing_summary_uses_full_models_and_actual_acceleration(self): self.conf["transcribe_provider"] = "local" self.conf["local_model"] = "ggml-large-v3-turbo-q5_0.bin" self.conf["cleanup_enabled"] = True self.conf["cleanup_provider"] = "local" self.conf["local_llm_model"] = "gemma-3-4b-it-Q4_K_M.gguf" self.conf["local_gpu"] = True with mock.patch.object(home_ui.ggml, "state", return_value={ "whisper": {"running": True, "backend": "CPU"}, "llama": {"running": True, "backend": "Vulkan"}, }): text = home_ui.processing_locations(self.conf) self.assertIn("ggml-large-v3-turbo-q5_0.bin (Local CPU)", text) self.assertIn("gemma-3-4b-it-Q4_K_M.gguf (Local GPU)", text) self.assertNotIn("API", text) with mock.patch.object(home_ui.ggml, "state", return_value={}): text = home_ui.processing_locations(self.conf) self.assertNotIn("GPU", text) self.assertIn("(Local)", text) self.assertIn(self.conf["meeting_model"], home_ui.processing_locations(self.conf, "meeting")) self.conf["assistant_cleanup"] = True self.conf["cleanup_provider"] = "gemini" self.conf["assistant_provider"] = "codex" text = home_ui.processing_locations(self.conf, "ask") self.assertIn(self.conf["cleanup_gemini_model"] + " (API)", text) self.assertIn("Codex default model (CLI)", text) def test_timestamped_file_summary_uses_the_timestamp_model(self): self.conf["transcribe_provider"] = "openrouter" self.conf["openrouter_transcribe_model"] = "google/gemini-audio" self.conf["openrouter_file_model"] = "openai/whisper-1" text = home_ui.processing_locations(self.conf, "file", file_timestamps=True) self.assertIn("openai/whisper-1 (API)", text) self.assertNotIn("google/gemini-audio", text) def test_meeting_summary_uses_segment_model_even_without_file_timestamps(self): self.conf["transcribe_provider"] = "openai" self.conf["transcribe_model"] = "gpt-4o-transcribe" text = home_ui.processing_locations(self.conf, "meeting", file_timestamps=False) self.assertIn("whisper-1 (API)", text) self.assertNotIn("gpt-4o-transcribe", text) self.conf["transcribe_provider"] = "openrouter" self.conf["openrouter_file_model"] = "mistralai/voxtral-small-24b-2507" text = home_ui.processing_locations(self.conf, "meeting", file_timestamps=False) self.assertIn("mistralai/voxtral-small-24b-2507 (API)", text) def test_unsupported_meeting_is_disabled(self): self.enterContext(mock.patch.object(home_ui.audio, "sound", return_value=SimpleNamespace(meetings=False))) self.window.show_mode("meeting") self.assertFalse(self.window.meeting_button.isEnabled()) self.assertIn("not supported", self.window.meeting_hint.text()) self.window._meeting() self.controller._toggle_meeting.assert_not_called() def test_assistant_scope_uses_actual_shortcut_and_permissions(self): self.conf["assistant_provider"] = "codex" self.conf["assistant_shortcut"] = "Ctrl+Alt+A" self.conf["assistant_dir"] = self.root self.conf["assistant_codex_sandbox"] = "danger-full-access" self.window.show_mode("ask") self.assertIn("Ctrl+Alt+A", self.window.ask_scope.text()) self.assertIn(self.root, self.window.ask_scope.text()) self.assertIn("No sandbox at all", self.window.ask_scope.text()) self.assertEqual(self.window.ask_button.text(), "Set up assistant") self.window._ask() self.controller.start_ask.assert_not_called() self.controller.open_settings.assert_called_once() def test_failed_settings_save_keeps_runtime_config_and_form_edits(self): before = dict(self.conf.data) self.settings.auto_paste.setChecked(not self.conf["auto_paste"]) self.assertEqual(self.settings.dirty_label.text(), "Unsaved changes") with mock.patch.object(self.conf, "save", side_effect=OSError("disk full")), mock.patch.object(QMessageBox, "warning"): self.settings._save() self.assertEqual(self.conf.data, before) self.assertNotEqual(self.settings.auto_paste.isChecked(), self.conf["auto_paste"]) self.settings.file_path = "/tmp/chosen.wav" self.settings._discard_changes() self.assertEqual(self.settings.dirty_label.text(), "") self.assertEqual(self.settings.file_path, "/tmp/chosen.wav") def test_small_window_keeps_navigation_and_footer_accessible(self): self.window.resize(460, 460) QApplication.processEvents() for mode in ("dictation", "file", "meeting", "ask", "history"): self.window.show_mode(mode) QApplication.processEvents() if self.window.footer.isVisible(): self.assertTrue(self.window.rect().contains(self.window.footer.geometry())) self.assertTrue(self.window.rect().contains(self.window.mode_buttons["dictation"].geometry().topLeft())) def test_apply_merges_unrelated_cli_changes_and_keeps_user_edits(self): self.settings.auto_paste.setChecked(False) self.conf["shortcut"] = "Ctrl+Shift+F9" self.conf["groq_transcribe_model"] = "external-model" self.settings.refresh_configuration() self.assertFalse(self.settings.auto_paste.isChecked()) self.settings._save() self.assertFalse(self.conf["auto_paste"]) self.assertEqual(self.conf["shortcut"], "Ctrl+Shift+F9") self.assertEqual(self.conf["groq_transcribe_model"], "external-model") self.assertEqual(self.settings._shortcut_rows["toggle"][0].currentText(), "Ctrl+Shift+F9") self.assertEqual(self.settings.dirty_label.text(), "") def test_clean_form_refreshes_from_cli_without_changing_file_result(self): self.settings.file_output.setPlainText("Existing file result") self.conf["shortcut"] = "Ctrl+Alt+F9" self.settings.refresh_configuration() self.assertEqual(self.settings._shortcut_rows["toggle"][0].currentText(), "Ctrl+Alt+F9") self.assertEqual(self.settings.file_output.toPlainText(), "Existing file result") self.assertEqual(self.settings.dirty_label.text(), "") def test_cached_provider_model_edits_remain_dirty_after_switching_back(self): self.settings._select_data(self.settings.transcribe_provider, "groq") self.settings.transcribe_model.setCurrentText("my-groq-model") self.settings._select_data(self.settings.transcribe_provider, "openai") self.assertEqual(self.settings.dirty_label.text(), "Unsaved changes") self.settings._save() self.assertEqual(self.conf["groq_transcribe_model"], "my-groq-model") def test_cli_selected_new_source_is_resolved_when_settings_reopens(self): self.conf["mic_target"] = "new-usb" self.settings.refresh_configuration() with mock.patch.object(home_ui.audio, "list_sources", return_value=[("new-usb", "New USB microphone")]), mock.patch.object(home_ui.audio, "list_monitors", return_value=[]): self.settings.refresh_sources() self.assertEqual(self.settings.mic.currentData(), "new-usb") self.assertEqual(self.settings.mic.currentText(), "New USB microphone") self.settings._save() self.assertEqual(self.conf["mic_target"], "new-usb") def test_language_rebuild_preserves_file_result_and_active_mode(self): from dikte.meeting import MeetingPipeline controller = Dikte.__new__(Dikte) controller.__dict__.update(vars(self.controller)) controller.meetings = MeetingPipeline(self.conf) controller._make_settings() old_settings = controller.settings_window old_home = home_ui.HomeWindow(controller, old_settings) controller.home_window = old_home old_home.show_mode("file") old_home.show() old_settings.file_path = "/tmp/chosen.wav" old_settings.file_label.setText("chosen.wav") old_settings.file_output.setPlainText("Retained transcript") old_settings.file_segments = [{"text": "Retained transcript", "start": 0, "end": 2}] i18n.set_language("tr") controller._reopen_settings() self.addCleanup(controller.settings_window.deleteLater) self.addCleanup(controller.settings_window.close) self.addCleanup(controller.home_window.deleteLater) self.addCleanup(controller.home_window.close) self.assertEqual(controller.home_window.mode, "file") self.assertEqual(controller.settings_window.file_path, "/tmp/chosen.wav") self.assertEqual(controller.settings_window.file_output.toPlainText(), "Retained transcript") self.assertTrue(controller.settings_window.file_save_srt.isEnabled()) self.assertEqual(controller.home_window.mode_buttons["file"].text(), "Dosya") def test_disconnected_configured_source_survives_an_unrelated_apply(self): self.conf["mic_target"] = "disconnected-usb" self.settings.refresh_configuration() self.assertEqual(self.settings.mic.currentData(), "disconnected-usb") self.settings.auto_paste.setChecked(False) self.settings._save() self.assertEqual(self.conf["mic_target"], "disconnected-usb") def test_source_refresh_keeps_selection_and_discovers_hotplugged_devices(self): self.settings.mic.addItem("Old microphone", "old") self.settings.mic.setCurrentIndex(self.settings.mic.findData("old")) with mock.patch.object(home_ui.audio, "list_sources", return_value=[("usb", "USB microphone")]), mock.patch.object(home_ui.audio, "list_monitors", return_value=[("loop", "Loopback")]): self.settings.refresh_sources() self.assertEqual(self.settings.mic.currentData(), "old") self.assertGreaterEqual(self.settings.mic.findData("usb"), 0) self.assertGreaterEqual(self.settings.meeting_mic.findData("usb"), 0) self.assertGreaterEqual(self.settings.meeting_system.findData("loop"), 0) def test_missing_assistant_directory_displays_the_actual_fallback(self): self.conf["assistant_provider"] = "codex" self.conf["assistant_dir"] = "/does/not/exist/dikte-test" self.window.show_mode("ask") self.assertNotIn(self.conf["assistant_dir"], self.window.ask_scope.text()) self.assertIn(home_ui.assistant.working_dir(self.conf), self.window.ask_scope.text()) def test_turkish_task_labels_and_runtime_status(self): i18n.set_language("tr") self.window.refresh() self.assertEqual(self.window.capture_status.text(), "Konuşmaya hazır") self.assertIn("Dikte:", self.window.capture_models.text()) self.assertIn("Temizleme:", self.window.capture_models.text()) def test_completed_run_refreshes_workspace_without_changing_controller_state(self): controller = Dikte.__new__(Dikte) controller.home_messages = {} controller.home_window = self.window controller._waiters = {} cfg.append_history({"text": "Finished"}) controller._settle("dictation", {"ok": True, "text": "Finished"}) self.assertEqual(self.window.latest_text.toPlainText(), "Finished") self.assertEqual(controller.home_messages["dictation"], "Transcript ready")