Update tests and test coverage

This commit is contained in:
Veronica Berglyd Olsen
2025-10-25 22:37:19 +02:00
parent 1938969a63
commit 2d67cb7dcc
12 changed files with 105 additions and 37 deletions
-6
View File
@@ -217,12 +217,6 @@ class NPushButton(QPushButton):
if icon: if icon:
self.refreshIcon() 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: def refreshIcon(self) -> None:
"""Reload the theme icon.""" """Reload the theme icon."""
if self._icon: if self._icon:
+2 -2
View File
@@ -34,7 +34,7 @@ from typing import TYPE_CHECKING, TypeVar
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QDesktopServices, QFont, QScreen 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.common import formatFileFilter
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
@@ -536,4 +536,4 @@ class _GuiAlert(QMessageBox):
def _onReject(self) -> None: def _onReject(self) -> None:
"""Process rejected state.""" """Process rejected state."""
self._state = False self._state = False
self.setResult(QDialog.DialogCode.Rejected) self.close()
+4 -1
View File
@@ -25,7 +25,10 @@ if __name__ == "__main__":
env["QT_SCALE_FACTOR"] = "1.0" env["QT_SCALE_FACTOR"] = "1.0"
if args.r or args.t or args.u: 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: else:
cmd = [sys.executable, "-m"] cmd = [sys.executable, "-m"]
+6 -2
View File
@@ -157,9 +157,11 @@ def projPath(fncPath):
def mockGUI(qtbot, monkeypatch): def mockGUI(qtbot, monkeypatch):
"""Create a mock instance of novelWriter's main GUI class.""" """Create a mock instance of novelWriter's main GUI class."""
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
from novelwriter.shared import _GuiAlert
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) 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() gui = MockGuiMain()
theme = GuiTheme() theme = GuiTheme()
monkeypatch.setattr(SHARED, "_gui", gui) monkeypatch.setattr(SHARED, "_gui", gui)
@@ -182,9 +184,11 @@ def nwGUI(qtbot, monkeypatch, functionFixture):
"""Create an instance of the novelWriter GUI.""" """Create an instance of the novelWriter GUI."""
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.shared import _GuiAlert
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) 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() CONFIG.loadConfig()
SHARED.initTheme(GuiTheme()) SHARED.initTheme(GuiTheme())
+6
View File
@@ -22,9 +22,12 @@ from __future__ import annotations
from unittest.mock import MagicMock from unittest.mock import MagicMock
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QFont, QIcon, QPixmap from PyQt6.QtGui import QFont, QIcon, QPixmap
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from novelwriter.extensions.modified import NPushButton
class MockGuiMain(QWidget): class MockGuiMain(QWidget):
@@ -72,6 +75,9 @@ class MockTheme:
def getHeaderDecoration(self, *a) -> QPixmap: def getHeaderDecoration(self, *a) -> QPixmap:
return QPixmap() return QPixmap()
def getStandardButton(self, *a) -> NPushButton:
return NPushButton(None, "", QSize(1, 1)) # type: ignore
def getIcon(self, *a) -> QIcon: def getIcon(self, *a) -> QIcon:
return QIcon() return QIcon()
+65 -5
View File
@@ -26,10 +26,10 @@ import pytest
from PyQt6.QtCore import QUrl from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices 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.core.project import NWProject
from novelwriter.shared import SharedData from novelwriter.shared import SharedData, _GuiAlert
from tests.mocked import MockGuiMain, MockTheme from tests.mocked import MockGuiMain, MockTheme
from tests.tools import buildTestProject from tests.tools import buildTestProject
@@ -143,10 +143,10 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog, mockGUI):
"""Test SharedData class alert helper functions.""" """Test SharedData class alert helper functions."""
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) monkeypatch.setattr(_GuiAlert, "finalState", True)
shared = SharedData() shared = SharedData()
@@ -188,3 +188,63 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog):
# Question box # Question box
assert shared.question("Why?") is True assert shared.question("Why?") is True
assert shared.lastAlert == "Why?" 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}<br><b>Exception</b>: {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
+3 -4
View File
@@ -25,13 +25,12 @@ from zipfile import ZipFile
import pytest import pytest
from PyQt6.QtWidgets import QMessageBox
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.project import NWProject, NWProjectState from novelwriter.core.project import NWProject, NWProjectState
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.shared import _GuiAlert
from tests.mocked import causeOSError from tests.mocked import causeOSError
from tests.tools import XML_IGNORE, C, buildTestProject, cmpFiles 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 # Won't convert legacy file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) 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 project.openProject(fncPath, clearLock=True) is False
assert "The file format of your project is about to be" in SHARED.lastAlert assert "The file format of your project is about to be" in SHARED.lastAlert
# Won't open project from newer version # Won't open project from newer version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) 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 project.openProject(fncPath, clearLock=True) is False
assert "This project was saved by a newer version" in SHARED.lastAlert assert "This project was saved by a newer version" in SHARED.lastAlert
+5 -4
View File
@@ -30,7 +30,7 @@ import pytest
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QInputDialog, QMessageBox from PyQt6.QtWidgets import QInputDialog
from novelwriter import CONFIG, SHARED, __hexversion__ from novelwriter import CONFIG, SHARED, __hexversion__
from novelwriter.common import jsonEncode from novelwriter.common import jsonEncode
@@ -42,6 +42,7 @@ from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.outline import GuiOutlineView
from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.shared import _GuiAlert
from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.welcome import GuiWelcome
from novelwriter.types import QtModCtrl, QtModShift from novelwriter.types import QtModCtrl, QtModShift
@@ -104,7 +105,7 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath):
# Check that closes can be blocked # Check that closes can be blocked
with monkeypatch.context() as mp: 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.openProject(projPath) is True
assert nwGUI.closeMain() is False assert nwGUI.closeMain() is False
nwGUI.closeProject() nwGUI.closeProject()
@@ -841,7 +842,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
# Block closing # Block closing
assert SHARED.hasProject is True assert SHARED.hasProject is True
with monkeypatch.context() as mp: 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 False
assert SHARED.hasProject is True assert SHARED.hasProject is True
@@ -854,7 +855,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
shutil.copyfile(lockBack, lockPath) shutil.copyfile(lockBack, lockPath)
with monkeypatch.context() as mp: 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 False
assert nwGUI.openProject(projPath) is True assert nwGUI.openProject(projPath) is True
+1 -3
View File
@@ -24,7 +24,7 @@ import sys
import pytest import pytest
from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox from PyQt6.QtWidgets import QApplication, QDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.about import GuiAbout 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): def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
"""Test loading the gui with a specific language.""" """Test loading the gui with a specific language."""
monkeypatch.setattr(QDialog, "exec", lambda *a: None) 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 # Set the test language
CONFIG.guiLocale = language CONFIG.guiLocale = language
+3 -2
View File
@@ -25,12 +25,13 @@ from unittest.mock import MagicMock
import pytest import pytest
from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock 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 import CONFIG, SHARED
from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode
from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.shared import _GuiAlert
from novelwriter.types import QtKeepAnchor, QtMoveRight, QtSelectWord from novelwriter.types import QtKeepAnchor, QtMoveRight, QtSelectWord
from tests.tools import C, buildTestProject, writeFile 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 # The document isn't empty, so the message box should pop
with monkeypatch.context() as mp: 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 not nwGUI.importDocument()
assert docEditor.getText() == "Bar" assert docEditor.getText() == "Bar"
+7 -6
View File
@@ -26,7 +26,7 @@ import pytest
from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF
from PyQt6.QtGui import QMouseEvent from PyQt6.QtGui import QMouseEvent
from PyQt6.QtWidgets import QMenu, QMessageBox from PyQt6.QtWidgets import QMenu
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docmerge import GuiDocMerge
@@ -34,6 +34,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.projtree import _TreeContextMenu from novelwriter.gui.projtree import _TreeContextMenu
from novelwriter.shared import _GuiAlert
from novelwriter.types import ( from novelwriter.types import (
QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected, QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected,
QtScrollAlwaysOff, QtScrollAsNeeded QtScrollAlwaysOff, QtScrollAsNeeded
@@ -627,7 +628,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m
# User can cancel move to trash # User can cancel move to trash
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) mp.setattr(_GuiAlert, "finalState", False)
projTree.processDeleteRequest(hScenes, askFirst=True) projTree.processDeleteRequest(hScenes, askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene", "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 # User can block permanent deletion
with monkeypatch.context() as mp: 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) projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene", "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 # Trash can be completely emptied, but user can block it
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) mp.setattr(_GuiAlert, "finalState", False)
projTree.emptyTrash() projTree.emptyTrash()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", "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 # Duplicate title page, but select no
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QtRejected) mp.setattr(_GuiAlert, "finalState", False)
projTree.duplicateFromHandle(C.hTitlePage) projTree.duplicateFromHandle(C.hTitlePage)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene", "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 # Click no on the dialog
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QtRejected) mp.setattr(_GuiAlert, "finalState", False)
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT) ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
assert nodeOne.item.isFolderType() assert nodeOne.item.isFolderType()
+3 -2
View File
@@ -26,13 +26,14 @@ import pytest
from PyQt6.QtCore import QUrl from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox from PyQt6.QtWidgets import QFileDialog, QListWidgetItem
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.buildsettings import BuildSettings
from novelwriter.enum import nwBuildFmt from novelwriter.enum import nwBuildFmt
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.shared import _GuiAlert
from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.types import QtDialogClose from novelwriter.types import QtDialogClose
@@ -134,7 +135,7 @@ def testToolManuscriptBuild_Main(
manus.buildPath.setText(str(fncPath)) manus.buildPath.setText(str(fncPath))
manus.buildName.setText("TestBuild") manus.buildName.setText("TestBuild")
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) mp.setattr(_GuiAlert, "finalState", False)
assert manus._runBuild() is False assert manus._runBuild() is False
# Test that the open button works # Test that the open button works