1494 lines
57 KiB
Python
1494 lines
57 KiB
Python
"""
|
||
novelWriter – Main GUI Project Tree Class Tester
|
||
================================================
|
||
|
||
This file is a part of novelWriter
|
||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful, but
|
||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
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 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 nwItemClass, nwItemLayout, nwItemType, nwWidget
|
||
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu
|
||
from novelwriter.guimain import GuiMain
|
||
from novelwriter.types import QtAccepted, QtModeNone, QtMouseLeft, QtMouseMiddle, QtRejected
|
||
|
||
from tests.mocked import causeOSError
|
||
from tests.tools import C, buildTestProject
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
|
||
"""Test adding and removing items from the project tree."""
|
||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||
|
||
projView = nwGUI.projView
|
||
projTree = nwGUI.projView.projTree
|
||
project = SHARED.project
|
||
|
||
# Try to add item with no project
|
||
assert projTree.newTreeItem(nwItemType.FILE) is False
|
||
|
||
# Create a project
|
||
buildTestProject(nwGUI, projPath)
|
||
|
||
# No itemType set
|
||
projTree.clearSelection()
|
||
assert projTree.newTreeItem(None) is False
|
||
|
||
# Root Items
|
||
# ==========
|
||
|
||
# No class set
|
||
assert projTree.newTreeItem(nwItemType.ROOT) is False
|
||
|
||
# Create root item
|
||
assert projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
|
||
assert "0000000000010" in project.tree
|
||
|
||
# File/Folder Items
|
||
# =================
|
||
|
||
# No location selected for new item
|
||
projTree.clearSelection()
|
||
caplog.clear()
|
||
assert projTree.newTreeItem(nwItemType.FILE) is False
|
||
assert projTree.newTreeItem(nwItemType.FOLDER) is False
|
||
assert "Did not find anywhere" 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
|
||
|
||
# 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
|
||
|
||
# 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")
|
||
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
|
||
assert projTree._getItemWordCount("0000000000013") == 2
|
||
|
||
# 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")
|
||
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
|
||
assert projTree._getItemWordCount("0000000000014") == 2
|
||
|
||
# Add a new scene with the content copied from the previous
|
||
assert nwGUI.openDocument("0000000000014")
|
||
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")
|
||
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
|
||
assert projTree._getItemWordCount("0000000000015") == 4
|
||
|
||
# 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")
|
||
assert nwGUI.docEditor.getText() == "# New Note\n\n"
|
||
assert projTree._getItemWordCount("0000000000016") == 2
|
||
|
||
# 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
|
||
|
||
# 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
|
||
|
||
# Get the trash folder
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(NWProject, "trashFolder", lambda *a: None)
|
||
assert projTree._addTrashRoot() is None
|
||
|
||
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
|
||
|
||
# Rename Item
|
||
# ===========
|
||
|
||
# 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
|
||
|
||
# Clean up
|
||
# qtbot.stop()
|
||
nwGUI.closeProject()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||
"""Test adding and removing items from the project tree."""
|
||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||
|
||
projView = nwGUI.projView
|
||
projTree = nwGUI.projView.projTree
|
||
|
||
# Try to move item with no project
|
||
assert projTree.moveTreeItem(1) is False
|
||
|
||
# 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",
|
||
]
|
||
|
||
# Move with no selections
|
||
projTree.clearSelection()
|
||
assert projTree.moveTreeItem(1) is False
|
||
|
||
# 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",
|
||
]
|
||
|
||
# Restore
|
||
assert projTree.moveTreeItem(1) is True
|
||
assert projTree.getTreeFromHandle(C.hChapterDir) == [
|
||
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
|
||
"0000000000010", "0000000000011", "0000000000012",
|
||
]
|
||
|
||
# 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",
|
||
]
|
||
|
||
# Restore
|
||
assert projTree.moveTreeItem(-1) is True
|
||
assert projTree.getTreeFromHandle(C.hChapterDir) == [
|
||
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
|
||
"0000000000010", "0000000000011", "0000000000012",
|
||
]
|
||
|
||
# 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",
|
||
]
|
||
|
||
# Root Folder
|
||
# ===========
|
||
|
||
projView.setSelectedHandle(C.hNovelRoot)
|
||
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
|
||
|
||
# Move novel folder up
|
||
assert projTree.moveTreeItem(-1) is False
|
||
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
|
||
|
||
# Move novel folder down
|
||
assert projTree.moveTreeItem(1) is True
|
||
assert SHARED.project.tree._order.index(C.hNovelRoot) == 1
|
||
|
||
# Move novel folder up again
|
||
assert projTree.moveTreeItem(-1) is True
|
||
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
|
||
|
||
# Clean up
|
||
# qtbot.stop()
|
||
nwGUI.closeProject()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
|
||
"""Test external requests for removing items from project tree."""
|
||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||
|
||
projView = nwGUI.projView
|
||
projTree = nwGUI.projView.projTree
|
||
|
||
# Try to run with no project
|
||
assert projView.requestDeleteItem() is False
|
||
|
||
# Create a project
|
||
buildTestProject(nwGUI, projPath)
|
||
|
||
# Try emptying the trash already now, when there is no trash folder
|
||
assert projView.emptyTrash() is False
|
||
|
||
# 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",
|
||
]
|
||
|
||
# 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)
|
||
|
||
# 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
|
||
# ===================
|
||
|
||
assert projView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked
|
||
assert projView.requestDeleteItem(C.hCharRoot) is True # Character Root
|
||
|
||
# Delete File
|
||
# ===========
|
||
|
||
# Block adding trash folder
|
||
funcPointer = projTree._addTrashRoot
|
||
projTree._addTrashRoot = lambda *a: None
|
||
assert projView.requestDeleteItem("0000000000012") is False
|
||
projTree._addTrashRoot = funcPointer
|
||
|
||
# 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"
|
||
]
|
||
|
||
# Try to delete the trash folder
|
||
caplog.clear()
|
||
assert projView.requestDeleteItem("0000000000013") is False
|
||
assert "Cannot delete the Trash folder" in caplog.text
|
||
|
||
nwGUI.closeProject()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
|
||
"""Test moving items to Trash."""
|
||
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.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
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
|
||
assert projTree.moveItemToTrash(C.hTitlePage) is False
|
||
assert project.tree.isTrash(C.hTitlePage) is False
|
||
|
||
# Move a document to Trash
|
||
assert projTree.moveItemToTrash(C.hTitlePage) is True
|
||
assert project.tree.isTrash(C.hTitlePage) is True
|
||
|
||
# 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
|
||
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
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
|
||
assert projTree.permDeleteItem(C.hTitlePage) is False
|
||
assert C.hTitlePage in project.tree
|
||
|
||
# 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
|
||
|
||
# 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
|
||
|
||
nwGUI.closeProject()
|
||
|
||
|
||
@pytest.mark.gui
|
||
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
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.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
|
||
|
||
# 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
|
||
|
||
# Running Empty Trash again is cancelled due to empty folder
|
||
assert projTree.emptyTrash() is False
|
||
|
||
nwGUI.closeProject()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
|
||
"""Test the merge document function."""
|
||
mergeData = {}
|
||
|
||
monkeypatch.setattr(GuiDocMerge, "__init__", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocMerge, "exec", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocMerge, "deleteLater", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted)
|
||
monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData)
|
||
|
||
buildTestProject(nwGUI, projPath)
|
||
|
||
project = SHARED.project
|
||
projTree = nwGUI.projView.projTree
|
||
|
||
mergedDoc1 = "0000000000014"
|
||
|
||
# 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
|
||
|
||
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
|
||
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
|
||
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
|
||
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
|
||
|
||
lenText1 = len(docText1)
|
||
lenText2 = len(docText2)
|
||
lenText3 = len(docText3)
|
||
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
|
||
|
||
projTree.revealNewTreeItem(hChapter1)
|
||
projTree.revealNewTreeItem(hSceneOne11)
|
||
projTree.revealNewTreeItem(hSceneOne12)
|
||
projTree.revealNewTreeItem(hSceneOne13)
|
||
|
||
# Invalid file handle
|
||
assert projTree._mergeDocuments(C.hInvalid, False) is False
|
||
|
||
# Cannot merge root item
|
||
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
|
||
|
||
# Merge to New Doc
|
||
# ================
|
||
|
||
# Set merge job for new documents
|
||
mergeData["finalItems"] = [hChapter1, hSceneOne11, hSceneOne12, hSceneOne13]
|
||
mergeData["moveToTrash"] = False
|
||
|
||
# User cancels merge
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected)
|
||
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
|
||
|
||
# Merge to Existing Doc
|
||
# =====================
|
||
|
||
# Set merge job for parent document
|
||
mergeData["finalItems"] = [hSceneOne11, hSceneOne12, hSceneOne13]
|
||
mergeData["moveToTrash"] = False
|
||
|
||
# Merging to a folder is not allowed
|
||
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
|
||
|
||
# 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
|
||
|
||
assert project.tree.isTrash(hSceneOne11) # type: ignore
|
||
assert project.tree.isTrash(hSceneOne12) # type: ignore
|
||
assert project.tree.isTrash(hSceneOne13) # type: ignore
|
||
|
||
# qtbot.stop()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
|
||
"""Test the split document function."""
|
||
splitData = {}
|
||
splitText = []
|
||
|
||
monkeypatch.setattr(GuiDocSplit, "__init__", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocSplit, "exec", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocSplit, "deleteLater", lambda *a: None)
|
||
monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted)
|
||
monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText))
|
||
|
||
# Create a project
|
||
buildTestProject(nwGUI, projPath)
|
||
|
||
project = SHARED.project
|
||
projTree = nwGUI.projView.projTree
|
||
|
||
docText = (
|
||
"Text\n\n"
|
||
"##! Prologue\n\nText\n\n"
|
||
"## Chapter One\n\nText\n\n"
|
||
"### Scene One\n\nText\n\n"
|
||
"### Scene Two\n\nText\n\n"
|
||
"## Chapter Two\n\nText\n\n"
|
||
"### Scene Three\n\nText\n\n"
|
||
"### Scene Four\n\nText\n\n"
|
||
"#! New Title\n\nText\n\n"
|
||
"## New Chapter\n\nText\n\n"
|
||
"### New Scene\n\nText\n\n"
|
||
"#### New Section\n\nText\n\n"
|
||
)
|
||
|
||
hSplitDoc = project.newFile("Split Doc", C.hNovelRoot)
|
||
project.writeNewFile(hSplitDoc, 1, True, docText) # type: ignore
|
||
projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True)
|
||
|
||
docText = f"# Split Doc\n\n{docText}"
|
||
splitData["headerList"] = [
|
||
(0, 1, "Split Doc"),
|
||
(4, 2, "Prologue"),
|
||
(8, 2, "Chapter One"),
|
||
(12, 3, "Scene One"),
|
||
(16, 3, "Scene Two"),
|
||
(20, 2, "Chapter Two"),
|
||
(24, 3, "Scene Three"),
|
||
(28, 3, "Scene Four"),
|
||
(32, 1, "New Title"),
|
||
(36, 2, "New Chapter"),
|
||
(40, 3, "New Scene"),
|
||
(44, 4, "New Section"),
|
||
]
|
||
|
||
fstSet = [
|
||
"0000000000011", "0000000000012", "0000000000013", "0000000000014",
|
||
"0000000000015", "0000000000016", "0000000000017", "0000000000018",
|
||
"0000000000019", "000000000001a", "000000000001b", "000000000001c",
|
||
]
|
||
sndSet = [
|
||
"000000000001d", "000000000001e", "000000000001f", "0000000000020",
|
||
"0000000000021", "0000000000022", "0000000000023", "0000000000024",
|
||
"0000000000025", "0000000000026", "0000000000027", "0000000000028",
|
||
]
|
||
trdSet = [
|
||
"000000000002a", "000000000002b", "000000000002c", "000000000002d",
|
||
"000000000002e", "000000000002f", "0000000000030", "0000000000031",
|
||
"0000000000032", "0000000000033", "0000000000034", "0000000000035",
|
||
]
|
||
|
||
# Try to split an invalid document and a non-document
|
||
assert projTree._splitDocument(C.hInvalid) is False
|
||
assert projTree._splitDocument(C.hNovelRoot) is False
|
||
|
||
# Split into same root folder
|
||
splitData["intoFolder"] = False
|
||
|
||
# Writing fails
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr("builtins.open", causeOSError)
|
||
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
|
||
for tHandle in sndSet:
|
||
assert tHandle in project.tree
|
||
assert (projPath / "content" / f"{tHandle}.nwd").is_file()
|
||
|
||
# Add to a folder and move source to trash
|
||
splitData["intoFolder"] = True
|
||
splitData["moveToTrash"] = 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
|
||
|
||
# Cancelled by user
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected)
|
||
assert projTree._splitDocument(hSplitDoc) is False
|
||
|
||
# qtbot.stop()
|
||
|
||
|
||
@pytest.mark.gui
|
||
def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
|
||
"""Test the duplicate items function."""
|
||
# Create a project
|
||
buildTestProject(nwGUI, projPath)
|
||
assert len(SHARED.project.tree) == 8
|
||
|
||
projTree = nwGUI.projView.projTree
|
||
projTree._getTreeItem(C.hNovelRoot).setExpanded(True) # type: ignore
|
||
projTree._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore
|
||
|
||
# Nothing to do
|
||
assert projTree._duplicateFromHandle(C.hInvalid) is False
|
||
assert len(SHARED.project.tree) == 8
|
||
|
||
# Duplicate title page, but select no
|
||
with monkeypatch.context() as mp:
|
||
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.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,
|
||
]
|
||
|
||
# Make the duplicator stop early
|
||
content = SHARED.project.storage.contentPath
|
||
assert isinstance(content, Path)
|
||
(content / "000000000001e.nwd").touch()
|
||
assert (content / "000000000001e.nwd").exists()
|
||
|
||
# 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
|
||
|
||
# qtbot.stop()
|
||
|
||
|
||
@pytest.mark.gui
|
||
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._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore
|
||
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 = QtModeNone
|
||
|
||
# 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
|
||
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 = QtModeNone
|
||
|
||
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
|
||
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, 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
|
||
|
||
# Method: initSettings
|
||
# ====================
|
||
|
||
# Test that the scrollbar setting works
|
||
CONFIG.hideVScroll = True
|
||
CONFIG.hideHScroll = True
|
||
projView.initSettings()
|
||
assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||
assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
|
||
|
||
CONFIG.hideVScroll = False
|
||
CONFIG.hideHScroll = False
|
||
projView.initSettings()
|
||
assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||
assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
|
||
|
||
# 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._treeDoubleClick(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._treeDoubleClick(QTreeWidgetItem(), 0)
|
||
assert nwGUI.docEditor.docHandle is None
|
||
|
||
# Successfully open a file
|
||
projTree._treeDoubleClick(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._treeDoubleClick(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 = QtModeNone
|
||
|
||
# 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
|
||
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 == [
|
||
"Create New ...", "Rename", "Set Status to ...", "Expand All",
|
||
"Collapse All", "Duplicate", "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 == [
|
||
"Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All",
|
||
"Collapse All", "Duplicate", "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", "Create New ...", "Rename", "Rename to Heading",
|
||
"Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
|
||
"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"
|
||
]
|
||
|
||
# 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", "Create New ...", "Rename", "Rename to Heading",
|
||
"Toggle Active", "Set Importance to ...", "Transform ...", "Duplicate", "Move to Trash",
|
||
]
|
||
assert getTransformSubMenu(ctxMenu) == [
|
||
"Split Document by Headings",
|
||
]
|
||
|
||
# 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", "Create New ...", "Rename", "Rename to Heading",
|
||
"Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
|
||
]
|
||
assert getTransformSubMenu(ctxMenu) == [
|
||
"Convert to Novel Document", "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])
|
||
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 projTree.newTreeItem(nwItemType.FOLDER) is True
|
||
projView.setSelectedHandle(hNewFolderOne)
|
||
assert projTree.newTreeItem(nwItemType.FILE) is True
|
||
|
||
projView.setSelectedHandle(hNovelNote)
|
||
assert projTree.newTreeItem(nwItemType.FOLDER) is True
|
||
projView.setSelectedHandle(hNewFolderTwo)
|
||
assert 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
|
||
|
||
# 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
|
||
|
||
# qtbot.stop()
|
||
|
||
|
||
@pytest.mark.gui
|
||
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(nwWidget.TREE)
|
||
nwGUI.show()
|
||
|
||
project = SHARED.project
|
||
|
||
projView = nwGUI.projView
|
||
projTree = projView.projTree
|
||
projBar = projView.projBar
|
||
|
||
# Handles for new objects
|
||
hTemplatesRoot = "0000000000010"
|
||
hSceneTemplate = "0000000000011"
|
||
hNoteTemplate = "0000000000012"
|
||
hNewScene = "0000000000013"
|
||
hNewCharacter = "0000000000014"
|
||
|
||
# Add template folder
|
||
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
|
||
nwTemplateRoot = project.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]
|
||
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"
|
||
|
||
# Add a note template
|
||
projTree.setSelectedHandle(hTemplatesRoot)
|
||
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
|
||
nwNoteTemplate = project.tree[hNoteTemplate]
|
||
assert nwNoteTemplate is not None
|
||
assert nwNoteTemplate.itemName == "New Note"
|
||
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"
|
||
|
||
# Add new content to template files
|
||
(projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
|
||
(projPath / "content" / f"{hNoteTemplate}.nwd").write_text("# Jane\n\n@tag: Jane\n\n")
|
||
|
||
# Add a new scene using the template
|
||
projTree.setSelectedHandle(C.hSceneDoc, doScroll=True)
|
||
projBar.mTemplates.actions()[0].trigger()
|
||
nwNewScene = project.tree[hNewScene]
|
||
assert nwNewScene is not None
|
||
assert nwNewScene.itemName == "Scene"
|
||
assert project.storage.getDocument(hNewScene).readDocument() == "### Scene\n\n@pov: Jane\n\n"
|
||
|
||
# Add a new note using the template
|
||
projTree.setSelectedHandle(C.hCharRoot, doScroll=True)
|
||
projBar.mTemplates.actions()[1].trigger()
|
||
nwNewCharacter = project.tree[hNewCharacter]
|
||
assert nwNewCharacter is not None
|
||
assert nwNewCharacter.itemName == "Note"
|
||
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
|
||
|
||
# qtbot.stop()
|