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