diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 7e245d21..b802a4d9 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -217,12 +217,6 @@ class NPushButton(QPushButton): if icon: self.refreshIcon() - def setThemeIcon(self, icon: str, color: str | None = None) -> None: - """Set an icon from the current theme.""" - self._icon = icon - self._color = color - self.refreshIcon() - def refreshIcon(self) -> None: """Reload the theme icon.""" if self._icon: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index be439b48..b76ad165 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -34,7 +34,7 @@ from typing import TYPE_CHECKING, TypeVar from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt6.QtGui import QDesktopServices, QFont, QScreen -from PyQt6.QtWidgets import QApplication, QDialog, QFileDialog, QFontDialog, QMessageBox, QWidget +from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter from novelwriter.constants import nwFiles @@ -536,4 +536,4 @@ class _GuiAlert(QMessageBox): def _onReject(self) -> None: """Process rejected state.""" self._state = False - self.setResult(QDialog.DialogCode.Rejected) + self.close() diff --git a/run_tests.py b/run_tests.py index 18e1eeae..6f593127 100755 --- a/run_tests.py +++ b/run_tests.py @@ -25,7 +25,10 @@ if __name__ == "__main__": env["QT_SCALE_FACTOR"] = "1.0" if args.r or args.t or args.u: - cmd = ["coverage", "run", "-m"] + cmd = ["coverage", "run"] + if args.lf or args.sw: + cmd += ["--append"] + cmd += ["-m"] else: cmd = [sys.executable, "-m"] diff --git a/tests/conftest.py b/tests/conftest.py index 765755bf..f8e3a5eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,9 +157,11 @@ def projPath(fncPath): def mockGUI(qtbot, monkeypatch): """Create a mock instance of novelWriter's main GUI class.""" from novelwriter.gui.theme import GuiTheme + from novelwriter.shared import _GuiAlert monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) gui = MockGuiMain() theme = GuiTheme() monkeypatch.setattr(SHARED, "_gui", gui) @@ -182,9 +184,11 @@ def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" from novelwriter.gui.theme import GuiTheme from novelwriter.guimain import GuiMain + from novelwriter.shared import _GuiAlert monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) CONFIG.loadConfig() SHARED.initTheme(GuiTheme()) diff --git a/tests/mocked.py b/tests/mocked.py index f86d5be0..92b56abe 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -22,9 +22,12 @@ from __future__ import annotations from unittest.mock import MagicMock +from PyQt6.QtCore import QSize from PyQt6.QtGui import QFont, QIcon, QPixmap from PyQt6.QtWidgets import QWidget +from novelwriter.extensions.modified import NPushButton + class MockGuiMain(QWidget): @@ -72,6 +75,9 @@ class MockTheme: def getHeaderDecoration(self, *a) -> QPixmap: return QPixmap() + def getStandardButton(self, *a) -> NPushButton: + return NPushButton(None, "", QSize(1, 1)) # type: ignore + def getIcon(self, *a) -> QIcon: return QIcon() diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 64cc306b..a020a694 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -26,10 +26,10 @@ import pytest from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices -from PyQt6.QtWidgets import QFileDialog, QMessageBox, QWidget +from PyQt6.QtWidgets import QFileDialog, QWidget from novelwriter.core.project import NWProject -from novelwriter.shared import SharedData +from novelwriter.shared import SharedData, _GuiAlert from tests.mocked import MockGuiMain, MockTheme from tests.tools import buildTestProject @@ -143,10 +143,10 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): @pytest.mark.base -def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): +def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog, mockGUI): """Test SharedData class alert helper functions.""" - monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) shared = SharedData() @@ -188,3 +188,63 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): # Question box assert shared.question("Why?") is True assert shared.lastAlert == "Why?" + + +@pytest.mark.base +def testBaseSharedData_GuiAlert(): + """Test the _GuiAlert class.""" + alert = _GuiAlert(None, MockTheme()) # type: ignore + + # Default states + assert alert.logMessage == "" + assert alert.finalState is False + + # Populate message + text = "one" + info = "two" + details = "three" + alert.setMessage(text, info, details) + assert alert.logMessage == f"{text} {info} {details}" + assert alert.text() == text + assert alert.informativeText() == info + assert alert.detailedText() == details + + # Populate exception + exc = Exception("oops") + alert.setException(exc) + assert alert.logMessage == f"{text} {info} {details}" + assert alert.informativeText() == f"{info}
Exception: {exc!s}" + + # Alert: Info + alert.setAlertType(_GuiAlert.INFO, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Information" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Warning + alert.setAlertType(_GuiAlert.WARN, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Warning" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Error + alert.setAlertType(_GuiAlert.ERROR, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Error" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Question + alert.setAlertType(_GuiAlert.ASK, True) + assert hasattr(alert, "_btnYes") + assert hasattr(alert, "_btnNo") + assert alert.windowTitle() == "Question" + alert._btnYes.click() + assert alert.finalState is True + alert._btnNo.click() + assert alert.finalState is False diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index cdb202c1..799cd423 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -25,13 +25,12 @@ from zipfile import ZipFile import pytest -from PyQt6.QtWidgets import QMessageBox - from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles from novelwriter.core.project import NWProject, NWProjectState from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.enum import nwItemClass +from novelwriter.shared import _GuiAlert from tests.mocked import causeOSError from tests.tools import XML_IGNORE, C, buildTestProject, cmpFiles @@ -274,14 +273,14 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): # Won't convert legacy file with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert project.openProject(fncPath, clearLock=True) is False assert "The file format of your project is about to be" in SHARED.lastAlert # Won't open project from newer version with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert project.openProject(fncPath, clearLock=True) is False assert "This project was saved by a newer version" in SHARED.lastAlert diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 0e15b71e..4cfa3a91 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -30,7 +30,7 @@ import pytest from PyQt6.QtCore import Qt from PyQt6.QtGui import QPalette -from PyQt6.QtWidgets import QInputDialog, QMessageBox +from PyQt6.QtWidgets import QInputDialog from novelwriter import CONFIG, SHARED, __hexversion__ from novelwriter.common import jsonEncode @@ -42,6 +42,7 @@ from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.shared import _GuiAlert from novelwriter.tools.welcome import GuiWelcome from novelwriter.types import QtModCtrl, QtModShift @@ -104,7 +105,7 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath): # Check that closes can be blocked with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is True assert nwGUI.closeMain() is False nwGUI.closeProject() @@ -841,7 +842,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) # Block closing assert SHARED.hasProject is True with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is False assert SHARED.hasProject is True @@ -854,7 +855,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) shutil.copyfile(lockBack, lockPath) with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is False assert nwGUI.openProject(projPath) is True diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index 915d83a4..e482d85f 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -24,7 +24,7 @@ import sys import pytest -from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt6.QtWidgets import QApplication, QDialog from novelwriter import CONFIG, SHARED from novelwriter.dialogs.about import GuiAbout @@ -49,8 +49,6 @@ LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW) def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath): """Test loading the gui with a specific language.""" monkeypatch.setattr(QDialog, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) # Set the test language CONFIG.guiLocale = language diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 0d6344bc..e599e9f9 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -25,12 +25,13 @@ from unittest.mock import MagicMock import pytest from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock -from PyQt6.QtWidgets import QFileDialog, QMessageBox +from PyQt6.QtWidgets import QFileDialog from novelwriter import CONFIG, SHARED from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.gui.doceditor import GuiDocEditor +from novelwriter.shared import _GuiAlert from novelwriter.types import QtKeepAnchor, QtMoveRight, QtSelectWord from tests.tools import C, buildTestProject, writeFile @@ -575,7 +576,7 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd # The document isn't empty, so the message box should pop with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert not nwGUI.importDocument() assert docEditor.getText() == "Bar" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index f6430f55..bdd50ad0 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ import pytest from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF from PyQt6.QtGui import QMouseEvent -from PyQt6.QtWidgets import QMenu, QMessageBox +from PyQt6.QtWidgets import QMenu from novelwriter import CONFIG, SHARED from novelwriter.dialogs.docmerge import GuiDocMerge @@ -34,6 +34,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.gui.projtree import _TreeContextMenu +from novelwriter.shared import _GuiAlert from novelwriter.types import ( QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected, QtScrollAlwaysOff, QtScrollAsNeeded @@ -627,7 +628,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # User can cancel move to trash with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.processDeleteRequest(hScenes, askFirst=True) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -645,7 +646,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # User can block permanent deletion with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.processDeleteRequest(hScenes[0:2], askFirst=True) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -677,7 +678,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # Trash can be completely emptied, but user can block it with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.emptyTrash() assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", @@ -995,7 +996,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Duplicate title page, but select no with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QtRejected) + mp.setattr(_GuiAlert, "finalState", False) projTree.duplicateFromHandle(C.hTitlePage) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -1302,7 +1303,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Click no on the dialog with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QtRejected) + mp.setattr(_GuiAlert, "finalState", False) ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT) assert nodeOne.item.isFolderType() diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 91924120..35ddb7fa 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -26,13 +26,14 @@ import pytest from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices -from PyQt6.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox +from PyQt6.QtWidgets import QFileDialog, QListWidgetItem from pytestqt.qtbot import QtBot from novelwriter.constants import nwLabels from novelwriter.core.buildsettings import BuildSettings from novelwriter.enum import nwBuildFmt from novelwriter.guimain import GuiMain +from novelwriter.shared import _GuiAlert from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.types import QtDialogClose @@ -134,7 +135,7 @@ def testToolManuscriptBuild_Main( manus.buildPath.setText(str(fncPath)) manus.buildName.setText("TestBuild") with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert manus._runBuild() is False # Test that the open button works