From 4cadd1597e853fed9c3f31a04cb9533d2239c866 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:07:32 +0100 Subject: [PATCH] Add coverage to remaining missed lines --- novelwriter/extensions/configlayout.py | 4 +- novelwriter/gui/itemdetails.py | 18 +----- novelwriter/gui/projtree.py | 17 +++--- tests/test_gui/test_gui_doceditor.py | 81 ++++++++++++++++++++++++-- tests/test_gui/test_gui_docviewer.py | 80 ++++++++++++++++++++++++- tests/test_gui/test_gui_guimain.py | 11 ++++ tests/test_gui/test_gui_projtree.py | 18 +++++- tests/test_gui/test_gui_statusbar.py | 14 +++-- 8 files changed, 199 insertions(+), 44 deletions(-) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 2bd48032..b6ef1951 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -183,7 +183,7 @@ class NScrollableForm(QScrollArea): def addRow( self, label: str | None, - widget: QWidget | list[QWidget | QPixmap | str | int], + widget: QWidget | list[QWidget | QPixmap | int], helpText: str = "", unit: str | None = None, button: QWidget | None = None, @@ -204,8 +204,6 @@ class NScrollableForm(QScrollArea): icon = QLabel(self) icon.setPixmap(item) wBox.addWidget(icon) - elif isinstance(item, str): - wBox.addWidget(QLabel(item, self)) elif isinstance(item, int): wBox.addSpacing(CONFIG.pxInt(item)) qWidget = QWidget(self) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 2a90c7b5..77df5acf 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -225,12 +225,7 @@ class GuiItemDetails(QWidget): def updateViewBox(self, tHandle: str | None) -> None: """Populate the details box from a given handle.""" - if tHandle is None: - self.clearDetails() - return - - nwItem = SHARED.project.tree[tHandle] - if nwItem is None: + if not (tHandle and (nwItem := SHARED.project.tree[tHandle])): self.clearDetails() return @@ -297,14 +292,3 @@ class GuiItemDetails(QWidget): elif change == nwChange.DELETE: self.updateViewBox(None) return - - @pyqtSlot(str, int, int, int) - def updateCounts(self, tHandle: str, cC: int, wC: int, pC: int) -> None: - """Update the counts if the handle is the same as the one we're - already showing. Otherwise, do nothing. - """ - if tHandle == self._handle: - self.cCountData.setText(f"{cC:n}") - self.wCountData.setText(f"{wC:n}") - self.pCountData.setText(f"{pC:n}") - return diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index d9ca56a6..2ca5eaf9 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -213,7 +213,7 @@ class GuiProjectView(QWidget): @pyqtSlot(str, Enum) def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: """Refresh other content when project item changed.""" - self.projBar.processTemplateDocuments(tHandle, change) + self.projBar.processTemplateDocuments(tHandle) return @pyqtSlot(str) @@ -404,17 +404,16 @@ class GuiProjectToolBar(QWidget): """Build the templates menu.""" for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE): for dHandle in SHARED.project.tree.subTree(tHandle): - self.processTemplateDocuments(dHandle, nwChange.CREATE) + self.processTemplateDocuments(dHandle) return - def processTemplateDocuments(self, tHandle: str, change: nwChange) -> None: + def processTemplateDocuments(self, tHandle: str) -> None: """Process change in tree items to update menu content.""" - if change in (nwChange.CREATE, nwChange.UPDATE): - if item := SHARED.project.tree[tHandle]: - if item.isTemplateFile() and item.isActive: - self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon()) - elif tHandle in self.mTemplates: - self.mTemplates.remove(tHandle) + if item := SHARED.project.tree[tHandle]: + if item.isTemplateFile() and item.isActive: + self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon()) + elif tHandle in self.mTemplates: + self.mTemplates.remove(tHandle) return ## diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index b9bdb26f..fd39e5f8 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -24,22 +24,23 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, Qt, QThreadPool, QUrl +from PyQt5.QtCore import QEvent, QMimeData, Qt, QThreadPool, QUrl from PyQt5.QtGui import ( - QClipboard, QDesktopServices, QFont, QMouseEvent, QTextBlock, QTextCursor, - QTextOption + QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, + QFont, QMouseEvent, QTextBlock, QTextCursor, QTextOption ) -from PyQt5.QtWidgets import QAction, QApplication, QMenu +from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit from novelwriter import CONFIG, SHARED +from novelwriter.common import decodeMimeHandles from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.text.counting import standardCounter from novelwriter.types import ( - QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtMouseLeft, - QtMoveAnchor, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded + QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone, + QtMouseLeft, QtMoveAnchor, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded ) from tests.mocked import causeOSError @@ -209,6 +210,74 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex # qtbot.stop() +@pytest.mark.gui +def testGuiEditor_DragAndDrop(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test drag and drop in the editor.""" + docEditor = nwGUI.docEditor + + buildTestProject(nwGUI, projPath) + assert nwGUI.openDocument(C.hTitlePage) is True + assert docEditor.docHandle == C.hTitlePage + + middle = docEditor.viewport().rect().center() + action = Qt.DropAction.MoveAction + mouse = Qt.MouseButton.NoButton + + model = SHARED.project.tree.model + docMime = model.mimeData([model.indexFromHandle(C.hSceneDoc)]) + noneMime = QMimeData() + noneMime.setData("plain/text", b"") + assert decodeMimeHandles(docMime) == [C.hSceneDoc] + + # Drag Enter + mockEnter = MagicMock() + docEvent = QDragEnterEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDragEnterEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QPlainTextEdit, "dragEnterEvent", mockEnter) + + # Document Enter + docEditor.dragEnterEvent(docEvent) + assert docEvent.isAccepted() is True + assert mockEnter.call_count == 0 + + # Regular Enter + docEditor.dragEnterEvent(noneEvent) + assert mockEnter.call_count == 1 + + # Drag Move + mockMove = MagicMock() + docEvent = QDragMoveEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDragMoveEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QPlainTextEdit, "dragMoveEvent", mockMove) + + # Document Move + docEditor.dragMoveEvent(docEvent) + assert docEvent.isAccepted() is True + assert mockMove.call_count == 0 + + # Regular Move + docEditor.dragMoveEvent(noneEvent) + assert mockMove.call_count == 1 + + # Drop + mockDrop = MagicMock() + docEvent = QDropEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDropEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QPlainTextEdit, "dropEvent", mockDrop) + + # Document Drop + docEditor.dropEvent(docEvent) + assert mockDrop.call_count == 0 + assert docEditor.docHandle == C.hSceneDoc + + # Regular Move + docEditor.dropEvent(noneEvent) + assert mockDrop.call_count == 1 + + @pytest.mark.gui def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): """Test extracting various meta data and other values.""" diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index c312f5ac..41847348 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -24,16 +24,21 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, QPoint, Qt, QUrl -from PyQt5.QtGui import QDesktopServices, QMouseEvent, QTextCursor -from PyQt5.QtWidgets import QAction, QApplication, QMenu +from PyQt5.QtCore import QEvent, QMimeData, QPoint, Qt, QUrl +from PyQt5.QtGui import ( + QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, + QTextCursor +) +from PyQt5.QtWidgets import QAction, QApplication, QMenu, QTextBrowser from novelwriter import CONFIG, SHARED +from novelwriter.common import decodeMimeHandles from novelwriter.enum import nwChange, nwDocAction from novelwriter.formats.toqdoc import ToQTextDocument from novelwriter.types import QtModNone, QtMouseLeft from tests.mocked import causeException +from tests.tools import C, buildTestProject @pytest.mark.gui @@ -238,3 +243,72 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): docViewer.updateTheme() # qtbot.stop() + + +@pytest.mark.gui +def testGuiViewer_DragAndDrop(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test drag and drop in the viewer.""" + docViewer = nwGUI.docViewer + + buildTestProject(nwGUI, projPath) + assert nwGUI.openDocument(C.hTitlePage) is True + assert nwGUI.viewDocument(C.hTitlePage) is True + assert docViewer.docHandle == C.hTitlePage + + middle = docViewer.viewport().rect().center() + action = Qt.DropAction.MoveAction + mouse = Qt.MouseButton.NoButton + + model = SHARED.project.tree.model + docMime = model.mimeData([model.indexFromHandle(C.hSceneDoc)]) + noneMime = QMimeData() + noneMime.setData("plain/text", b"") + assert decodeMimeHandles(docMime) == [C.hSceneDoc] + + # Drag Enter + mockEnter = MagicMock() + docEvent = QDragEnterEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDragEnterEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QTextBrowser, "dragEnterEvent", mockEnter) + + # Document Enter + docViewer.dragEnterEvent(docEvent) + assert docEvent.isAccepted() is True + assert mockEnter.call_count == 0 + + # Regular Enter + docViewer.dragEnterEvent(noneEvent) + assert mockEnter.call_count == 1 + + # Drag Move + mockMove = MagicMock() + docEvent = QDragMoveEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDragMoveEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QTextBrowser, "dragMoveEvent", mockMove) + + # Document Move + docViewer.dragMoveEvent(docEvent) + assert docEvent.isAccepted() is True + assert mockMove.call_count == 0 + + # Regular Move + docViewer.dragMoveEvent(noneEvent) + assert mockMove.call_count == 1 + + # Drop + mockDrop = MagicMock() + docEvent = QDropEvent(middle, action, docMime, mouse, QtModNone) + noneEvent = QDropEvent(middle, action, noneMime, mouse, QtModNone) + with monkeypatch.context() as mp: + mp.setattr(QTextBrowser, "dropEvent", mockDrop) + + # Document Drop + docViewer.dropEvent(docEvent) + assert mockDrop.call_count == 0 + assert docViewer.docHandle == C.hSceneDoc + + # Regular Move + docViewer.dropEvent(noneEvent) + assert mockDrop.call_count == 1 diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 7ca32b8b..9668185f 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -702,6 +702,17 @@ def testGuiMain_Viewing(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.viewDocument(C.hSceneDoc) assert nwGUI.docViewer.toPlainText() == "New Scene\nWith some stuff in it!" + # Open with keypress + nwGUI.closeDocViewer() + assert nwGUI.docViewer.docHandle is None + nwGUI.projView.setSelectedHandle(C.hSceneDoc) + with monkeypatch.context() as mp: + mp.setattr(nwGUI.projView.projTree, "hasFocus", lambda *a: True) + qtbot.keyClick( + nwGUI.projView.projTree, Qt.Key.Key_Return, modifier=QtModShift, delay=KEY_DELAY + ) + assert nwGUI.docViewer.docHandle == C.hSceneDoc + # qtbot.stop() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 3d5ede31..0a073aae 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -663,12 +663,22 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m "Scene 3", "Scene 4", ] + # Opening documents before delete, will close them + nwGUI.openDocument(hScenes[2]) + nwGUI.viewDocument(hScenes[3]) + + assert nwGUI.docEditor.docHandle == hScenes[2] + assert nwGUI.docViewer.docHandle == hScenes[3] + # Trash can be completely emptied projTree.emptyTrash() assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", ] + assert nwGUI.docEditor.docHandle is None + assert nwGUI.docViewer.docHandle is None + # Emptying empty trash pops an alert projTree.emptyTrash() assert SHARED.lastAlert == "The Trash folder is already empty." @@ -1423,7 +1433,13 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert nwNewCharacter.itemName == "Note" assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n" - # Remove the templates + # Clearing the menu and rebuilding it should work + projBar.mTemplates.clearMenu() + assert len(projBar.mTemplates.actions()) == 0 + projBar.buildTemplatesMenu() + assert len(projBar.mTemplates.actions()) == 2 + + # Remove the note template assert projBar.mTemplates.menuAction().isVisible() is True assert len(projBar.mTemplates.actions()) == 2 assert trash.childCount() == 0 diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 624f1f84..71d0d0d1 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject @pytest.mark.gui -def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): +def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the the various features of the status bar.""" buildTestProject(nwGUI, projPath) cHandle = SHARED.project.newFile("A Note", C.hCharRoot) @@ -89,9 +89,13 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): nwGUI._lastTotalCount = 0 nwGUI._updateStatusWordCount() assert nwGUI.mainStatus.statsText.text() == "Words: 9 (+9)" - CONFIG.incNotesWCount = True - nwGUI._lastTotalCount = 0 - nwGUI._updateStatusWordCount() - assert nwGUI.mainStatus.statsText.text() == "Words: 11 (+11)" + + # Update again, but through time tick + with monkeypatch.context() as mp: + mp.setattr("novelwriter.guimain.time", lambda *a: 50.0) + CONFIG.incNotesWCount = True + nwGUI._lastTotalCount = 0 + nwGUI._timeTick() + assert nwGUI.mainStatus.statsText.text() == "Words: 11 (+11)" # qtbot.stop()