From 62ea54e1ed35ac565dbfd368e3ac3979e9915413 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 22 Nov 2023 18:08:34 +0100 Subject: [PATCH] Add test coverage of tree context menu class --- tests/test_gui/test_gui_projtree.py | 408 +++++++++++++++++++--------- 1 file changed, 287 insertions(+), 121 deletions(-) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c2163081..cbc603ed 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -23,6 +23,7 @@ from __future__ import annotations import pytest from pathlib import Path +from novelwriter.core.item import NWItem from tools import C, buildTestProject from mocked import causeOSError @@ -32,9 +33,9 @@ from PyQt5.QtCore import QMimeData, QPoint, QTimer, Qt from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidget, QTreeWidgetItem, QDialog from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass +from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass, nwWidget from novelwriter.guimain import GuiMain -from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView +from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel @@ -514,125 +515,6 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock # END Test testGuiProjTree_EmptyTrash -@pytest.mark.gui -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)) - monkeypatch.setattr(QMenu, "exec_", lambda *a: None) - - # Create a project - buildTestProject(nwGUI, projPath) - - # 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 - - projTree.setSelectedHandle(C.hCharRoot) - projTree.newTreeItem(nwItemType.FILE) - projTree.setSelectedHandle(C.hNovelRoot) - projTree.newTreeItem(nwItemType.FILE, isNote=True) - - SHARED.project.newFile("SubNote", hNovelNote) - projTree.revealNewTreeItem(hSubNote) - assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore - - def itemPos(tHandle): - return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() - - # No item under menu - assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False - - # Generate the possible menu combinations - assert projTree._openContextMenu(itemPos(hTrashRoot)) is True - assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True - assert projTree._openContextMenu(itemPos(hNovelNote)) is True - assert projTree._openContextMenu(itemPos(C.hTitlePage)) is True - assert projTree._openContextMenu(itemPos(C.hChapterDir)) is True - assert projTree._openContextMenu(itemPos(C.hChapterDoc)) is True - assert projTree._openContextMenu(itemPos(C.hCharRoot)) is True - assert projTree._openContextMenu(itemPos(hCharNote)) is True - assert projTree._openContextMenu(itemPos(hNovelNote)) is True - - # Check the keyboard shortcut handler as well - projTree.setSelectedHandle(C.hNovelRoot) - assert projTree.openContextOnSelected() is True - projTree.clearSelection() - assert projTree.openContextOnSelected() is False - - # Direct Edit Functions - # ===================== - # Trigger the dedicated functions the menu entries connect to - nwItem = SHARED.project.tree[hNovelNote] - assert nwItem is not None - - # Toggle active flag - assert nwItem.isActive is True - projTree._toggleItemActive(hNovelNote) - assert nwItem.isActive is False - - # Change item status - assert nwItem.itemStatus == "s000000" - projTree._changeItemStatus(hNovelNote, "s000001") - assert nwItem.itemStatus == "s000001" - - # Change item importance - assert nwItem.itemImport == "i000004" - projTree._changeItemImport(hNovelNote, "i000005") - assert nwItem.itemImport == "i000005" - - # Change item layout - assert nwItem.itemLayout == nwItemLayout.NOTE - projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT) - assert nwItem.itemLayout == nwItemLayout.DOCUMENT - projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE) - assert nwItem.itemLayout == nwItemLayout.NOTE - - # Convert Folders to Documents - # ============================ - - projView.setSelectedHandle(hNovelNote) - assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True - projView.setSelectedHandle(hNewFolderOne) - assert projView.projTree.newTreeItem(nwItemType.FILE) is True - - projView.setSelectedHandle(hNovelNote) - assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True - projView.setSelectedHandle(hNewFolderTwo) - assert projView.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True - - # Click no on the dialog - with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) - projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) - assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore - - # Convert the first folder to a document - projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) - assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore - assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore - - # Convert the second folder to a note - projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE) - assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore - assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore - - # qtbot.stop() - -# END Test testGuiProjTree_ContextMenu - - @pytest.mark.gui def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): """Test the merge document function.""" @@ -1141,3 +1023,287 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) # qtbot.stop() # END Test testGuiProjTree_Other + + +@pytest.mark.gui +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)) + + # Create a project + buildTestProject(nwGUI, projPath) + nwGUI.openProject(projPath) + nwGUI.switchFocus(nwWidget.TREE) + + # 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 + + projTree.setSelectedHandle(C.hCharRoot) + projTree.newTreeItem(nwItemType.FILE) + projTree.setSelectedHandle(C.hNovelRoot) + projTree.newTreeItem(nwItemType.FILE, isNote=True) + + SHARED.project.newFile("SubNote", hNovelNote) + projTree.revealNewTreeItem(hSubNote) + assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore + + def itemPos(tHandle): + return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() + + # Pop the menu + with monkeypatch.context() as mp: + mp.setattr(QMenu, "exec_", lambda *a: None) + projTree.clearSelection() + + # No item under menu + assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False + + # Open Trash Menu + assert projTree._openContextMenu(itemPos(hTrashRoot)) is True + + # Open Single Select Menu + assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True + + # Open Multi-Select Menu + projTree._getTreeItem(hNovelNote).setSelected(True) + projTree._getTreeItem(hSubNote).setSelected(True) + assert projTree._openContextMenu(itemPos(hCharNote)) is True + + # Check the keyboard shortcut handler as well + projTree.setSelectedHandle(C.hNovelRoot) + assert projTree.openContextOnSelected() is True + projTree.clearSelection() + assert projTree.openContextOnSelected() is False + + # Menu Builders + # ============= + + # Context Menu on Root Item + nwItem = SHARED.project.tree[C.hNovelRoot] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(True) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Rename", "Set Status to ...", "Expand All", "Collapse All", + "Duplicate from Here", "Delete Permanently", + ] + + # Context Menu on Folder Item + nwItem = SHARED.project.tree[C.hChapterDir] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(True) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Rename", "Set Status to ...", "Transform", "Expand All", "Collapse All", + "Duplicate from Here", "Move to Trash", + ] + + def getTransformSubMenu(menu: QMenu) -> list[str]: + for action in menu.actions(): + if action.text() == "Transform": + return [x.text() for x in action.menu().actions() if x.text()] + return [] + + # Context Menu on Document File Item + nwItem = SHARED.project.tree[C.hChapterDoc] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(True) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Open Document", "View Document", "Rename", "Toggle Active", "Set Status to ...", + "Transform", "Expand All", "Collapse All", "Duplicate from Here", "Move to Trash", + ] + assert getTransformSubMenu(ctxMenu) == [ + "Convert to Project Note", "Merge Child Items into Self", + "Merge Child Items into New", "Split Document by Headers" + ] + + # 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) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Open Document", "View Document", "Rename", "Toggle Active", "Set Importance to ...", + "Transform", "Duplicate Document", "Move to Trash", + ] + assert getTransformSubMenu(ctxMenu) == [ + "Split Document by Headers", + ] + + # 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) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Open Document", "View Document", "Rename", "Toggle Active", "Set Status to ...", + "Transform", "Duplicate Document", "Move to Trash", + ] + assert getTransformSubMenu(ctxMenu) == [ + "Convert to Novel Document", "Split Document by Headers", + ] + + # 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]) + 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]) + actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert actions == [ + "Set Active to ...", "Set Importance to ...", "Move to Trash", + ] + + # Direct Edit Functions, Single + # ============================= + + nwItem = SHARED.project.tree[hNovelNote] + assert isinstance(nwItem, NWItem) + + # # Toggle active flag + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(False) + assert nwItem.isActive is True + ctxMenu._toggleItemActive() + assert nwItem.isActive is False + + # Change item status + assert nwItem.itemStatus == "s000000" + ctxMenu._changeItemStatus("s000001") + assert nwItem.itemStatus == "s000001" + + # Change item importance + assert nwItem.itemImport == "i000004" + ctxMenu._changeItemImport("i000005") + assert nwItem.itemImport == "i000005" + + # Change item layout + assert nwItem.itemLayout == nwItemLayout.NOTE + ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT) + assert nwItem.itemLayout == nwItemLayout.DOCUMENT + ctxMenu._changeItemLayout(nwItemLayout.NOTE) + assert nwItem.itemLayout == nwItemLayout.NOTE + + # Convert Folders to Documents + # ============================ + + projView.setSelectedHandle(hNovelNote) + assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True + projView.setSelectedHandle(hNewFolderOne) + assert projView.projTree.newTreeItem(nwItemType.FILE) is True + + projView.setSelectedHandle(hNovelNote) + assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True + projView.setSelectedHandle(hNewFolderTwo) + assert projView.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True + + nwItem = SHARED.project.tree[hNewFolderOne] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(False) + + # Click no on the dialog + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) + ctxMenu._covertFolderToFile(nwItemLayout.DOCUMENT) + assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore + + # Convert the first folder to a document + ctxMenu._covertFolderToFile(nwItemLayout.DOCUMENT) + assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore + assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore + + nwItem = SHARED.project.tree[hNewFolderTwo] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildSingleSelectMenu(False) + + # Convert the second folder to a note + ctxMenu._covertFolderToFile(nwItemLayout.NOTE) + assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore + assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore + + # Direct Edit Functions, Multi + # ============================ + + nwItem = SHARED.project.tree[hCharNote] + assert isinstance(nwItem, NWItem) + ctxMenu = _TreeContextMenu(projTree, nwItem) + ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote]) + + projTree.clearSelection() + projTree._getTreeItem(hCharNote).setSelected(True) + projTree._getTreeItem(hNovelNote).setSelected(True) + projTree._getTreeItem(hSubNote).setSelected(True) + + # 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 + 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 + 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 + + # 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 + 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 + + # 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 + 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 + + # qtbot.stop() + +# END Test testGuiProjTree_ContextMenu