712 lines
25 KiB
Python
712 lines
25 KiB
Python
"""
|
|
Filename: main_unit_tests.py
|
|
Description: Unit tests for functionality validation
|
|
|
|
Author: Tyler de Zeeuw
|
|
License: GPL-3.0
|
|
"""
|
|
|
|
# Built-in imports
|
|
import os
|
|
import pickle
|
|
import configparser
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# External library imports
|
|
import pytest
|
|
from PySide6.QtWidgets import QApplication, QMenu
|
|
|
|
import main
|
|
from updater import LocalPendingUpdateCheckThread, UpdateCheckThread
|
|
|
|
'''
|
|
These test fluff currently. Very basic "Does the UI exist?" and not the functionality.
|
|
main_test.py::test_save_project_actions_pass_correct_ask_parameter
|
|
main_test.py::test_main_window_opens
|
|
main_test.py::test_file_recent_submenus_exist
|
|
main_test.py::test_view_reset_layout
|
|
main_test.py::test_preferences_actions[2D Data Bypass-2d_data_bypass]
|
|
main_test.py::test_preferences_actions[Incompatible Save Bypass-incompatible_save_bypass]
|
|
main_test.py::test_preferences_actions[Missing Events Bypass-missing_events_bypass]
|
|
main_test.py::test_preferences_actions[Analysis Clearing Bypass-analysis_clearing_bypass]
|
|
main_test.py::test_preferences_actions[Folding Bypass-folding_bypass]
|
|
main_test.py::test_preferences_actions[Show Advanced Parameters-advanced_parameters]
|
|
'''
|
|
|
|
# ---------------------- HELPERS ----------------------
|
|
def get_menu_by_title(menu_bar, title):
|
|
"""Return the first QMenu with the given title, or None."""
|
|
for menu in menu_bar.findChildren(QMenu):
|
|
if menu.title() == title:
|
|
return menu
|
|
return None
|
|
|
|
|
|
# ---------------------- FIXTURES ----------------------
|
|
@pytest.fixture(autouse=True)
|
|
def disable_updater_threads():
|
|
"""Stops updater threads from running asynchronously during qtbot teardown."""
|
|
with patch.object(UpdateCheckThread, "start", return_value=None), \
|
|
patch.object(LocalPendingUpdateCheckThread, "start", return_value=None):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_app_globals():
|
|
"""Initializes global configuration objects that main.py expects at runtime."""
|
|
main.cfg_path = os.path.join(os.getcwd(), f"{main.APP_NAME}.cfg")
|
|
main.file_cfg = configparser.ConfigParser()
|
|
main.ref_cfg = configparser.ConfigParser()
|
|
|
|
if hasattr(main, "config_init"):
|
|
main.config_init()
|
|
|
|
|
|
# ===================== FILE MENU =====================
|
|
def test_main_window_opens(qtbot):
|
|
"""Test 1: Verify MainApplication launches and becomes visible."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert window.isVisible()
|
|
|
|
|
|
def test_open_file_dialog_with_mne_mock(qtbot, tmp_path):
|
|
dummy_snirf = tmp_path / "test_data.snirf"
|
|
dummy_snirf.write_text("dummy content")
|
|
expected_path = os.path.normpath(str(dummy_snirf))
|
|
|
|
# Mock MNE Raw object returned by read_raw_snirf
|
|
mock_raw = MagicMock()
|
|
mock_raw.info = {"meas_date": "2026-01-01", "ch_names": ["S1_D1 760"], "dig": None}
|
|
mock_raw.ch_names = ["S1_D1 760"]
|
|
mock_raw.annotations = []
|
|
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch("PySide6.QtWidgets.QFileDialog.getOpenFileName", return_value=(expected_path, "SNIRF Files (*.snirf)")), \
|
|
patch("mne.io.snirf.read_raw_snirf", return_value=mock_raw), \
|
|
patch("project_manager.source_detector_distances", return_value=[0.03]):
|
|
|
|
window.project_manager.open_file_dialog()
|
|
|
|
assert expected_path in window.selected_paths
|
|
assert expected_path in window.bubble_widgets
|
|
|
|
window.files_are_dirty = False
|
|
window.is_saved = True
|
|
|
|
|
|
def test_open_folder_dialog(qtbot, tmp_path):
|
|
"""Verify that open_folder_dialog recursively finds and loads all .snirf files."""
|
|
sub_dir = tmp_path / "sub_folder"
|
|
sub_dir.mkdir()
|
|
|
|
file1 = tmp_path / "root_file.snirf"
|
|
file2 = sub_dir / "nested_file.snirf"
|
|
ignored_file = tmp_path / "notes.txt"
|
|
|
|
file1.write_text("dummy snirf 1")
|
|
file2.write_text("dummy snirf 2")
|
|
ignored_file.write_text("text note")
|
|
|
|
folder_path = str(tmp_path)
|
|
expected_paths = {
|
|
os.path.normpath(str(file1)),
|
|
os.path.normpath(str(file2)),
|
|
}
|
|
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch(
|
|
"PySide6.QtWidgets.QFileDialog.getExistingDirectory",
|
|
return_value=folder_path,
|
|
):
|
|
window.project_manager.open_folder_dialog()
|
|
|
|
loaded_paths = set(window.selected_paths)
|
|
assert expected_paths.issubset(loaded_paths)
|
|
assert os.path.normpath(str(ignored_file)) not in loaded_paths
|
|
|
|
window.files_are_dirty = False
|
|
window.is_saved = True
|
|
|
|
|
|
def test_load_project_dialog(qtbot, tmp_path):
|
|
"""Verify loading a valid pickled .flare project restores application state."""
|
|
project_file = tmp_path / "test_project.flare"
|
|
|
|
dummy_project_data = {
|
|
"version": "1.1.7",
|
|
"file_metadata": {"rel_sample.snirf": {"channels": 4}},
|
|
"file_parameters": {"rel_sample.snirf": {"AGE": "25", "SEX": "M", "HAND": "R", "GROUP": "A"}},
|
|
"roi_channel_map_dict": {},
|
|
"file_list": ["rel_sample.snirf"],
|
|
"progress_states": {"rel_sample.snirf": "completed"},
|
|
"current_ui_params": {},
|
|
}
|
|
|
|
with open(project_file, "wb") as f:
|
|
pickle.dump(dummy_project_data, f)
|
|
|
|
file_path = str(project_file)
|
|
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
if not hasattr(main, "DATA_SCHEMA"):
|
|
main.DATA_SCHEMA = []
|
|
|
|
with patch(
|
|
"PySide6.QtWidgets.QFileDialog.getOpenFileName",
|
|
return_value=(file_path, "FLARE Project (*.flare)"),
|
|
), patch("PySide6.QtWidgets.QMessageBox.information") as mock_info, patch.object(
|
|
window, "show_files_as_bubbles_from_list"
|
|
):
|
|
|
|
window.project_manager.load_project_dialog()
|
|
|
|
assert window.current_project_path == file_path
|
|
mock_info.assert_called_once()
|
|
|
|
|
|
def test_load_project_incompatible_version(qtbot, tmp_path):
|
|
"""Verify that a missing required key triggers an incompatibility error."""
|
|
invalid_file = tmp_path / "corrupt.flare"
|
|
|
|
incomplete_data = {
|
|
"file_metadata": {},
|
|
"file_parameters": {},
|
|
"roi_channel_map_dict": {},
|
|
}
|
|
|
|
with open(invalid_file, "wb") as f:
|
|
pickle.dump(incomplete_data, f)
|
|
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch("PySide6.QtWidgets.QMessageBox.critical") as mock_critical, \
|
|
patch("PySide6.QtWidgets.QMessageBox.warning") as mock_warning:
|
|
|
|
window.project_manager.load_project(str(invalid_file))
|
|
|
|
assert mock_critical.called or mock_warning.called, "Expected a QMessageBox warning or critical popup."
|
|
assert len(getattr(window, "selected_paths", [])) == 0
|
|
|
|
|
|
def test_save_project_no_data_shows_warning(qtbot):
|
|
"""Verify saving an empty project triggers a 'no data to save' warning."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch("PySide6.QtWidgets.QMessageBox.warning") as mock_warning:
|
|
window.project_manager.save_project(ask=True)
|
|
|
|
mock_warning.assert_called_once()
|
|
assert "no data" in mock_warning.call_args[0][2].lower()
|
|
|
|
|
|
def test_save_project_success(qtbot, tmp_path):
|
|
"""Verify saving a loaded project outputs a valid pickled .flare file."""
|
|
save_file_path = tmp_path / "test_project.flare"
|
|
dummy_snirf_path = str(tmp_path / "sample_subject.snirf")
|
|
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
# 1. Satisfy 'has_files' check
|
|
window.selected_paths = [dummy_snirf_path]
|
|
|
|
# 2. Add mock bubble widget so step 4 populates file_list
|
|
mock_bubble = MagicMock()
|
|
mock_bubble.file_path = dummy_snirf_path
|
|
mock_bubble.current_step = 0
|
|
window.bubble_widgets = {dummy_snirf_path: mock_bubble}
|
|
|
|
with patch("PySide6.QtWidgets.QFileDialog.getSaveFileName", return_value=(str(save_file_path), "FLARE Project (*.flare)")), \
|
|
patch("PySide6.QtWidgets.QMessageBox.information") as mock_info:
|
|
|
|
window.project_manager.save_project(ask=True)
|
|
|
|
# Wait for SaveProjectThread to finish writing to disk
|
|
qtbot.waitUntil(lambda: save_file_path.exists(), timeout=3000)
|
|
mock_info.assert_called_once()
|
|
|
|
# 3. Verify the saved payload structure
|
|
assert save_file_path.is_file()
|
|
with open(save_file_path, "rb") as f:
|
|
data = pickle.load(f)
|
|
assert "version" in data
|
|
# file_list contains relative paths normalized by sanitize()
|
|
assert "sample_subject.snirf" in data["file_list"]
|
|
|
|
# Reset dirty state so teardown completes cleanly
|
|
window.files_are_dirty = False
|
|
window.is_saved = True
|
|
|
|
|
|
def test_save_project_actions_pass_correct_ask_parameter(qtbot):
|
|
"""
|
|
Verify that the 'Save Project...' action calls save_project(ask=False)
|
|
and 'Save Project As...' calls save_project(ask=True).
|
|
"""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
file_menu = get_menu_by_title(window.menuBar(), "File")
|
|
assert file_menu is not None, "File menu not found"
|
|
|
|
save_action = next(a for a in file_menu.actions() if a.text() == "Save Project...")
|
|
save_as_action = next(a for a in file_menu.actions() if a.text() == "Save Project As...")
|
|
|
|
with patch.object(window.project_manager, 'save_project') as mock_save:
|
|
save_action.trigger()
|
|
mock_save.assert_called_once_with(ask=False)
|
|
mock_save.reset_mock()
|
|
|
|
save_as_action.trigger()
|
|
mock_save.assert_called_once_with(ask=True)
|
|
|
|
|
|
def test_file_exit(qtbot):
|
|
"""Verify that File → Exit calls QApplication.quit()."""
|
|
with patch.object(QApplication, 'quit') as mock_quit:
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
file_menu = get_menu_by_title(window.menuBar(), "File")
|
|
assert file_menu is not None, "File menu not found"
|
|
|
|
exit_action = next(a for a in file_menu.actions() if a.text() == "Exit")
|
|
exit_action.trigger()
|
|
mock_quit.assert_called_once()
|
|
|
|
|
|
def test_file_recent_submenus_exist(qtbot):
|
|
"""Verify that the 'Recent Files' and 'Recent Projects' submenus are created."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
file_menu = get_menu_by_title(window.menuBar(), "File")
|
|
assert file_menu is not None, "File menu not found"
|
|
|
|
recent_files_action = next((a for a in file_menu.actions() if a.text() == "Recent Files"), None)
|
|
assert recent_files_action is not None
|
|
recent_files_menu = recent_files_action.menu()
|
|
assert recent_files_menu is not None
|
|
|
|
recent_projects_action = next((a for a in file_menu.actions() if a.text() == "Recent Projects"), None)
|
|
assert recent_projects_action is not None
|
|
recent_projects_menu = recent_projects_action.menu()
|
|
assert recent_projects_menu is not None
|
|
|
|
|
|
# ===================== EDIT MENU =====================
|
|
def test_edit_cut(qtbot):
|
|
"""Verify Edit → Cut calls top_left_widget.cut()."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
edit_menu = get_menu_by_title(window.menuBar(), "Edit")
|
|
assert edit_menu is not None, "Edit menu not found"
|
|
|
|
cut_action = next(a for a in edit_menu.actions() if a.text() == "Cut")
|
|
with patch.object(window.top_left_widget, 'cut') as mock_cut:
|
|
cut_action.trigger()
|
|
mock_cut.assert_called_once()
|
|
|
|
|
|
def test_edit_copy(qtbot):
|
|
"""Verify Edit → Copy calls top_left_widget.copy()."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
edit_menu = get_menu_by_title(window.menuBar(), "Edit")
|
|
assert edit_menu is not None, "Edit menu not found"
|
|
|
|
copy_action = next(a for a in edit_menu.actions() if a.text() == "Copy")
|
|
with patch.object(window.top_left_widget, 'copy') as mock_copy:
|
|
copy_action.trigger()
|
|
mock_copy.assert_called_once()
|
|
|
|
|
|
def test_edit_paste(qtbot):
|
|
"""Verify Edit → Paste calls top_left_widget.paste()."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
edit_menu = get_menu_by_title(window.menuBar(), "Edit")
|
|
assert edit_menu is not None, "Edit menu not found"
|
|
|
|
paste_action = next(a for a in edit_menu.actions() if a.text() == "Paste")
|
|
with patch.object(window.top_left_widget, 'paste') as mock_paste:
|
|
paste_action.trigger()
|
|
mock_paste.assert_called_once()
|
|
|
|
|
|
# ===================== VIEW MENU =====================
|
|
def test_view_toggle_statusbar(qtbot):
|
|
"""Verify View → Toggle Status Bar toggles visibility and calls _update_config_setting."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
view_menu = get_menu_by_title(window.menuBar(), "View")
|
|
assert view_menu is not None, "View menu not found"
|
|
|
|
toggle_action = next(a for a in view_menu.actions() if a.text() == "Toggle Status Bar")
|
|
assert toggle_action.isCheckable() is True
|
|
|
|
# Initially checked (True in create_menu_bar)
|
|
assert toggle_action.isChecked() is True
|
|
assert window.statusbar.isVisible() is True
|
|
|
|
# Trigger once to hide
|
|
with patch.object(window, '_update_config_setting') as mock_update:
|
|
toggle_action.trigger()
|
|
assert not toggle_action.isChecked()
|
|
assert not window.statusbar.isVisible()
|
|
mock_update.assert_called_once_with("View", "status_bar", False)
|
|
|
|
# Trigger again to show
|
|
with patch.object(window, '_update_config_setting') as mock_update:
|
|
toggle_action.trigger()
|
|
assert toggle_action.isChecked() is True
|
|
assert window.statusbar.isVisible() is True
|
|
mock_update.assert_called_once_with("View", "status_bar", True)
|
|
|
|
|
|
def test_view_reset_layout(qtbot):
|
|
"""Verify View → Reset Window Layout calls apply_splitter_ratios."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
view_menu = get_menu_by_title(window.menuBar(), "View")
|
|
assert view_menu is not None, "View menu not found"
|
|
|
|
reset_action = next(a for a in view_menu.actions() if a.text() == "Reset Window Layout")
|
|
with patch.object(window, 'apply_splitter_ratios') as mock_apply:
|
|
reset_action.trigger()
|
|
mock_apply.assert_called_once()
|
|
|
|
|
|
# ===================== OPTIONS MENU =====================
|
|
def test_about_window_opens(qtbot):
|
|
"""Verify AboutWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "about", None) is None
|
|
window.about_window()
|
|
|
|
assert window.about is not None
|
|
assert window.about.isVisible() is True
|
|
|
|
first_instance = window.about
|
|
window.about_window()
|
|
assert window.about is first_instance
|
|
|
|
|
|
def test_user_guide_window_opens(qtbot):
|
|
"""Verify UserGuideWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "help", None) is None
|
|
window.user_guide()
|
|
|
|
assert window.help is not None
|
|
assert window.help.isVisible() is True
|
|
|
|
first_instance = window.help
|
|
window.user_guide()
|
|
assert window.help is first_instance
|
|
|
|
|
|
def test_show_update_changelog(qtbot):
|
|
"""Verify WelcomeDialog is instantiated and shown."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch.object(main, "WelcomeDialog") as mock_dialog_cls:
|
|
mock_dialog_instance = MagicMock()
|
|
mock_dialog_cls.return_value = mock_dialog_instance
|
|
|
|
window.show_update_changelog()
|
|
|
|
mock_dialog_cls.assert_called_once_with(window, direct=False)
|
|
mock_dialog_instance.show.assert_called_once()
|
|
|
|
|
|
def test_group_metadata_no_data_shows_msgbox(qtbot):
|
|
"""Verify group_metadata triggers an information QMessageBox when file_metadata is empty."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
window.file_metadata = {}
|
|
|
|
with patch("PySide6.QtWidgets.QMessageBox.information") as mock_msgbox:
|
|
window.group_metadata()
|
|
|
|
mock_msgbox.assert_called_once()
|
|
assert "No Data" in mock_msgbox.call_args[0]
|
|
|
|
|
|
def test_group_metadata_with_data_applies_mappings(qtbot):
|
|
"""Verify group_metadata opens GroupAssignmentDialog and executes _apply_group_mappings on success."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
window.file_metadata = {"sub-01.snirf": {"age": "25"}}
|
|
mock_result = ("Age", {"sub-01.snirf": "GroupA"})
|
|
|
|
with patch.object(main.GroupAssignmentDialog, "run", return_value=mock_result), \
|
|
patch.object(window, "_apply_group_mappings") as mock_apply:
|
|
|
|
window.group_metadata()
|
|
|
|
mock_apply.assert_called_once_with({"sub-01.snirf": "GroupA"}, field_name="Age")
|
|
|
|
|
|
def test_manual_check_for_updates(qtbot):
|
|
"""Verify Options → Check for Updates triggers the updater method."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
options_menu = get_menu_by_title(window.menuBar(), "Options")
|
|
assert options_menu is not None, "Options menu not found"
|
|
|
|
update_action = next(a for a in options_menu.actions() if a.text() == "Check for Updates")
|
|
assert update_action is not None
|
|
assert update_action.isEnabled() is True
|
|
|
|
# Patch the updater's manual_check_for_updates method
|
|
with patch.object(window.updater, 'manual_check_for_updates') as mock_method:
|
|
update_action.trigger()
|
|
mock_method.assert_called_once()
|
|
|
|
|
|
def test_update_optode_positions_opens(qtbot):
|
|
"""Verify UpdateOptodesWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "optodes", None) is None
|
|
window.update_optode_positions()
|
|
|
|
assert window.optodes is not None
|
|
assert window.optodes.isVisible() is True
|
|
|
|
first_instance = window.optodes
|
|
window.update_optode_positions()
|
|
assert window.optodes is first_instance
|
|
|
|
|
|
def test_update_event_markers_opens(qtbot):
|
|
"""Verify UpdateEventsWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "events", None) is None
|
|
window.update_event_markers()
|
|
|
|
assert window.events is not None
|
|
assert window.events.isVisible() is True
|
|
|
|
first_instance = window.events
|
|
window.update_event_markers()
|
|
assert window.events is first_instance
|
|
|
|
|
|
def test_update_event_markers_blazes_opens(qtbot):
|
|
"""Verify UpdateEventsBlazesWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "events_blazes", None) is None
|
|
window.update_event_markers_blazes()
|
|
|
|
assert window.events_blazes is not None
|
|
assert window.events_blazes.isVisible() is True
|
|
|
|
first_instance = window.events_blazes
|
|
window.update_event_markers_blazes()
|
|
assert window.events_blazes is first_instance
|
|
|
|
|
|
def test_reset_to_default_configuration_user_cancels(qtbot):
|
|
"""Verify nothing is reset when the user clicks 'No' on the prompt."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch("main.QMessageBox.question", return_value=main.QMessageBox.StandardButton.No), \
|
|
patch("main.open") as mock_open, \
|
|
patch.object(window, "sync_app_with_config") as mock_sync:
|
|
|
|
window.reset_to_default_configuration()
|
|
|
|
mock_open.assert_not_called()
|
|
mock_sync.assert_not_called()
|
|
|
|
|
|
def test_reset_to_default_configuration_success(qtbot):
|
|
"""Verify file write, widget resets, config sync, and singleShot timer call when confirmed."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
# Mock child ParamSection widgets
|
|
mock_section1 = MagicMock()
|
|
mock_section2 = MagicMock()
|
|
|
|
with patch("main.QMessageBox.question", return_value=main.QMessageBox.StandardButton.Yes), \
|
|
patch("main.open") as mock_open, \
|
|
patch("main.file_cfg") as mock_cfg, \
|
|
patch.object(window, "findChildren", return_value=[mock_section1, mock_section2]), \
|
|
patch.object(window, "sync_app_with_config") as mock_sync, \
|
|
patch.object(window, "update_sections") as mock_update, \
|
|
patch("main.QTimer.singleShot") as mock_timer:
|
|
|
|
window.reset_to_default_configuration()
|
|
|
|
# Check file overwrite and parser reload
|
|
mock_open.assert_called_once()
|
|
mock_cfg.read.assert_called_once_with(main.cfg_path)
|
|
|
|
# Check section UI resets and app syncing
|
|
mock_section1.reset_to_defaults.assert_called_once()
|
|
mock_section2.reset_to_defaults.assert_called_once()
|
|
mock_sync.assert_called_once()
|
|
mock_update.assert_called_once_with(0)
|
|
|
|
# Verify post-reset dialog singleShot queue
|
|
mock_timer.assert_called_once_with(100, window._show_reset_success_dialog)
|
|
|
|
|
|
def test_reset_to_default_configuration_file_error_fallback(qtbot):
|
|
"""Verify fallback to in-memory read_string when file writing raises an Exception."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
with patch("main.QMessageBox.question", return_value=main.QMessageBox.StandardButton.Yes), \
|
|
patch("main.open", side_effect=PermissionError("Access denied")), \
|
|
patch("main.file_cfg") as mock_cfg, \
|
|
patch.object(window, "sync_app_with_config"), \
|
|
patch.object(window, "update_sections"), \
|
|
patch("main.QTimer.singleShot"):
|
|
|
|
window.reset_to_default_configuration()
|
|
|
|
# Verify fallback read_string execution
|
|
mock_cfg.read_string.assert_called_once_with(main.DEFAULT_CONFIG)
|
|
|
|
|
|
def test_show_reset_success_dialog(qtbot):
|
|
"""Verify success dialog pops up and statusbar updates."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
window.statusbar = MagicMock()
|
|
|
|
with patch("main.QMessageBox.information") as mock_info:
|
|
window._show_reset_success_dialog()
|
|
|
|
mock_info.assert_called_once_with(
|
|
window,
|
|
"Reset Successful",
|
|
"All application settings have been successfully restored to their default values."
|
|
)
|
|
window.statusbar.showMessage.assert_called_once_with(
|
|
"All settings have been reset to their default values.", 5000
|
|
)
|
|
|
|
|
|
# ===================== PREFERENCES MENU =====================
|
|
@pytest.mark.parametrize("action_text, config_key", [
|
|
("2D Data Bypass", "2d_data_bypass"),
|
|
("Incompatible Save Bypass", "incompatible_save_bypass"),
|
|
("Missing Events Bypass", "missing_events_bypass"),
|
|
("Analysis Clearing Bypass", "analysis_clearing_bypass"),
|
|
("Folding Bypass", "folding_bypass"),
|
|
("Show Advanced Parameters", "advanced_parameters"),
|
|
])
|
|
def test_preferences_actions(qtbot, action_text, config_key):
|
|
"""
|
|
Verify each Preferences action toggles checked state and updates config.
|
|
Uses the current checked state as a starting point and verifies toggling.
|
|
"""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
pref_menu = get_menu_by_title(window.menuBar(), "Preferences")
|
|
assert pref_menu is not None, "Preferences menu not found"
|
|
|
|
action = next(a for a in pref_menu.actions() if a.text() == action_text)
|
|
assert action.isCheckable() is True
|
|
|
|
# Record the initial state
|
|
initial_checked = action.isChecked()
|
|
|
|
# Trigger once → state should toggle
|
|
with patch.object(window, '_update_config_setting') as mock_update:
|
|
action.trigger()
|
|
assert action.isChecked() == (not initial_checked)
|
|
mock_update.assert_called_once_with("Preferences", config_key, not initial_checked)
|
|
|
|
# Trigger again → should toggle back to initial
|
|
with patch.object(window, '_update_config_setting') as mock_update:
|
|
action.trigger()
|
|
assert action.isChecked() == initial_checked
|
|
mock_update.assert_called_once_with("Preferences", config_key, initial_checked)
|
|
|
|
|
|
# ===================== TERMINAL MENU =====================
|
|
def test_terminal_gui_opens(qtbot):
|
|
"""Verify TerminalWindow opens and prevents duplicate instances."""
|
|
window = main.MainApplication()
|
|
qtbot.addWidget(window)
|
|
window.show()
|
|
|
|
assert getattr(window, "terminal", None) is None
|
|
window.terminal_gui()
|
|
|
|
assert window.terminal is not None
|
|
assert window.terminal.isVisible() is True
|
|
|
|
first_instance = window.terminal
|
|
window.terminal_gui()
|
|
assert window.terminal is first_instance
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |