From 0449cf824d8df17f7b33e20b77a6dc9003842809 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:39:23 +0100 Subject: [PATCH] Add test coverage of the gui project tree --- novelwriter/core/itemmodel.py | 8 + novelwriter/gui/projtree.py | 50 +- novelwriter/tools/manussettings.py | 3 +- .../guiEditor_Main_Final_nwProject.nwx | 20 +- tests/test_core/test_core_itemmodel.py | 10 + tests/test_gui/test_gui_projtree.py | 1784 ++++++++--------- 6 files changed, 910 insertions(+), 965 deletions(-) diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index c83e7907..df4d33c1 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -240,6 +240,14 @@ class ProjectNode: self._refreshChildrenPos() return + def setExpanded(self, state: bool) -> None: + """Set the node's expanded state.""" + if state and self._children: + self._item.setExpanded(True) + else: + self._item.setExpanded(False) + return + ## # Internal Functions ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index aa3ea021..d9ca56a6 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -101,22 +101,22 @@ class GuiProjectView(QWidget): self.keyGoPrev = QShortcut(self.projTree) self.keyGoPrev.setKey("Alt+Up") self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoPrev.activated.connect(self.projTree.moveSiblingUp) + self.keyGoPrev.activated.connect(self.projTree.goToSiblingUp) self.keyGoNext = QShortcut(self.projTree) self.keyGoNext.setKey("Alt+Down") self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoNext.activated.connect(self.projTree.moveSiblingDown) + self.keyGoNext.activated.connect(self.projTree.goToSiblingDown) self.keyGoUp = QShortcut(self.projTree) self.keyGoUp.setKey("Alt+Left") self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoUp.activated.connect(self.projTree.moveToParent) + self.keyGoUp.activated.connect(self.projTree.goToParent) self.keyGoDown = QShortcut(self.projTree) self.keyGoDown.setKey("Alt+Right") self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoDown.activated.connect(self.projTree.moveToFirstChild) + self.keyGoDown.activated.connect(self.projTree.goToFirstChild) self.keyContext = QShortcut(self.projTree) self.keyContext.setKey("Ctrl+.") @@ -415,8 +415,6 @@ class GuiProjectToolBar(QWidget): self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon()) elif tHandle in self.mTemplates: self.mTemplates.remove(tHandle) - elif change == nwChange.DELETE and tHandle in self.mTemplates: - self.mTemplates.remove(tHandle) return ## @@ -680,7 +678,8 @@ class GuiProjectTree(QTreeView): tHandle = SHARED.project.newFolder(newLabel, sHandle, pos) # Select the new item automatically - self.setSelectedHandle(tHandle) + if tHandle: + self.setSelectedHandle(tHandle) return @@ -793,6 +792,7 @@ class GuiProjectTree(QTreeView): dHandles = docDup.duplicate(itemTree) if len(dHandles) != len(itemTree): SHARED.warn(self.tr("Could not duplicate all items.")) + self.restoreExpandedState() return ## @@ -807,7 +807,7 @@ class GuiProjectTree(QTreeView): super().mousePressEvent(event) if event.button() == QtMouseLeft: if not self.indexAt(event.pos()).isValid(): - self.selectionModel().clearCurrentIndex() + self._clearSelection() elif event.button() == QtMouseMiddle: if (node := self._getNode(self.indexAt(event.pos()))) and node.item.isFileType(): self.projView.openDocumentRequest.emit( @@ -841,7 +841,7 @@ class GuiProjectTree(QTreeView): return @pyqtSlot() - def moveSiblingUp(self) -> None: + def goToSiblingUp(self) -> None: """Skip to the previous sibling.""" if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (move := parent.child(node.row() - 1)) and (model := self._getModel()): @@ -849,7 +849,7 @@ class GuiProjectTree(QTreeView): return @pyqtSlot() - def moveSiblingDown(self) -> None: + def goToSiblingDown(self) -> None: """Skip to the next sibling.""" if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (move := parent.child(node.row() + 1)) and (model := self._getModel()): @@ -857,7 +857,7 @@ class GuiProjectTree(QTreeView): return @pyqtSlot() - def moveToParent(self) -> None: + def goToParent(self) -> None: """Move to parent item.""" if ( (model := self._getModel()) @@ -868,7 +868,7 @@ class GuiProjectTree(QTreeView): return @pyqtSlot() - def moveToFirstChild(self) -> None: + def goToFirstChild(self) -> None: """Move to first child item.""" if ( (model := self._getModel()) @@ -893,7 +893,9 @@ class GuiProjectTree(QTreeView): return @pyqtSlot() - def processDeleteRequest(self, handles: list[str] = [], askFirst: bool = True) -> None: + def processDeleteRequest( + self, handles: list[str] | None = None, askFirst: bool = True + ) -> None: """Move selected items to Trash.""" if handles and (model := self._getModel()): indices = [model.indexFromHandle(handle) for handle in handles] @@ -1002,20 +1004,26 @@ class GuiProjectTree(QTreeView): def _onNodeCollapsed(self, index: QModelIndex) -> None: """Capture a node collapse, and pass it to the model.""" if node := self._getNode(index): - node.item.setExpanded(False) + node.setExpanded(False) return @pyqtSlot(QModelIndex) def _onNodeExpanded(self, index: QModelIndex) -> None: """Capture a node expand, and pass it to the model.""" if node := self._getNode(index): - node.item.setExpanded(True) + node.setExpanded(True) return ## # Internal Functions ## + def _clearSelection(self) -> None: + """Clear the currently selected items.""" + self.clearSelection() + self.selectionModel().clearCurrentIndex() + return + def _selectedRows(self) -> list[QModelIndex]: """Return all column 0 indexes.""" return [i for i in self.selectedIndexes() if i.column() == 0] @@ -1162,9 +1170,8 @@ class _TreeContextMenu(QMenu): # Process Item if self._children: self._expandCollapse() - if isFile: - action = self.addAction(self.tr("Duplicate")) - action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle)) + action = self.addAction(self.tr("Duplicate")) + action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle)) self._deleteOrTrash() return @@ -1321,9 +1328,10 @@ class _TreeContextMenu(QMenu): def _deleteOrTrash(self) -> None: """Add move to Trash action.""" - if self._model.trashSelection(self._indices): - text = self.tr("Delete Permanently") - elif len(self._indices) == 1 and self._item.isRootType(): + if ( + self._model.trashSelection(self._indices) + or len(self._indices) == 1 and self._item.isRootType() + ): text = self.tr("Delete Permanently") else: text = self.tr("Move to Trash") diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index bca7cbaa..e5221cd2 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -419,8 +419,7 @@ class _FilterTab(NFixedPage): logger.debug("Building project tree") self._treeMap = {} self.optTree.clear() - for node in SHARED.project.tree.model.root.allChildren(): - nwItem = node.item + for nwItem in SHARED.project.tree: tHandle = nwItem.itemHandle pHandle = nwItem.itemParent rHandle = nwItem.itemRoot diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 3b05335b..d3051769 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -34,7 +34,7 @@ Novel - + Title Page @@ -42,39 +42,39 @@ New Folder - + New Chapter - + New Scene - + Plot - + New Note - + Characters - + New Note - + Locations - + New Note - + Trash diff --git a/tests/test_core/test_core_itemmodel.py b/tests/test_core/test_core_itemmodel.py index 5d26d2ea..f63afa35 100644 --- a/tests/test_core/test_core_itemmodel.py +++ b/tests/test_core/test_core_itemmodel.py @@ -116,6 +116,16 @@ def testCoreItemModel_ProjectNode_Children(mockGUI, mockRnd, fncPath): assert child010.parent() is child01 assert child011.parent() is child01 + # Expand + child0.setExpanded(True) + child1.setExpanded(True) + child2.setExpanded(True) + child3.setExpanded(True) + assert child0.item.isExpanded is True # Only one with children + assert child1.item.isExpanded is False + assert child2.item.isExpanded is False + assert child3.item.isExpanded is False + @pytest.mark.core def testCoreItemModel_ProjectNode_Modify(mockGUI, mockRnd, fncPath): diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 2c572383..3d5ede31 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -20,24 +20,20 @@ along with this program. If not, see . """ from __future__ import annotations -from pathlib import Path from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, QMimeData, QPoint, Qt, QTimer -from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent -from PyQt5.QtWidgets import QMenu, QMessageBox, QTreeWidget, QTreeWidgetItem +from PyQt5.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPoint +from PyQt5.QtGui import QMouseEvent +from PyQt5.QtWidgets import QMenu, QMessageBox from novelwriter import CONFIG, SHARED -from novelwriter.core.item import NWItem -from novelwriter.core.project import NWProject from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.enum import nwFocus, nwItemClass, nwItemLayout, nwItemType -from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu -from novelwriter.guimain import GuiMain +from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType +from novelwriter.gui.projtree import _TreeContextMenu from novelwriter.types import ( QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected, QtScrollAlwaysOff, QtScrollAsNeeded @@ -48,122 +44,264 @@ from tests.tools import C, buildTestProject @pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): - """Test adding and removing items from the project tree.""" +def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): + """Test adding items to the project tree.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) projView = nwGUI.projView - projTree = nwGUI.projView.projTree + projTree = projView.projTree project = SHARED.project + tree = project.tree # Try to add item with no project - assert projTree.newTreeItem(nwItemType.FILE) is False + projTree.newTreeItem(nwItemType.FILE) + assert len(tree) == 0 # Create a project buildTestProject(nwGUI, projPath) - - # No itemType set - projTree.clearSelection() - assert projTree.newTreeItem(None) is False + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # Root Items # ========== # No class set - assert projTree.newTreeItem(nwItemType.ROOT) is False + projTree.newTreeItem(nwItemType.ROOT) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] - # Create root item - assert projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True - assert "0000000000010" in project.tree + # Create Objects root item after Locations + hObjectRoot = "0000000000011" + projView.setSelectedHandle(C.hWorldRoot) + projTree.newTreeItem(nwItemType.ROOT, nwItemClass.OBJECT) + assert hObjectRoot in tree + item = tree[hObjectRoot] + assert item is not None + assert item.itemName == "Objects" + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Objects", "Trash", + ] # File/Folder Items # ================= # No location selected for new item - projTree.clearSelection() + projTree._clearSelection() + caplog.clear() - assert projTree.newTreeItem(nwItemType.FILE) is False - assert projTree.newTreeItem(nwItemType.FOLDER) is False + projTree.newTreeItem(nwItemType.FILE) assert "Did not find anywhere" in caplog.text + caplog.clear() + projTree.newTreeItem(nwItemType.FOLDER) + assert "Did not find anywhere" in caplog.text + + # Try to add them to Trash + projView.setSelectedHandle(trash.item.itemHandle) + + caplog.clear() + projTree.newTreeItem(nwItemType.FILE) + assert "Cannot add new files or folders" in caplog.text + + caplog.clear() + projTree.newTreeItem(nwItemType.FOLDER) + assert "Cannot add new files or folders" in caplog.text + # Create new folder as child of Novel folder - projView.setSelectedHandle(C.hNovelRoot) - assert projTree.newTreeItem(nwItemType.FOLDER) is True - assert project.tree["0000000000011"].itemParent == C.hNovelRoot # type: ignore - assert project.tree["0000000000011"].itemRoot == C.hNovelRoot # type: ignore - assert project.tree["0000000000011"].itemClass == nwItemClass.NOVEL # type: ignore + hNewFolder = "0000000000012" + projView.setSelectedHandle(C.hNovelRoot, doScroll=True) + projTree.newTreeItem(nwItemType.FOLDER) + assert hNewFolder in tree + item = tree[hNewFolder] + assert item is not None + assert item.itemName == "New Folder" + assert item.itemParent == C.hNovelRoot + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "Plot", "Characters", "Locations", "Objects", "Trash", + ] # Add a new file in the new folder - projView.setSelectedHandle("0000000000011") - assert projTree.newTreeItem(nwItemType.FILE) is True - assert project.tree["0000000000012"].itemParent == "0000000000011" # type: ignore - assert project.tree["0000000000012"].itemRoot == C.hNovelRoot # type: ignore - assert project.tree["0000000000012"].itemClass == nwItemClass.NOVEL # type: ignore + hNewFile = "0000000000013" + projView.setSelectedHandle(hNewFolder, doScroll=True) + projTree.newTreeItem(nwItemType.FILE) + assert hNewFile in tree + item = tree[hNewFile] + assert item is not None + assert item.itemName == "New Document" + assert item.itemParent == hNewFolder + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "Plot", "Characters", "Locations", + "Objects", "Trash", + ] # Add a new chapter next to the other new file - projView.setSelectedHandle("0000000000012") - assert projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True - assert project.tree["0000000000013"].itemParent == "0000000000011" # type: ignore - assert project.tree["0000000000013"].itemRoot == C.hNovelRoot # type: ignore - assert project.tree["0000000000013"].itemClass == nwItemClass.NOVEL # type: ignore - assert nwGUI.openDocument("0000000000013") + hNewChapter = "0000000000014" + projView.setSelectedHandle(hNewFile, doScroll=True) + projTree.newTreeItem(nwItemType.FILE, hLevel=2) + assert hNewChapter in tree + item = tree[hNewChapter] + assert item is not None + assert item.itemName == "New Chapter" + assert item.itemParent == hNewFolder + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument(hNewChapter) assert nwGUI.docEditor.getText() == "## New Chapter\n\n" + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "Plot", "Characters", + "Locations", "Objects", "Trash", + ] # Add a new scene next to the other new file - projView.setSelectedHandle("0000000000012") - assert projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True - assert project.tree["0000000000014"].itemParent == "0000000000011" # type: ignore - assert project.tree["0000000000014"].itemRoot == C.hNovelRoot # type: ignore - assert project.tree["0000000000014"].itemClass == nwItemClass.NOVEL # type: ignore - assert nwGUI.openDocument("0000000000014") + hNewScene = "0000000000015" + projView.setSelectedHandle(hNewChapter, doScroll=True) + projTree.newTreeItem(nwItemType.FILE, hLevel=3) + assert hNewScene in tree + item = tree[hNewScene] + assert item is not None + assert item.itemName == "New Scene" + assert item.itemParent == hNewFolder + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument(hNewScene) assert nwGUI.docEditor.getText() == "### New Scene\n\n" + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "Plot", + "Characters", "Locations", "Objects", "Trash", + ] # Add a new scene with the content copied from the previous - assert nwGUI.openDocument("0000000000014") + nwGUI.openDocument(hNewScene) nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n") nwGUI.saveDocument() - projView.setSelectedHandle("0000000000014") - assert projTree.newTreeItem(nwItemType.FILE, copyDoc="0000000000014") is True - assert project.tree["0000000000015"].itemParent == "0000000000011" # type: ignore - assert project.tree["0000000000015"].itemRoot == C.hNovelRoot # type: ignore - assert project.tree["0000000000015"].itemClass == nwItemClass.NOVEL # type: ignore - assert nwGUI.openDocument("0000000000015") + + hNewSceneCopy = "0000000000016" + projView.setSelectedHandle(hNewScene, doScroll=True) + projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene) + assert hNewSceneCopy in tree + item = tree[hNewSceneCopy] + assert item is not None + assert item.itemName == "New Scene" + assert item.itemParent == hNewFolder + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument(hNewSceneCopy) assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n" + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "Plot", "Characters", "Locations", "Objects", "Trash", + ] # Add a new file to the characters folder - projView.setSelectedHandle(C.hCharRoot) - assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True - assert project.tree["0000000000016"].itemParent == C.hCharRoot # type: ignore - assert project.tree["0000000000016"].itemRoot == C.hCharRoot # type: ignore - assert project.tree["0000000000016"].itemClass == nwItemClass.CHARACTER # type: ignore - assert nwGUI.openDocument("0000000000016") + hNewCharacter = "0000000000017" + projView.setSelectedHandle(C.hCharRoot, doScroll=True) + projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + assert hNewCharacter in tree + item = tree[hNewCharacter] + assert item is not None + assert item.itemName == "New Note" + assert item.itemParent == C.hCharRoot + assert item.itemRoot == C.hCharRoot + assert item.itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument(hNewCharacter) assert nwGUI.docEditor.getText() == "# New Note\n\n" - - # Make sure the sibling folder bug trap works - projView.setSelectedHandle("0000000000013") - project.tree["0000000000013"].setParent(None) # This should not happen # type: ignore - caplog.clear() - assert projTree.newTreeItem(nwItemType.FILE) is False - assert "Internal error" in caplog.text - project.tree["0000000000013"].setParent("0000000000011") # type: ignore + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "Plot", "Characters", "New Note", "Locations", "Objects", "Trash", + ] # Cancel during creation with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False)) - projView.setSelectedHandle("0000000000013") - assert projTree.newTreeItem(nwItemType.FILE) is False + projView.setSelectedHandle(hNewFile, doScroll=True) + projTree.newTreeItem(nwItemType.FILE) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "Plot", "Characters", "New Note", "Locations", "Objects", "Trash", + ] - # Get the trash folder - with monkeypatch.context() as mp: - mp.setattr(NWProject, "trashFolder", lambda *a: None) - assert projTree._addTrashRoot() is None + # From Template + # ============= - assert isinstance(projTree._addTrashRoot(), QTreeWidgetItem) - trashHandle = project.trashFolder() - projView.setSelectedHandle(trashHandle) - assert projTree.newTreeItem(nwItemType.FILE) is False - assert "Cannot add new files or folders to the Trash folder" in caplog.text + # Create template folder + hTemplateRoot = "0000000000018" + projView.setSelectedHandle(hObjectRoot) + projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE) + assert hTemplateRoot in tree + item = tree[hTemplateRoot] + assert item is not None + assert item.itemName == "Templates" + assert item.itemParent is None + assert item.itemRoot == hTemplateRoot + assert item.itemClass == nwItemClass.TEMPLATE + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "Plot", "Characters", "New Note", "Locations", "Objects", "Templates", + "Trash", + ] + + # Create scene template + hSceneTemplate = "0000000000019" + projView.setSelectedHandle(hTemplateRoot, doScroll=True) + projTree.newTreeItem(nwItemType.FILE, hLevel=3) + assert hSceneTemplate in tree + item = tree[hSceneTemplate] + assert item is not None + assert item.itemName == "New Scene" + assert item.itemParent == hTemplateRoot + assert item.itemRoot == hTemplateRoot + assert item.itemClass == nwItemClass.TEMPLATE + + item.setName("New Scene Template") + nwGUI.openDocument(hSceneTemplate) + nwGUI.docEditor.setPlainText("### New Scene Template\n\nWith Stuff\n\n") + nwGUI.saveDocument() + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "Plot", "Characters", "New Note", "Locations", "Objects", "Templates", + "New Scene Template", "Trash", + ] + + # Create from template + hNewFromTemplate = "000000000001a" + projView.setSelectedHandle(hNewSceneCopy, doScroll=True) + projView.createFileFromTemplate(hSceneTemplate) + assert hNewFromTemplate in tree + item = tree[hNewFromTemplate] + assert item is not None + assert item.itemName == "New Scene Template" + assert item.itemParent == hNewFolder + assert item.itemRoot == C.hNovelRoot + assert item.itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument(hNewFromTemplate) + assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n" + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Folder", "New Document", "New Chapter", "New Scene", "New Scene", + "New Scene Template", "Plot", "Characters", "New Note", "Locations", + "Objects", "Templates", "New Scene Template", "Trash", + ] # Rename Item # =========== @@ -171,25 +309,11 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn # Rename plot folder with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True)) - projTree.renameTreeItem(C.hPlotRoot) - assert project.tree[C.hPlotRoot].itemName == "Stuff" # type: ignore - - # Other Checks - # ============ - - # Also check error handling in reveal function - assert projTree.revealNewTreeItem("abc") is False - - # Add an item that cannot be displayed in the tree - nHandle = project.newFile("Test", None) # type: ignore - assert projTree.revealNewTreeItem(nHandle) is False - - # Adding an invalid item directly to the tree should also fail - assert projTree._addTreeItem(None) is None - - # Setting values for a non-existing tree item should be handled - projTree.setTreeItemValues(None) - projTree.setTreeItemValues(C.hInvalid) # The function used to take handles + projView.setSelectedHandle(C.hPlotRoot, doScroll=True) + projView.renameTreeItem() + item = tree[C.hPlotRoot] + assert item is not None + assert item.itemName == "Stuff" # Clean up # qtbot.stop() @@ -197,343 +321,373 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn @pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): - """Test adding and removing items from the project tree.""" +def testGuiProjTree_SimpleOperations(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test simple operations in the project tree like internal move, + change selection and expand/collapse nodes. + """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) projView = nwGUI.projView - projTree = nwGUI.projView.projTree + projTree = projView.projTree + project = SHARED.project + tree = project.tree + model = tree.model - # Try to move item with no project - assert projTree.moveTreeItem(1) is False + # The default model is empty + assert projView.getSelectedHandle() is None + assert projTree._getModel() is None + assert projTree._selectedRows() == [] + assert projTree._getNode(QModelIndex()) is None # Create a project buildTestProject(nwGUI, projPath) - - # Move Documents - # ============== - - # Add some files - projView.setSelectedHandle(C.hChapterDir) - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000011", "0000000000012", + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", ] - # Move with no selections - projTree.clearSelection() - assert projTree.moveTreeItem(1) is False + # Add some scenes + hFolder = "0000000000011" + hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"] + projView.setSelectedHandle(C.hNovelRoot, doScroll=True) + projTree.newTreeItem(nwItemType.FOLDER) + assert hFolder in tree + item = tree[hFolder] + assert item is not None + item.setName("Chapter Folder") - # Move second item up twice (should give same result) - projView.setSelectedHandle(C.hSceneDoc) - assert projTree.moveTreeItem(-1) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hSceneDoc, C.hChapterDoc, - "0000000000010", "0000000000011", "0000000000012", - ] - assert projTree.moveTreeItem(-1) is False - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hSceneDoc, C.hChapterDoc, - "0000000000010", "0000000000011", "0000000000012", + projView.setSelectedHandle(hFolder, doScroll=True) + for n, hScene in enumerate(hScenes): + projTree.newTreeItem(nwItemType.FILE, hLevel=3) + assert hScene in tree + item = tree[hScene] + assert item is not None + item.setName(f"Scene {n+1}") + + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Locations", "Trash", ] - # Restore - assert projTree.moveTreeItem(1) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000011", "0000000000012", + # Expansion + # ========= + + # Default State + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [ + C.hNovelRoot, hFolder ] - # Move fifth item down twice (should give same result) - projView.setSelectedHandle("0000000000011") - assert projTree.moveTreeItem(1) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000012", "0000000000011", - ] - assert projTree.moveTreeItem(1) is False - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000012", "0000000000011", + # Expand Novel + projTree.expandFromIndex(model.indexFromHandle(C.hNovelRoot)) + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [ + C.hNovelRoot, C.hChapterDir, hFolder ] - # Restore - assert projTree.moveTreeItem(-1) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000011", "0000000000012", + # Collapse Novel + projTree.collapseFromIndex(model.indexFromHandle(C.hNovelRoot)) + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [ + C.hNovelRoot ] - # Move down again - projView.setSelectedHandle("0000000000011") - assert projTree.moveTreeItem(1) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000012", "0000000000011", + # Move Up + # ======= + + # Already first + projView.setSelectedHandle(hScenes[0], doScroll=True) + projTree.moveItemUp() + assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [ + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", ] - # Root Folder - # =========== + # Proper move + projView.setSelectedHandle(hScenes[1], doScroll=True) + projTree.moveItemUp() + assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [ + "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4", + ] - projView.setSelectedHandle(C.hNovelRoot) - assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 + # Move Down + # ========= - # Move novel folder up - assert projTree.moveTreeItem(-1) is False - assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 + # Already last + projView.setSelectedHandle(hScenes[3], doScroll=True) + projTree.moveItemDown() + assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [ + "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4", + ] - # Move novel folder down - assert projTree.moveTreeItem(1) is True - assert SHARED.project.tree._order.index(C.hNovelRoot) == 1 + # Proper move + projView.setSelectedHandle(hScenes[1], doScroll=True) + projTree.moveItemDown() + assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [ + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + ] - # Move novel folder up again - assert projTree.moveTreeItem(-1) is True - assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 + # Select Up + # ========= - # Clean up - # qtbot.stop() - nwGUI.closeProject() + # Sibling + projView.setSelectedHandle(hFolder, doScroll=True) + assert projView.getSelectedHandle() == hFolder + projTree.goToSiblingUp() + assert projView.getSelectedHandle() == C.hChapterDir + projTree.goToSiblingUp() + assert projView.getSelectedHandle() == C.hTitlePage + projTree.goToSiblingUp() + assert projView.getSelectedHandle() == C.hTitlePage + + # Parent + projTree.goToParent() + assert projView.getSelectedHandle() == C.hNovelRoot + projTree.goToParent() + assert projView.getSelectedHandle() == C.hNovelRoot + + # Select Down + # ========= + + # Child + projView.setSelectedHandle(C.hNovelRoot, doScroll=True) + assert projView.getSelectedHandle() == C.hNovelRoot + projTree.goToFirstChild() + assert projView.getSelectedHandle() == C.hTitlePage + + # Sibling + projTree.goToSiblingDown() + assert projView.getSelectedHandle() == C.hChapterDir + projTree.goToSiblingDown() + assert projView.getSelectedHandle() == hFolder + projTree.goToSiblingDown() + assert projView.getSelectedHandle() == hFolder @pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): - """Test external requests for removing items from project tree.""" +def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test mouse clicks in the project tree.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) projView = nwGUI.projView - projTree = nwGUI.projView.projTree + projTree = projView.projTree + project = SHARED.project + tree = project.tree + model = tree.model - # Try to run with no project - assert projView.requestDeleteItem() is False + # The default model is empty + assert projView.getSelectedHandle() is None + assert projTree._getModel() is None + assert projTree._selectedRows() == [] + assert projTree._getNode(QModelIndex()) is None # Create a project buildTestProject(nwGUI, projPath) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 - # Try emptying the trash already now, when there is no trash folder - assert projView.emptyTrash() is False + # Single click emits a signal + with qtbot.waitSignal(projView.selectedItemChanged) as signal: + projTree._onSingleClick(model.indexFromHandle(C.hNovelRoot)) + assert signal.args[0] == C.hNovelRoot - # Add some files - projView.setSelectedHandle(C.hChapterDir) - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.newTreeItem(nwItemType.FILE) is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010", "0000000000011", "0000000000012", + # Double click on folder expands/collapses it + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [] + projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot)) + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [ + C.hNovelRoot ] + projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot)) + assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [] - # Delete item without focus -> blocked - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - projView.setSelectedHandle("0000000000012") - assert projView.requestDeleteItem() is False - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + # Double click on file opens it + with qtbot.waitSignal(projView.openDocumentRequest) as signal: + projTree._onDoubleClick(model.indexFromHandle(C.hChapterDoc)) + assert signal.args[0] == C.hChapterDoc + assert signal.args[1] == nwDocMode.EDIT + assert signal.args[2] == "" + assert signal.args[3] is True - # No selection made - projTree.clearSelection() - caplog.clear() - assert projView.requestDeleteItem() is False - assert "no item to delete" in caplog.text - - # Not a valid handle - projTree.clearSelection() - caplog.clear() - assert projView.requestDeleteItem("0000000000000") is False - assert "No tree item with handle '0000000000000'" in caplog.text - - # Delete Root Folders + # Mouse Button Clicks # =================== - assert projView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked - assert projView.requestDeleteItem(C.hCharRoot) is True # Character Root + eType = QEvent.Type.MouseButtonPress + modifier = QtModNone - # Delete File - # =========== + # Trigger the viewer + pos = projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center() + button = QtMouseMiddle + event = QMouseEvent(eType, pos, button, button, modifier) + projTree.mousePressEvent(event) + assert nwGUI.docViewer.docHandle == C.hChapterDoc - # Block adding trash folder - funcPointer = projTree._addTrashRoot - projTree._addTrashRoot = lambda *a: None - assert projView.requestDeleteItem("0000000000012") is False - projTree._addTrashRoot = funcPointer + # Trigger the left click clear + pos = QPoint(5000, 5000) + button = QtMouseLeft + event = QMouseEvent(eType, pos, button, button, modifier) - # Delete last two documents, which also adds the trash folder - assert projView.requestDeleteItem("0000000000012") is True - assert projView.requestDeleteItem("0000000000011") is True - assert projTree.getTreeFromHandle(C.hChapterDir) == [ - C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000010" - ] - trashHandle = SHARED.project.tree.trashRoot - assert projTree.getTreeFromHandle(trashHandle) == [ - trashHandle, "0000000000012", "0000000000011" + projTree.setSelectedHandle(C.hChapterDoc) + assert projView.getSelectedHandle() == C.hChapterDoc + + projTree.mousePressEvent(event) + assert projView.getSelectedHandle() is None + + +@pytest.mark.gui +def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): + """Test delete requests in the project tree.""" + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + + projView = nwGUI.projView + projTree = projView.projTree + project = SHARED.project + tree = project.tree + + # Create a project + buildTestProject(nwGUI, projPath) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", ] - # Try to delete the trash folder - caplog.clear() - assert projView.requestDeleteItem("0000000000013") is False - assert "Cannot delete the Trash folder" in caplog.text + # Add some scenes + hFolder = "0000000000011" + hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"] + projView.setSelectedHandle(C.hNovelRoot, doScroll=True) + projTree.newTreeItem(nwItemType.FOLDER) + assert hFolder in tree + item = tree[hFolder] + assert item is not None + item.setName("Chapter Folder") - nwGUI.closeProject() + projView.setSelectedHandle(hFolder, doScroll=True) + for n, hScene in enumerate(hScenes): + projTree.newTreeItem(nwItemType.FILE, hLevel=3) + assert hScene in tree + item = tree[hScene] + assert item is not None + item.setName(f"Scene {n+1}") + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Locations", "Trash", + ] -@pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): - """Test moving items to Trash.""" - monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + # Calling it with no selection, does nothing + projTree._clearSelection() + projTree.processDeleteRequest() + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Locations", "Trash", + ] - project = SHARED.project - projTree = nwGUI.projView.projTree + # Deleting a used root raises an error + projTree.processDeleteRequest([C.hNovelRoot]) + assert SHARED.lastAlert == "Root folders can only be deleted when they are empty." + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Locations", "Trash", + ] - # Create a project - buildTestProject(nwGUI, projPath) + # Deleting a unused root is fine + projTree.processDeleteRequest([C.hWorldRoot]) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Trash", + ] - # Invalid item - caplog.clear() - assert projTree.moveItemToTrash(C.hInvalid) is False - assert "Could not find tree item for deletion" in caplog.text - - # Root folders cannot be moved to Trash - caplog.clear() - assert projTree.moveItemToTrash(C.hNovelRoot) is False - assert "Root folders cannot be moved to Trash" in caplog.text - - # Block adding trash folder - funcPointer = projTree._addTrashRoot - projTree._addTrashRoot = lambda *a: None - - caplog.clear() - assert projTree.moveItemToTrash(C.hTitlePage) is False - assert project.tree.isTrash(C.hTitlePage) is False - assert "Could not delete item" in caplog.text - - projTree._addTrashRoot = funcPointer - - # User cancels action + # User can cancel move to trash with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) - assert projTree.moveItemToTrash(C.hTitlePage) is False - assert project.tree.isTrash(C.hTitlePage) is 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", + "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4", + "Plot", "Characters", "Trash", + ] - # Move a document to Trash - assert projTree.moveItemToTrash(C.hTitlePage) is True - assert project.tree.isTrash(C.hTitlePage) is True + # Items not already in trash can be moved there + 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", + "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 1", "Scene 2", "Scene 3", "Scene 4", + ] - # Cannot be moved again - caplog.clear() - assert projTree.moveItemToTrash(C.hTitlePage) is False - assert "Item is already in the Trash folder" in caplog.text - - nwGUI.closeProject() - - -@pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): - """Test permanently deleting items.""" - monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - project = SHARED.project - projTree = nwGUI.projView.projTree - - # Create a project - buildTestProject(nwGUI, projPath) - - # Invalid item - caplog.clear() - assert projTree.permDeleteItem(C.hInvalid) is False - assert "Could not find tree item for deletion" in caplog.text - - # Not deleting root item in use - caplog.clear() - assert projTree.permDeleteItem(C.hNovelRoot) is False - assert "Root folders can only be deleted when they are empty" in caplog.text - assert C.hNovelRoot in project.tree - - # Deleting unused root item is allowed - caplog.clear() - assert projTree.permDeleteItem(C.hPlotRoot) is True - assert C.hPlotRoot not in project.tree - - # User cancels action + # User can block permanent deletion with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) - assert projTree.permDeleteItem(C.hTitlePage) is False - assert C.hTitlePage in project.tree + 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", + "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 1", "Scene 2", "Scene 3", "Scene 4", + ] - # Deleting file is OK, and if it is open, it should close - assert nwGUI.openDocument(C.hTitlePage) is True - assert nwGUI.docEditor.docHandle == C.hTitlePage - assert projTree.permDeleteItem(C.hTitlePage) is True - assert C.hTitlePage not in project.tree - assert nwGUI.docEditor.docHandle is None + # Items in trash can be permanently deleted + 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", + "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 3", "Scene 4", + ] - # Deleting folder + files recursively is ok - assert projTree.permDeleteItem(C.hChapterDir) is True - assert C.hChapterDir not in project.tree - assert C.hChapterDoc not in project.tree - assert C.hSceneDoc not in project.tree + # Moving a parent item to trash, includes children + projTree.processDeleteRequest([C.hChapterDir], askFirst=True) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 3", "Scene 4", "New Folder", "New Chapter", "New Scene", + ] - nwGUI.closeProject() + # Permanently delete in trash is recursive + projTree.processDeleteRequest([C.hChapterDir], askFirst=True) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 3", "Scene 4", + ] - -@pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): - """Test emptying Trash.""" - monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - project = SHARED.project - projTree = nwGUI.projView.projTree - - # No project open - caplog.clear() - assert projTree.emptyTrash() is False - assert "No project open" in caplog.text - - # Create a project - buildTestProject(nwGUI, projPath) - - # No Trash folder - assert projTree.emptyTrash() is False - - # Move some documents to Trash - assert projTree.moveItemToTrash(C.hTitlePage) is True - assert projTree.moveItemToTrash(C.hChapterDir) is True - - assert project.tree.isTrash(C.hTitlePage) is True - assert project.tree.isTrash(C.hChapterDir) is True - assert project.tree.isTrash(C.hChapterDoc) is True - assert project.tree.isTrash(C.hSceneDoc) is True - - # User cancels + # Trash can be completely emptied, but user can block it with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) - assert projTree.emptyTrash() is False - assert C.hTitlePage in project.tree - assert C.hChapterDir in project.tree - assert C.hChapterDoc in project.tree - assert C.hSceneDoc in project.tree + projTree.emptyTrash() + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", + "Scene 3", "Scene 4", + ] - # Run again to empty all items - assert projTree.emptyTrash() is True - assert C.hTitlePage not in project.tree - assert C.hChapterDir not in project.tree - assert C.hChapterDoc not in project.tree - assert C.hSceneDoc not in project.tree + # 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", + ] - # Running Empty Trash again is cancelled due to empty folder - assert projTree.emptyTrash() is False + # Emptying empty trash pops an alert + projTree.emptyTrash() + assert SHARED.lastAlert == "The Trash folder is already empty." - nwGUI.closeProject() + # Trash can be deleted if empty + projTree.processDeleteRequest([trash.item.itemHandle]) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Chapter Folder", "Plot", "Characters" + ] + + # Emptying trash when it doesn't exist, recreates it + projTree.emptyTrash() + assert SHARED.lastAlert == "The Trash folder is already empty." + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", + ] @pytest.mark.gui -@pytest.mark.skip def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): """Test the merge document function.""" mergeData = {} @@ -544,18 +698,33 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted) monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData) - buildTestProject(nwGUI, projPath) - + projView = nwGUI.projView + projTree = projView.projTree project = SHARED.project - projTree = nwGUI.projView.projTree + tree = project.tree - mergedDoc1 = "0000000000014" + # Create a project + buildTestProject(nwGUI, projPath) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # Create File to Merge hChapter1 = project.newFile("Chapter 1", C.hNovelRoot) - hSceneOne11 = project.newFile("Scene 1.1", hChapter1) # type: ignore - hSceneOne12 = project.newFile("Scene 1.2", hChapter1) # type: ignore - hSceneOne13 = project.newFile("Scene 1.3", hChapter1) # type: ignore + assert hChapter1 == "0000000000011" + + hSceneOne11 = project.newFile("Scene 1.1", hChapter1) + hSceneOne12 = project.newFile("Scene 1.2", hChapter1) + hSceneOne13 = project.newFile("Scene 1.3", hChapter1) + assert hSceneOne11 == "0000000000012" + assert hSceneOne12 == "0000000000013" + assert hSceneOne13 == "0000000000014" + + mergedDoc1 = "0000000000015" docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n" docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n" @@ -568,25 +737,26 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, lenText4 = len(docText4) lenAll = lenText1 + lenText2 + lenText3 + lenText4 - project.writeNewFile(hChapter1, 2, True, docText1) # type: ignore - project.writeNewFile(hSceneOne11, 3, True, docText2) # type: ignore - project.writeNewFile(hSceneOne12, 3, True, docText3) # type: ignore - project.writeNewFile(hSceneOne13, 3, True, docText4) # type: ignore + project.writeNewFile(hChapter1, 2, True, docText1) + project.writeNewFile(hSceneOne11, 3, True, docText2) + project.writeNewFile(hSceneOne12, 3, True, docText3) + project.writeNewFile(hSceneOne13, 3, True, docText4) - projTree.revealNewTreeItem(hChapter1) - projTree.revealNewTreeItem(hSceneOne11) - projTree.revealNewTreeItem(hSceneOne12) - projTree.revealNewTreeItem(hSceneOne13) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3", + "Plot", "Characters", "Locations", "Trash", + ] # Invalid file handle - assert projTree._mergeDocuments(C.hInvalid, False) is False + assert projTree.mergeDocuments(C.hInvalid, False) is False # Cannot merge root item - assert projTree._mergeDocuments(C.hNovelRoot, False) is False + assert projTree.mergeDocuments(C.hNovelRoot, False) is False # Merge to new file, but there is now merge data mergeData.clear() - assert projTree._mergeDocuments(hChapter1, True) is False + assert projTree.mergeDocuments(hChapter1, True) is False # Merge to New Doc # ================ @@ -598,11 +768,18 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # User cancels merge with monkeypatch.context() as mp: mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected) - assert projTree._mergeDocuments(hChapter1, True) is False + assert projTree.mergeDocuments(hChapter1, True) is False # The merge goes through - assert projTree._mergeDocuments(hChapter1, True) is True - assert len(project.storage.getDocument(mergedDoc1).readDocument()) > lenAll # type: ignore + assert projTree.mergeDocuments(hChapter1, True) is True + mergedText = project.storage.getDocument(mergedDoc1).readDocument() + assert mergedText is not None + assert len(mergedText) > lenAll + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3", "[Merged] Chapter 1", + "Plot", "Characters", "Locations", "Trash", + ] # Merge to Existing Doc # ===================== @@ -612,28 +789,34 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, mergeData["moveToTrash"] = False # Merging to a folder is not allowed - assert projTree._mergeDocuments(C.hChapterDir, False) is False + assert projTree.mergeDocuments(C.hChapterDir, False) is False # Block writing and check error handling with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - assert projTree._mergeDocuments(hChapter1, False) is False + assert projTree.mergeDocuments(hChapter1, False) is False # Successful merge, and move to trash mergeData["moveToTrash"] = True - assert len(project.storage.getDocument(hChapter1).readDocument()) < lenAll # type: ignore - assert projTree._mergeDocuments(hChapter1, False) is True - assert len(project.storage.getDocument(hChapter1).readDocument()) > lenAll # type: ignore + mergedText = project.storage.getDocument(hChapter1).readDocument() + assert mergedText is not None + assert len(mergedText) < lenAll + assert projTree.mergeDocuments(hChapter1, False) is True + mergedText = project.storage.getDocument(hChapter1).readDocument() + assert mergedText is not None + assert len(mergedText) > lenAll - assert project.tree.isTrash(hSceneOne11) # type: ignore - assert project.tree.isTrash(hSceneOne12) # type: ignore - assert project.tree.isTrash(hSceneOne13) # type: ignore + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Chapter 1", "[Merged] Chapter 1", + "Plot", "Characters", "Locations", "Trash", + "Scene 1.1", "Scene 1.2", "Scene 1.3", + ] # qtbot.stop() @pytest.mark.gui -@pytest.mark.skip def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): """Test the split document function.""" splitData = {} @@ -645,11 +828,20 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted) monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText)) + projView = nwGUI.projView + projTree = projView.projTree + project = SHARED.project + tree = project.tree + # Create a project buildTestProject(nwGUI, projPath) - - project = SHARED.project - projTree = nwGUI.projView.projTree + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] docText = ( "Text\n\n" @@ -667,8 +859,8 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ) hSplitDoc = project.newFile("Split Doc", C.hNovelRoot) - project.writeNewFile(hSplitDoc, 1, True, docText) # type: ignore - projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True) + assert hSplitDoc is not None + project.writeNewFile(hSplitDoc, 1, True, docText) docText = f"# Split Doc\n\n{docText}" splitData["headerList"] = [ @@ -687,24 +879,24 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ] fstSet = [ - "0000000000011", "0000000000012", "0000000000013", "0000000000014", - "0000000000015", "0000000000016", "0000000000017", "0000000000018", - "0000000000019", "000000000001a", "000000000001b", "000000000001c", + "0000000000012", "0000000000013", "0000000000014", "0000000000015", + "0000000000016", "0000000000017", "0000000000018", "0000000000019", + "000000000001a", "000000000001b", "000000000001c", "000000000001d", ] sndSet = [ - "000000000001d", "000000000001e", "000000000001f", "0000000000020", - "0000000000021", "0000000000022", "0000000000023", "0000000000024", - "0000000000025", "0000000000026", "0000000000027", "0000000000028", + "000000000001e", "000000000001f", "0000000000020", "0000000000021", + "0000000000022", "0000000000023", "0000000000024", "0000000000025", + "0000000000026", "0000000000027", "0000000000028", "0000000000029", ] trdSet = [ - "000000000002a", "000000000002b", "000000000002c", "000000000002d", - "000000000002e", "000000000002f", "0000000000030", "0000000000031", - "0000000000032", "0000000000033", "0000000000034", "0000000000035", + "000000000002b", "000000000002c", "000000000002d", "000000000002e", + "000000000002f", "0000000000030", "0000000000031", "0000000000032", + "0000000000033", "0000000000034", "0000000000035", "0000000000036", ] # Try to split an invalid document and a non-document - assert projTree._splitDocument(C.hInvalid) is False - assert projTree._splitDocument(C.hNovelRoot) is False + assert projTree.splitDocument(C.hInvalid) is False + assert projTree.splitDocument(C.hNovelRoot) is False # Split into same root folder splitData["intoFolder"] = False @@ -712,13 +904,13 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # Writing fails with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - assert projTree._splitDocument(hSplitDoc) is True + assert projTree.splitDocument(hSplitDoc) is True for tHandle in fstSet: assert tHandle in project.tree assert not (projPath / "content" / f"{tHandle}.nwd").is_file() # Writing succeeds - assert projTree._splitDocument(hSplitDoc) is True + assert projTree.splitDocument(hSplitDoc) is True for tHandle in sndSet: assert tHandle in project.tree assert (projPath / "content" / f"{tHandle}.nwd").is_file() @@ -726,240 +918,95 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # Add to a folder and move source to trash splitData["intoFolder"] = True splitData["moveToTrash"] = True - assert projTree._splitDocument(hSplitDoc) is True + assert projTree.splitDocument(hSplitDoc) is True assert "0000000000029" in project.tree # The folder for tHandle in trdSet: assert tHandle in project.tree assert (projPath / "content" / f"{tHandle}.nwd").is_file() - assert project.tree.isTrash(hSplitDoc) is True # type: ignore + assert trash.allChildren() == [tree.nodes[hSplitDoc]] # Cancelled by user with monkeypatch.context() as mp: mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected) - assert projTree._splitDocument(hSplitDoc) is False + assert projTree.splitDocument(hSplitDoc) is False # qtbot.stop() @pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd): +def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the duplicate items function.""" # Create a project - buildTestProject(nwGUI, projPath) - assert len(SHARED.project.tree) == 8 + projView = nwGUI.projView + projTree = projView.projTree + project = SHARED.project + tree = project.tree + + # Create a project + buildTestProject(nwGUI, projPath) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] - projTree = nwGUI.projView.projTree projTree.expandAll() # Nothing to do - assert projTree.duplicateFromHandle(C.hInvalid) is False - assert len(SHARED.project.tree) == 8 + projTree.duplicateFromHandle(C.hInvalid) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # Duplicate title page, but select no with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) - assert projTree.duplicateFromHandle(C.hTitlePage) is False - assert len(SHARED.project.tree) == 8 - - # Duplicate title page - assert projTree.duplicateFromHandle(C.hTitlePage) is True - assert len(SHARED.project.tree) == 9 - - # Duplicate folder - assert projTree.duplicateFromHandle(C.hChapterDir) is True - assert len(SHARED.project.tree) == 12 - - # Duplicate novel root - assert projTree.duplicateFromHandle(C.hNovelRoot) is True - assert len(SHARED.project.tree) == 21 - - # Check tree order that all items are next to each other - assert SHARED.project.tree._order == [ - C.hNovelRoot, C.hTitlePage, "0000000000010", C.hChapterDir, C.hChapterDoc, C.hSceneDoc, - "0000000000011", "0000000000012", "0000000000013", "0000000000014", "0000000000015", - "0000000000016", "0000000000017", "0000000000018", "0000000000019", "000000000001a", - "000000000001b", "000000000001c", C.hPlotRoot, C.hCharRoot, C.hWorldRoot, + mp.setattr(QMessageBox, "result", lambda *a: QtRejected) + projTree.duplicateFromHandle(C.hTitlePage) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", ] - # Make the duplicator stop early - content = SHARED.project.storage.contentPath - assert isinstance(content, Path) - (content / "000000000001e.nwd").touch() - assert (content / "000000000001e.nwd").exists() + # Duplicate title page + projTree.duplicateFromHandle(C.hTitlePage) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Title Page", "New Folder", "New Chapter", + "New Scene", "Plot", "Characters", "Locations", "Trash", + ] - # Should only create the folder, and skip the two files because the - # next handle is already a file - assert projTree.duplicateFromHandle(C.hChapterDir) is True - assert len(SHARED.project.tree) == 22 + # Duplicate folder + projTree.duplicateFromHandle(C.hChapterDir) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Title Page", "New Folder", "New Chapter", + "New Scene", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] + + # Duplicate novel root + projTree.duplicateFromHandle(C.hNovelRoot) + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "Title Page", "New Folder", "New Chapter", + "New Scene", "New Folder", "New Chapter", "New Scene", + "Novel", "Title Page", "Title Page", "New Folder", "New Chapter", + "New Scene", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # qtbot.stop() @pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_AutoScroll(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd): - """Test the auto scroll feature.""" - buildTestProject(nwGUI, projPath) - projTree: GuiProjectTree = nwGUI.projView.projTree - - monkeypatch.setattr(GuiEditLabel, "getLabel", lambda parent, text: (text, True)) - monkeypatch.setattr(QTreeWidget, "dragMoveEvent", lambda *a, **k: None) - monkeypatch.setattr(QTimer, "isActive", lambda *a: False) - monkeypatch.setattr(QTimer, "start", lambda *a: None) - - projTree.setSelectedHandle(C.hChapterDir, True) - projTree.expandAll() - for i in range(100): - projTree.newTreeItem(nwItemType.FILE, None, 3, False) - - projTree.setSelectedHandle("0000000000015", True) - nwGUI.resize(500, 500) - - action = Qt.DropAction.MoveAction - mime = QMimeData() - mouse = QtMouseLeft - modifier = QtModNone - - # Scroll Down - h = projTree.height() - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-1), action, mime, mouse, modifier)) - assert projTree._scrollDirection == 1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-2), action, mime, mouse, modifier)) - assert projTree._scrollDirection == 1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-3), action, mime, mouse, modifier)) - assert projTree._scrollDirection == 1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-4), action, mime, mouse, modifier)) - assert projTree._scrollDirection == 1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - - # Scroll Up - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 1), action, mime, mouse, modifier)) - assert projTree._scrollDirection == -1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 2), action, mime, mouse, modifier)) - assert projTree._scrollDirection == -1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 3), action, mime, mouse, modifier)) - assert projTree._scrollDirection == -1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 4), action, mime, mouse, modifier)) - assert projTree._scrollDirection == -1 - projTree._doAutoScroll() - assert projTree._scrollDirection == 0 - - # qtbot.stop() - - -@pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_DragAndDrop(qtbot, monkeypatch, caplog, nwGUI: GuiMain, projPath, mockRnd): - """Test the auto scroll feature.""" - buildTestProject(nwGUI, projPath) - projTree: GuiProjectTree = nwGUI.projView.projTree - - monkeypatch.setattr(QTreeWidget, "dropEvent", lambda *a, **k: None) - - projTree.setSelectedHandle(C.hSceneDoc, True) - projTree._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore - - nPos = projTree.visualItemRect(projTree._getTreeItem(C.hNovelRoot)).bottomLeft() - action = Qt.DropAction.MoveAction - mime = QMimeData() - mouse = QtMouseLeft - modifier = QtModNone - - projTree.saveTreeOrder() - treeOrder = SHARED.project.tree._order - - # Move an item, but no selection - event = QDropEvent(nPos, action, mime, mouse, modifier) - projTree.dropEvent(event) - projTree.saveTreeOrder() - assert SHARED.project.tree._order == treeOrder - - # Invalid location - caplog.clear() - event = QDropEvent(QPoint(1000, 1000), action, mime, mouse, modifier) - projTree.dropEvent(event) - assert event.isAccepted() is False - assert "Invalid drop location" in caplog.text - projTree.saveTreeOrder() - assert SHARED.project.tree._order == treeOrder - - # Root item selected - caplog.clear() - event = QDropEvent(nPos, action, mime, mouse, modifier) - projTree.clearSelection() - projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore - projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore - projTree.dropEvent(event) - assert event.isAccepted() is False - projTree.saveTreeOrder() - assert SHARED.project.tree._order == treeOrder - - # Make sure illegal drag events are cancelled - with monkeypatch.context() as mp: - mp.setattr(QTreeWidget, "dragEnterEvent", lambda *a: None) - mime = QMimeData() - mime.setText("foobar") - event = QDragEnterEvent(nPos, action, mime, mouse, modifier) - projTree.clearSelection() - projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore - projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore - projTree._getTreeItem(C.hChapterDoc).setSelected(True) # type: ignore - assert projTree.selectedItems() == [ # Novel Root selection is cancelled automatically - projTree._getTreeItem(C.hTitlePage), projTree._getTreeItem(C.hChapterDoc) - ] - projTree.dragEnterEvent(event) - assert mime.text() == "" - assert projTree._popAlert is not None - - # Pop the alert - with monkeypatch.context() as mp: - mp.setattr(QTreeWidget, "startDrag", lambda *a: None) - projTree.startDrag(None) # type: ignore - assert projTree._popAlert is None - - # Valid drag events are processed - with monkeypatch.context() as mp: - mp.setattr(QTreeWidget, "dragEnterEvent", lambda *a: None) - mime = QMimeData() - mime.setText("foobar") - event = QDragEnterEvent(nPos, action, mime, mouse, modifier) - projTree.clearSelection() - projTree._getTreeItem(C.hChapterDoc).setSelected(True) # type: ignore - projTree._getTreeItem(C.hSceneDoc).setSelected(True) # type: ignore - assert projTree.selectedItems() == [ # Novel Root selection is cancelled automatically - projTree._getTreeItem(C.hChapterDoc), projTree._getTreeItem(C.hSceneDoc) - ] - projTree.dragEnterEvent(event) - assert mime.text() == "foobar" - assert projTree._popAlert is None - - # qtbot.stop() - - -@pytest.mark.gui -@pytest.mark.skip -def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd): +def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test various parts of the project tree class not covered by other tests. """ buildTestProject(nwGUI, projPath) - projView: GuiProjectView = nwGUI.projView - projTree: GuiProjectTree = nwGUI.projView.projTree + projView = nwGUI.projView + projTree = projView.projTree # Method: initSettings # ==================== @@ -977,224 +1024,100 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) assert projTree.verticalScrollBarPolicy() == QtScrollAsNeeded assert projTree.horizontalScrollBarPolicy() == QtScrollAsNeeded - # Method: revealNewTreeItem - # ========================= - - # Send invalid handle - assert projTree.revealNewTreeItem(C.hInvalid) is False - - # Try to add an orphaned file to the tree - nHandle = SHARED.project.newFile("Test", C.hNovelRoot) - SHARED.project.tree[nHandle].setParent(None) # type: ignore - assert projTree.revealNewTreeItem(nHandle) is False - - # Try to add an item with unknown parent to the tree - nHandle = SHARED.project.newFile("Test", C.hNovelRoot) - SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore - assert projTree.revealNewTreeItem(nHandle) is False - - # Slot: _treeDoubleClick - # ====================== - - # Try to open a file with nothings selected - projTree.clearSelection() - projTree._onDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle is None - - # When the item cannot be found - projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore - with monkeypatch.context() as mp: - mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None) - projTree._onDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle is None - - # Successfully open a file - projTree._onDoubleClick(projTree._getTreeItem(C.hTitlePage), 0) - assert nwGUI.docEditor.docHandle == C.hTitlePage - projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore - - # A non-file item should be expanded instead - projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore - projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore - projTree._onDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1) - assert nwGUI.docEditor.docHandle == C.hTitlePage - assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore - - # Navigate the Tree - # ================= - - # Expand handles - projTree.setExpandedFromHandle(C.hNovelRoot, True) - projTree.setSelectedHandle(C.hSceneDoc) - assert projTree.getSelectedHandle() == C.hSceneDoc - - # Move between documents in that folder - projTree.moveToNextItem(-1) - assert projTree.getSelectedHandle() == C.hChapterDoc - projTree.moveToNextItem(-1) # Can't move further up - assert projTree.getSelectedHandle() == C.hChapterDoc - projTree.moveToNextItem(1) - assert projTree.getSelectedHandle() == C.hSceneDoc - projTree.moveToNextItem(1) # Can't move further down - assert projTree.getSelectedHandle() == C.hSceneDoc - - # Move up/down the parent/child hierarchy - projTree.moveToLevel(-1) - assert projTree.getSelectedHandle() == C.hChapterDir - projTree.moveToLevel(-1) - assert projTree.getSelectedHandle() == C.hNovelRoot - projTree.moveToLevel(-1) # Can't move further up - assert projTree.getSelectedHandle() == C.hNovelRoot - projTree.moveToLevel(1) - assert projTree.getSelectedHandle() == C.hTitlePage - projTree.moveToLevel(1) # Can't move further down - assert projTree.getSelectedHandle() == C.hTitlePage - - # Move between roots - projTree.setSelectedHandle(C.hNovelRoot) - projTree.moveToNextItem(1) - assert projTree.getSelectedHandle() == C.hPlotRoot - projTree.moveToNextItem(1) - assert projTree.getSelectedHandle() == C.hCharRoot - projTree.moveToNextItem(1) - assert projTree.getSelectedHandle() == C.hWorldRoot - projTree.moveToNextItem(1) # Can't move further down - assert projTree.getSelectedHandle() == C.hWorldRoot - - # When nothing is selected, nothing happens - projTree.clearSelection() - assert projTree.getSelectedHandle() is None - projTree.moveToNextItem(-1) - assert projTree.getSelectedHandle() is None - projTree.moveToNextItem(1) - assert projTree.getSelectedHandle() is None - projTree.moveToLevel(-1) - assert projTree.getSelectedHandle() is None - projTree.moveToLevel(1) - assert projTree.getSelectedHandle() is None - - # Mouse Button Clicks - # =================== - - eType = QEvent.Type.MouseButtonPress - pos = projTree.visualItemRect(projTree._getTreeItem(C.hChapterDoc)).center() - button = QtMouseMiddle - modifier = QtModNone - - # Trigger the viewer - event = QMouseEvent(eType, pos, button, button, modifier) - projTree.mousePressEvent(event) - assert nwGUI.docViewer.docHandle == C.hChapterDoc - - # Trigger the left click clear - pos = QPoint(5000, 5000) - button = QtMouseLeft - event = QMouseEvent(eType, pos, button, button, modifier) - projTree.setSelectedHandle(C.hChapterDoc) - projTree.mousePressEvent(event) - assert projTree.selectedItems() == [] - - # Rename Item - # =========== - - with monkeypatch.context() as mp: - mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("FooBar", True)) - projTree.clearSelection() - assert SHARED.project.tree[C.hChapterDoc].itemName == "New Chapter" # type: ignore - projView.renameTreeItem(C.hChapterDoc) - assert SHARED.project.tree[C.hChapterDoc].itemName == "FooBar" # type: ignore - - projTree.setSelectedHandle(C.hSceneDoc) - assert SHARED.project.tree[C.hSceneDoc].itemName == "New Scene" # type: ignore - projView.renameTreeItem() - assert SHARED.project.tree[C.hSceneDoc].itemName == "FooBar" # type: ignore - - # Check Crash Resistance - # ====================== - projTree._postItemMove("dfghj") # This should exit cleanly - # qtbot.stop() @pytest.mark.gui -@pytest.mark.skip def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the building of the project tree context menu. All this does is test that the menu builds. It doesn't open the actual menu. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + QtSelect = QItemSelectionModel.SelectionFlag.Select + + # Create a project + projView = nwGUI.projView + projTree = projView.projTree + project = SHARED.project + tree = project.tree + model = tree.model # Create a project buildTestProject(nwGUI, projPath) - nwGUI.openProject(projPath) - nwGUI._switchFocus(nwFocus.TREE) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # Handles for new objects hCharNote = "0000000000011" hNovelNote = "0000000000012" - hSubNote = "0000000000013" - hNewFolderOne = "0000000000014" - hNewFolderTwo = "0000000000016" - - projView = nwGUI.projView - projTree = nwGUI.projView.projTree - projTree.setExpandedFromHandle(None, True) - - projTree._addTrashRoot() - hTrashRoot = SHARED.project.tree.trashRoot + hTrashDoc = "0000000000013" + hSubNote = "0000000000014" + hNewFolderOne = "0000000000015" + hNewFolderTwo = "0000000000017" + hTrashRoot = trash.item.itemHandle + projTree.expandAll() projTree.setSelectedHandle(C.hCharRoot) - projTree.newTreeItem(nwItemType.FILE) + projTree.newTreeItem(nwItemType.FILE, isNote=True) projTree.setSelectedHandle(C.hNovelRoot) projTree.newTreeItem(nwItemType.FILE, isNote=True) + projTree.setSelectedHandle(C.hNovelRoot) + projTree.newTreeItem(nwItemType.FILE) + projTree.setSelectedHandle(hTrashDoc) + projTree.processDeleteRequest() - SHARED.project.newFile("SubNote", hNovelNote) - projTree.revealNewTreeItem(hSubNote) - assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore + project.newFile("SubNote", hNovelNote) + item = tree[hSubNote] + assert item is not None + assert item.itemName == "SubNote" + assert item.itemParent == hNovelNote - def itemPos(tHandle): - return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "New Note", "SubNote", "Plot", "Characters", "New Note", "Locations", + "Trash", "New Document", + ] # Pop the menu in various positions and check for success with monkeypatch.context() as mp: mockMenu = MagicMock() mp.setattr(QMenu, "exec", mockMenu) - projTree.clearSelection() + projTree._clearSelection() # No item under menu projTree.openContextMenu(projTree.viewport().rect().bottomRight()) assert mockMenu.call_count == 0 # Open Trash Menu - projTree.openContextMenu(itemPos(hTrashRoot)) + projView.setSelectedHandle(hTrashRoot) + projTree.openContextMenu() assert mockMenu.call_count == 1 # Open Single Select Menu - projTree.openContextMenu(itemPos(C.hNovelRoot)) + projView.setSelectedHandle(C.hNovelRoot) + projTree.openContextMenu() assert mockMenu.call_count == 2 # Open Multi-Select Menu - projTree._getTreeItem(hNovelNote).setSelected(True) - projTree._getTreeItem(hSubNote).setSelected(True) - projTree.openContextMenu(itemPos(hCharNote)) + projTree.selectionModel().select(model.indexFromHandle(hNovelNote), QtSelect) + projTree.selectionModel().select(model.indexFromHandle(hSubNote), QtSelect) + projTree.openContextMenu() assert mockMenu.call_count == 3 - - # Check the keyboard shortcut handler as well - projTree.setSelectedHandle(C.hNovelRoot) - projTree.openContextMenu(None) - assert mockMenu.call_count == 4 projTree.clearSelection() - projTree.openContextMenu(None) - assert mockMenu.call_count == 4 # Menu Builders # ============= # Context Menu on Root Item - nwItem = SHARED.project.tree[C.hNovelRoot] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(True) + node = tree.nodes[C.hNovelRoot] + indices = [model.indexFromHandle(C.hNovelRoot)] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Create New ...", "Rename", "Set Status to ...", "Expand All", @@ -1202,10 +1125,10 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): ] # Context Menu on Folder Item - nwItem = SHARED.project.tree[C.hChapterDir] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(True) + node = tree.nodes[C.hChapterDir] + indices = [model.indexFromHandle(C.hChapterDir)] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All", @@ -1219,26 +1142,24 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): return [] # Context Menu on Document File Item - nwItem = SHARED.project.tree[C.hChapterDoc] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(True) + node = tree.nodes[C.hChapterDoc] + indices = [model.indexFromHandle(C.hChapterDoc)] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading", - "Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All", - "Duplicate", "Move to Trash", + "Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash", ] assert getTransformSubMenu(ctxMenu) == [ - "Convert to Project Note", "Merge Child Items into Self", - "Merge Child Items into New", "Split Document by Headings" + "Convert to Project Note", "Split Document by Headings" ] # Context Menu on Note File Item in Character Folder - nwItem = SHARED.project.tree[hCharNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(False) + node = tree.nodes[hCharNote] + indices = [model.indexFromHandle(hCharNote)] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading", @@ -1249,34 +1170,44 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): ] # Context Menu on Note File Item in Novel Tree - nwItem = SHARED.project.tree[hNovelNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(False) + node = tree.nodes[hNovelNote] + indices = [model.indexFromHandle(hNovelNote)] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading", - "Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash", + "Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All", + "Duplicate", "Move to Trash", ] assert getTransformSubMenu(ctxMenu) == [ - "Convert to Novel Document", "Split Document by Headings", + "Convert to Novel Document", "Merge Child Items into Self", + "Merge Child Items into New", "Split Document by Headings", ] # Context Menu on Multiple Items, Clicked on Document - nwItem = SHARED.project.tree[hNovelNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) + node = tree.nodes[hNovelNote] + indices = [ + model.indexFromHandle(hCharNote), + model.indexFromHandle(hNovelNote), + model.indexFromHandle(hSubNote), + ] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildMultiSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Set Active to ...", "Set Status to ...", "Move to Trash", ] # Context Menu on Multiple Items, Clicked on Note - nwItem = SHARED.project.tree[hCharNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) + node = tree.nodes[hCharNote] + indices = [ + model.indexFromHandle(hCharNote), + model.indexFromHandle(hNovelNote), + model.indexFromHandle(hSubNote), + ] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildMultiSelectMenu() actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Set Active to ...", "Set Importance to ...", "Move to Trash", @@ -1285,189 +1216,180 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Direct Edit Functions, Single # ============================= - nwItem = SHARED.project.tree[hNovelNote] - assert isinstance(nwItem, NWItem) + node = tree.nodes[hNovelNote] + indices = [model.indexFromHandle(hNovelNote)] - # # Toggle active flag - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(False) - assert nwItem.isActive is True + # Toggle active flag + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildSingleSelectMenu() + assert node.item.isActive is True ctxMenu._toggleItemActive() - assert nwItem.isActive is False + assert node.item.isActive is False # Change item status - assert nwItem.itemStatus == "s000000" + assert node.item.itemStatus == "s000000" ctxMenu._changeItemStatus("s000001") - assert nwItem.itemStatus == "s000001" + assert node.item.itemStatus == "s000001" # Change item importance - assert nwItem.itemImport == "i000004" + assert node.item.itemImport == "i000004" ctxMenu._changeItemImport("i000005") - assert nwItem.itemImport == "i000005" + assert node.item.itemImport == "i000005" # Change item layout - assert nwItem.itemLayout == nwItemLayout.NOTE + assert node.item.itemLayout == nwItemLayout.NOTE ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT) - assert nwItem.itemLayout == nwItemLayout.DOCUMENT + assert node.item.itemLayout == nwItemLayout.DOCUMENT ctxMenu._changeItemLayout(nwItemLayout.NOTE) - assert nwItem.itemLayout == nwItemLayout.NOTE + assert node.item.itemLayout == nwItemLayout.NOTE # Convert Folders to Documents # ============================ projView.setSelectedHandle(hNovelNote) - assert projTree.newTreeItem(nwItemType.FOLDER) is True + projTree.newTreeItem(nwItemType.FOLDER) projView.setSelectedHandle(hNewFolderOne) - assert projTree.newTreeItem(nwItemType.FILE) is True + projTree.newTreeItem(nwItemType.FILE) projView.setSelectedHandle(hNovelNote) - assert projTree.newTreeItem(nwItemType.FOLDER) is True + projTree.newTreeItem(nwItemType.FOLDER) projView.setSelectedHandle(hNewFolderTwo) - assert projTree.newTreeItem(nwItemType.FILE, isNote=True) is True + projTree.newTreeItem(nwItemType.FILE, isNote=True) - nwItem = SHARED.project.tree[hNewFolderOne] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(False) + assert hNewFolderOne in tree + assert hNewFolderTwo in tree + + nodeOne = tree.nodes[hNewFolderOne] + nodeTwo = tree.nodes[hNewFolderTwo] + + # Select Folder One + indices = [model.indexFromHandle(hNewFolderOne)] + ctxMenu = _TreeContextMenu(projTree, model, nodeOne, indices) + ctxMenu.buildSingleSelectMenu() # Click no on the dialog with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(QMessageBox, "result", lambda *a: QtRejected) ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT) - assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore + assert nodeOne.item.isFolderType() # Convert the first folder to a document + assert nodeOne.item.isFolderType() ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT) - assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore - assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore + assert nodeOne.item.isFileType() + assert nodeOne.item.isDocumentLayout() - nwItem = SHARED.project.tree[hNewFolderTwo] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildSingleSelectMenu(False) + # Select Folder Two + indices = [model.indexFromHandle(hNewFolderTwo)] + ctxMenu = _TreeContextMenu(projTree, model, nodeTwo, indices) + ctxMenu.buildSingleSelectMenu() # Convert the second folder to a note ctxMenu._convertFolderToFile(nwItemLayout.NOTE) - assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore - assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore + assert nodeTwo.item.isFileType() + assert nodeTwo.item.isNoteLayout() # Direct Edit Functions, Multi # ============================ - nwItem = SHARED.project.tree[hCharNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) + node = tree.nodes[hCharNote] + indices = [ + model.indexFromHandle(hCharNote), + model.indexFromHandle(hNovelNote), + model.indexFromHandle(hSubNote), + ] + ctxMenu = _TreeContextMenu(projTree, model, node, indices) + ctxMenu.buildMultiSelectMenu() - projTree.clearSelection() - projTree._getTreeItem(hCharNote).setSelected(True) - projTree._getTreeItem(hNovelNote).setSelected(True) - projTree._getTreeItem(hSubNote).setSelected(True) + # projTree.clearSelection() + # projTree._getTreeItem(hCharNote).setSelected(True) + # projTree._getTreeItem(hNovelNote).setSelected(True) + # projTree._getTreeItem(hSubNote).setSelected(True) + + nodeCNote = tree.nodes[hCharNote] + nodeNNote = tree.nodes[hNovelNote] + nodeSNote = tree.nodes[hSubNote] # Item Active - assert SHARED.project.tree[hCharNote].isActive is True # type: ignore - assert SHARED.project.tree[hNovelNote].isActive is False # type: ignore - assert SHARED.project.tree[hSubNote].isActive is True # type: ignore + assert nodeCNote.item.isActive is True + assert nodeNNote.item.isActive is False + assert nodeSNote.item.isActive is True ctxMenu._iterItemActive(False) - assert SHARED.project.tree[hCharNote].isActive is False # type: ignore - assert SHARED.project.tree[hNovelNote].isActive is False # type: ignore - assert SHARED.project.tree[hSubNote].isActive is False # type: ignore + assert nodeCNote.item.isActive is False + assert nodeNNote.item.isActive is False + assert nodeSNote.item.isActive is False ctxMenu._iterItemActive(True) - assert SHARED.project.tree[hCharNote].isActive is True # type: ignore - assert SHARED.project.tree[hNovelNote].isActive is True # type: ignore - assert SHARED.project.tree[hSubNote].isActive is True # type: ignore + assert nodeCNote.item.isActive is True + assert nodeNNote.item.isActive is True + assert nodeSNote.item.isActive is True # Item Status - assert SHARED.project.tree[hCharNote].itemStatus == "s000000" # type: ignore - assert SHARED.project.tree[hNovelNote].itemStatus == "s000001" # type: ignore - assert SHARED.project.tree[hSubNote].itemStatus == "s000000" # type: ignore + assert nodeCNote.item.itemStatus == "s000000" + assert nodeNNote.item.itemStatus == "s000001" + assert nodeSNote.item.itemStatus == "s000000" ctxMenu._iterSetItemStatus("s000003") - assert SHARED.project.tree[hCharNote].itemStatus == "s000000" # type: ignore - assert SHARED.project.tree[hNovelNote].itemStatus == "s000003" # type: ignore - assert SHARED.project.tree[hSubNote].itemStatus == "s000003" # type: ignore + assert nodeCNote.item.itemStatus == "s000000" + assert nodeNNote.item.itemStatus == "s000003" + assert nodeSNote.item.itemStatus == "s000003" # Item Importance - assert SHARED.project.tree[hCharNote].itemImport == "i000004" # type: ignore - assert SHARED.project.tree[hNovelNote].itemImport == "i000005" # type: ignore - assert SHARED.project.tree[hSubNote].itemImport == "i000004" # type: ignore + assert nodeCNote.item.itemImport == "i000004" + assert nodeNNote.item.itemImport == "i000005" + assert nodeSNote.item.itemImport == "i000004" ctxMenu._iterSetItemImport("i000007") - assert SHARED.project.tree[hCharNote].itemImport == "i000007" # type: ignore - assert SHARED.project.tree[hNovelNote].itemImport == "i000005" # type: ignore - assert SHARED.project.tree[hSubNote].itemImport == "i000004" # type: ignore - - # Move to Trash - assert SHARED.project.tree[hCharNote].itemRoot == C.hCharRoot # type: ignore - assert SHARED.project.tree[hNovelNote].itemRoot == C.hNovelRoot # type: ignore - assert SHARED.project.tree[hSubNote].itemRoot == C.hNovelRoot # type: ignore - ctxMenu._iterMoveToTrash() - assert SHARED.project.tree[hCharNote].itemRoot == hTrashRoot # type: ignore - assert SHARED.project.tree[hNovelNote].itemRoot == hTrashRoot # type: ignore - assert SHARED.project.tree[hSubNote].itemRoot == hTrashRoot # type: ignore - - # Permanently Delete Menu - nwItem = SHARED.project.tree[hCharNote] - assert isinstance(nwItem, NWItem) - ctxMenu = _TreeContextMenu(projTree, nwItem) - ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) - actions = [x.text() for x in ctxMenu.actions() if x.text()] - assert actions == [ - "Set Active to ...", "Set Importance to ...", "Delete Permanently", - ] - - # Permanently Delete - ctxMenu._iterPermDelete() - assert SHARED.project.tree[hCharNote] is None - assert SHARED.project.tree[hNovelNote] is None - assert SHARED.project.tree[hSubNote] is None + assert nodeCNote.item.itemImport == "i000007" + assert nodeNNote.item.itemImport == "i000005" + assert nodeSNote.item.itemImport == "i000004" # qtbot.stop() @pytest.mark.gui -@pytest.mark.skip def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the templates feature of the project tree.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a project - buildTestProject(nwGUI, projPath) - nwGUI.openProject(projPath) - nwGUI._switchFocus(nwFocus.TREE) - nwGUI.show() - - project = SHARED.project - projView = nwGUI.projView projTree = projView.projTree projBar = projView.projBar + project = SHARED.project + tree = project.tree + + # Create a project + buildTestProject(nwGUI, projPath) + trash = tree.trash + assert trash is not None + assert len(tree) == 9 + assert [n.item.itemName for n in tree.model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] # Handles for new objects - hTemplatesRoot = "0000000000010" - hSceneTemplate = "0000000000011" - hNoteTemplate = "0000000000012" - hNewScene = "0000000000013" - hNewCharacter = "0000000000014" + hTemplatesRoot = "0000000000011" + hSceneTemplate = "0000000000012" + hNoteTemplate = "0000000000013" + hNewScene = "0000000000014" + hNewCharacter = "0000000000015" # Add template folder projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE) - nwTemplateRoot = project.tree[hTemplatesRoot] + nwTemplateRoot = tree[hTemplatesRoot] assert nwTemplateRoot is not None assert nwTemplateRoot.itemName == "Templates" # Add a scene template projTree.setSelectedHandle(hTemplatesRoot) projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) - nwSceneTemplate = project.tree[hSceneTemplate] + nwSceneTemplate = tree[hSceneTemplate] assert nwSceneTemplate is not None assert nwSceneTemplate.itemName == "New Scene" assert projBar.mTemplates.actions()[0].text() == "New Scene" # Rename the scene template - with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal: - projTree.renameTreeItem(hSceneTemplate, name="Scene") - assert signal.args[0] == hSceneTemplate - assert signal.args[1].itemName == "Scene" - assert projBar.mTemplates.actions()[0].text() == "Scene" + projView.renameTreeItem(hSceneTemplate, name="Scene") + assert projBar.mTemplates.actions()[0].text() == "Scene" # Add a note template projTree.setSelectedHandle(hTemplatesRoot) @@ -1478,11 +1400,8 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert projBar.mTemplates.actions()[1].text() == "New Note" # Rename the note template - with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal: - projTree.renameTreeItem(hNoteTemplate, name="Note") - assert signal.args[0] == hNoteTemplate - assert signal.args[1].itemName == "Note" - assert projBar.mTemplates.actions()[1].text() == "Note" + projView.renameTreeItem(hNoteTemplate, name="Note") + assert projBar.mTemplates.actions()[1].text() == "Note" # Add new content to template files (projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n") @@ -1505,13 +1424,14 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n" # Remove the templates - with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal: - assert projBar.mTemplates.menuAction().isVisible() is True - assert len(projBar.mTemplates.actions()) == 2 - projTree.moveItemToTrash(hNoteTemplate) - assert len(projBar.mTemplates.actions()) == 1 - projTree.moveItemToTrash(hSceneTemplate) - assert len(projBar.mTemplates.actions()) == 0 - assert projBar.mTemplates.menuAction().isVisible() is False + assert projBar.mTemplates.menuAction().isVisible() is True + assert len(projBar.mTemplates.actions()) == 2 + assert trash.childCount() == 0 + projTree.processDeleteRequest([hNoteTemplate]) + assert trash.childCount() == 1 + assert len(projBar.mTemplates.actions()) == 1 + projTree.processDeleteRequest([hSceneTemplate]) + assert len(projBar.mTemplates.actions()) == 0 + assert projBar.mTemplates.menuAction().isVisible() is False # qtbot.stop()