Update test coverage
This commit is contained in:
+13
-12
@@ -256,6 +256,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""Reveal a newly added project item in the project tree.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is None:
|
||||
return False
|
||||
|
||||
trItem = self._addTreeItem(nwItem, nHandle)
|
||||
if trItem is None:
|
||||
return False
|
||||
@@ -459,13 +462,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
if doPermanent:
|
||||
logger.debug("Permanently deleting file with handle '%s'", tHandle)
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
|
||||
if self.theParent.docEditor.docHandle() == tHandle:
|
||||
self.theParent.closeDocument()
|
||||
|
||||
delDoc = NWDoc(self.theProject, tHandle)
|
||||
if not delDoc.deleteDocument():
|
||||
self.theParent.makeAlert([
|
||||
@@ -473,6 +469,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
|
||||
if self.theParent.docEditor.docHandle() == tHandle:
|
||||
self.theParent.closeDocument()
|
||||
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
@@ -486,13 +489,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
|
||||
)
|
||||
if msgYes:
|
||||
if pHandle is None:
|
||||
logger.warning("File has no parent item")
|
||||
|
||||
logger.debug("Moving file '%s' to trash", tHandle)
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
trItemT.addChild(trItemC)
|
||||
self._updateItemParent(tHandle)
|
||||
@@ -508,6 +508,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
if trItemP is None:
|
||||
logger.error("Could not delete folder")
|
||||
return False
|
||||
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
trItemP.takeChild(tIndex)
|
||||
@@ -993,7 +994,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
if nwItem.itemClass in nwLists.CLS_NOVEL:
|
||||
self.novelItemChanged.emit()
|
||||
else:
|
||||
self.noteItemChanged.emit()
|
||||
|
||||
+3
-1
@@ -166,7 +166,9 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
|
||||
"""
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr("novelwriter.CONFIG", fncConf)
|
||||
nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir])
|
||||
nwGUI = novelwriter.main(
|
||||
["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir]
|
||||
)
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.wait(20)
|
||||
|
||||
@@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
assert theProject.openProject(nwLipsum)
|
||||
assert theProject.openProject(nwLipsum) is True
|
||||
assert theProject.projTree["636b6aa9b697b"] is None
|
||||
assert theProject.closeProject()
|
||||
|
||||
# Add a file with non-existent parent
|
||||
# This file will be renoved from the project on open
|
||||
assert theProject.newFile("Oops", "0000000000000")
|
||||
|
||||
# Save and close
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
# First Item with Meta Data
|
||||
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
|
||||
|
||||
@@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
@@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles
|
||||
def mockItems(mockGUI):
|
||||
"""Create a list of mock items.
|
||||
"""
|
||||
random.seed(42)
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
itemA = NWItem(theProject)
|
||||
@@ -151,8 +153,28 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
# Check that we have the correct archive and trash folders
|
||||
assert theTree.trashRoot() == "a000000000003"
|
||||
assert theTree.archiveRoot() == "a000000000002"
|
||||
assert theTree.isTrashRoot("a000000000003")
|
||||
assert theTree.isRoot("a000000000002")
|
||||
assert theTree.isTrashRoot("a000000000003") is True
|
||||
assert theTree.isRoot("a000000000002") is True
|
||||
|
||||
# Check the isTrash function
|
||||
assert theTree.isTrash("0000000000000") is True # Doesn't exist
|
||||
assert theTree.isTrash("a000000000003") is True # This the trash folder
|
||||
|
||||
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
|
||||
assert theTree.isTrash("a000000000003") is True # This is still trash
|
||||
theTree["a000000000003"].setClass(nwItemClass.TRASH)
|
||||
|
||||
assert theTree.isTrash("b000000000002") is False # This is not trash
|
||||
|
||||
value = theTree["b000000000002"].itemParent
|
||||
theTree["b000000000002"].setParent("a000000000003")
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setParent(value)
|
||||
|
||||
value = theTree["b000000000002"].itemRoot
|
||||
theTree["b000000000002"].setRoot("a000000000003")
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setRoot(value)
|
||||
|
||||
# Try to add another trash folder
|
||||
itemT = NWItem(theProject)
|
||||
@@ -161,7 +183,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
itemT._class = nwItemClass.TRASH
|
||||
itemT._expanded = False
|
||||
|
||||
assert not theTree.append("1234567890abc", None, itemT)
|
||||
assert theTree.append("1234567890abc", None, itemT) is False
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
# Generate handle automatically
|
||||
@@ -251,12 +273,24 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
|
||||
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
|
||||
|
||||
# Add a fake item to root and check that it can handle it
|
||||
theTree._treeRoots["0000000000000"] = NWItem(theProject)
|
||||
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||
del theTree._treeRoots["0000000000000"]
|
||||
|
||||
# Get item path
|
||||
assert theTree.getItemPath("stuff") == []
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001", "a000000000001"
|
||||
]
|
||||
|
||||
# Cause recursion error
|
||||
maxDepth = theTree.MAX_DEPTH
|
||||
theTree.MAX_DEPTH = 0
|
||||
with pytest.raises(RecursionError):
|
||||
theTree.getItemPath("c000000000001")
|
||||
theTree.MAX_DEPTH = maxDepth
|
||||
|
||||
# Break the folder parent handle
|
||||
theTree["b000000000001"]._parent = "stuff"
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
@@ -381,7 +415,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
|
||||
def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData):
|
||||
"""Test packing and unpacking the tree to and from XML.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
@@ -395,39 +429,41 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
|
||||
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
theTree.packXML(nwXML)
|
||||
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
|
||||
b'<novelWriterXML>'
|
||||
b'<content count="8">'
|
||||
b'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
|
||||
b'class="NOVEL"><meta expanded="True"/><name status="s43dac0" '
|
||||
b'import="i6c52c4">Novel</name></item>'
|
||||
b'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
|
||||
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="s43dac0" '
|
||||
b'import="i6c52c4">Act One</name></item>'
|
||||
b'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
|
||||
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" '
|
||||
b'paraCount="2" cursorPos="0"/><name status="s43dac0" import="i6c52c4" '
|
||||
b'exported="True">Chapter One</name></item>'
|
||||
b'<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
|
||||
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="3000" wordCount="500" '
|
||||
b'paraCount="20" cursorPos="0"/><name status="s43dac0" import="i6c52c4" '
|
||||
b'exported="True">Scene One</name></item>'
|
||||
b'<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
|
||||
b'class="ARCHIVE"><meta expanded="False"/><name status="s43dac0" '
|
||||
b'import="i6c52c4">Outtakes</name></item>'
|
||||
b'<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="TRASH" '
|
||||
b'class="TRASH"><meta expanded="False"/><name status="s43dac0" '
|
||||
b'import="i6c52c4">Trash</name></item>'
|
||||
b'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
|
||||
b'class="CHARACTER"><meta expanded="True"/><name status="s43dac0" '
|
||||
b'import="i6c52c4">Characters</name></item>'
|
||||
b'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
|
||||
b'type="FILE" class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" '
|
||||
b'paraCount="16" cursorPos="0"/><name status="s43dac0" import="i6c52c4" '
|
||||
b'exported="True">Jane Doe</name></item>'
|
||||
b'</content>'
|
||||
b'</novelWriterXML>'
|
||||
)
|
||||
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes((
|
||||
'<novelWriterXML>'
|
||||
'<content count="8">'
|
||||
'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
|
||||
'class="NOVEL"><meta expanded="True"/><name status="{s0}" '
|
||||
'import="{i0}">Novel</name></item>'
|
||||
'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
|
||||
'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="{s0}" '
|
||||
'import="{i0}">Act One</name></item>'
|
||||
'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
|
||||
'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" '
|
||||
'paraCount="2" cursorPos="0"/><name status="{s0}" import="{i0}" '
|
||||
'exported="True">Chapter One</name></item>'
|
||||
'<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
|
||||
'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="3000" wordCount="500" '
|
||||
'paraCount="20" cursorPos="0"/><name status="{s0}" import="{i0}" '
|
||||
'exported="True">Scene One</name></item>'
|
||||
'<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
|
||||
'class="ARCHIVE"><meta expanded="False"/><name status="{s0}" '
|
||||
'import="{i0}">Outtakes</name></item>'
|
||||
'<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="TRASH" '
|
||||
'class="TRASH"><meta expanded="False"/><name status="{s0}" '
|
||||
'import="{i0}">Trash</name></item>'
|
||||
'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
|
||||
'class="CHARACTER"><meta expanded="True"/><name status="{s0}" '
|
||||
'import="{i0}">Characters</name></item>'
|
||||
'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
|
||||
'type="FILE" class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" '
|
||||
'paraCount="16" cursorPos="0"/><name status="{s0}" import="{i0}" '
|
||||
'exported="True">Jane Doe</name></item>'
|
||||
'</content>'
|
||||
'</novelWriterXML>'
|
||||
).format(
|
||||
s0=constData.statusKeys[0], i0=constData.importKeys[0]
|
||||
), encoding="utf8")
|
||||
|
||||
theTree.clear()
|
||||
assert len(theTree) == 0
|
||||
|
||||
+376
-181
@@ -22,9 +22,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from tools import writeFile
|
||||
|
||||
from PyQt5.QtCore import QItemSelectionModel
|
||||
from PyQt5.QtWidgets import QAction, QMessageBox
|
||||
|
||||
from novelwriter.guimain import GuiMain
|
||||
@@ -33,208 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
|
||||
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
nwTree = nwGUI.treeView
|
||||
|
||||
##
|
||||
# Add New Items
|
||||
##
|
||||
# Try to add item with no project
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is False
|
||||
|
||||
# Try to add and move item with no project
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, None) is False
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
# Create a project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
assert nwGUI.newProject({"projPath": prjDir}) is True
|
||||
|
||||
# Open a project
|
||||
assert nwGUI.openProject(nwMinimal) is True
|
||||
# No itemType set
|
||||
nwTree.clearSelection()
|
||||
assert nwTree.newTreeItem(None) is False
|
||||
|
||||
# Root Items
|
||||
# ==========
|
||||
|
||||
# No class set
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT) is False
|
||||
|
||||
# Create root item
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
|
||||
assert "1a6562590ef19" in nwGUI.theProject.projTree
|
||||
|
||||
# File/Folder Items
|
||||
# =================
|
||||
|
||||
# No location selected for new item
|
||||
nwTree.clearSelection()
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, None) is False
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True
|
||||
caplog.clear()
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER) is False
|
||||
assert "Did not find anywhere" in caplog.text
|
||||
|
||||
# No itemType set or ROOT, but no class
|
||||
nwTree.clearSelection()
|
||||
assert nwTree.newTreeItem(None, None) is False
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT, None) is False
|
||||
# Create new folder as child of Novel folder
|
||||
nwTree.setSelectedHandle("73475cb40a568")
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER) is True
|
||||
assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568"
|
||||
assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568"
|
||||
assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL
|
||||
|
||||
# Select a location
|
||||
chItem = nwTree._getTreeItem("a6d311a93600a")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
chItem.setExpanded(True)
|
||||
# Add a new file in the new folder
|
||||
nwTree.setSelectedHandle("031b4af5197ec")
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec"
|
||||
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568"
|
||||
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL
|
||||
|
||||
# Create new item with no class set (defaults to NOVEL)
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, None) is True
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True
|
||||
# Add a new file next to the other new file
|
||||
nwTree.setSelectedHandle("41cfc0d1f2d12")
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec"
|
||||
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568"
|
||||
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL
|
||||
assert nwGUI.openDocument("2858dcd1057d3")
|
||||
assert nwGUI.docEditor.getText() == "### New Document\n\n"
|
||||
|
||||
# Check that we have the correct tree order
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
# Add a new file to the characters folder
|
||||
nwTree.setSelectedHandle("71ee45a3c0db9")
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9"
|
||||
assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9"
|
||||
assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER
|
||||
assert nwGUI.openDocument("2fca346db6561")
|
||||
assert nwGUI.docEditor.getText() == "# New Note\n\n"
|
||||
|
||||
# Add more roots
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate
|
||||
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique
|
||||
# Make sure the sibling folder bug trap works
|
||||
nwTree.setSelectedHandle("2858dcd1057d3")
|
||||
nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen
|
||||
caplog.clear()
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert "Internal error" in caplog.text
|
||||
nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec")
|
||||
|
||||
# Change max depth and try to add a subfolder that is too deep
|
||||
monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2)
|
||||
chItem = nwTree._getTreeItem("71ee45a3c0db9")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False
|
||||
# Get the trash folder
|
||||
nwTree._addTrashRoot()
|
||||
trashHandle = nwGUI.theProject.trashFolder()
|
||||
nwTree.setSelectedHandle(trashHandle)
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is False
|
||||
assert "Cannot add new files or folders to the Trash folder" in caplog.text
|
||||
|
||||
##
|
||||
# Move Items
|
||||
##
|
||||
# Other Checks
|
||||
# ============
|
||||
|
||||
nwTree.setSelectedHandle("8c659a11cd429")
|
||||
# Also check error handling in reveal function
|
||||
assert nwTree.revealNewTreeItem("abc") is False
|
||||
|
||||
# Shift focus and try to move item
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# Move second item up twice (should give same result)
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
|
||||
# Move it back down four times (last two should be the same)
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
|
||||
# Move up twice, and undo
|
||||
nwTree._lastMove = {}
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
|
||||
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
|
||||
]
|
||||
|
||||
# Move a root item (top level items are different) twice
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
|
||||
nwTree.setSelectedHandle("9d5247ab588e0")
|
||||
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
|
||||
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 12
|
||||
|
||||
##
|
||||
# Delete and Trash
|
||||
##
|
||||
|
||||
# Add some content to the new file
|
||||
nwGUI.openDocument("73475cb40a568")
|
||||
nwGUI.docEditor.setText("# Hello World\n")
|
||||
nwGUI.saveDocument()
|
||||
nwGUI.saveProject()
|
||||
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
|
||||
# Delete the items we added earlier
|
||||
nwTree.clearSelection()
|
||||
assert nwTree.emptyTrash() is False # No folder yet
|
||||
assert nwTree.deleteItem(None) is False
|
||||
assert nwTree.deleteItem("1111111111111") is False
|
||||
assert nwTree.deleteItem("73475cb40a568") is True # New File
|
||||
assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder
|
||||
assert nwTree.deleteItem("811786ad1ae74") is True # Custom Root
|
||||
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
|
||||
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
|
||||
assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
|
||||
|
||||
# The file is in trash, empty it
|
||||
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
assert nwTree.emptyTrash() is True
|
||||
assert nwTree.emptyTrash() is False # Already empty
|
||||
assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
|
||||
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
|
||||
|
||||
# Should not be allowed to add files and folders to Trash
|
||||
trashHandle = nwGUI.theProject.projTree.trashRoot()
|
||||
chItem = nwTree._getTreeItem(trashHandle)
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, None) is False
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False
|
||||
|
||||
# Close the project
|
||||
nwGUI.closeProject()
|
||||
|
||||
##
|
||||
# Orphaned Files
|
||||
##
|
||||
|
||||
# Add an orphaned file
|
||||
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
|
||||
writeFile(orphFile, "# Hello World\n")
|
||||
|
||||
# Open the project again
|
||||
nwGUI.openProject(nwMinimal)
|
||||
|
||||
# Check that the orphaned file was found and added to the tree
|
||||
nwTree.flushTreeOrder()
|
||||
assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
|
||||
orItem = nwTree._getTreeItem("1234567890abc")
|
||||
assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
|
||||
|
||||
##
|
||||
# Unexpected Error Handling
|
||||
##
|
||||
|
||||
# Add an item with an invalid type
|
||||
assert nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False
|
||||
assert "Failed to add new item" in caplog.messages[-1]
|
||||
|
||||
# Add new file after one that has no parent handle
|
||||
chItem = nwTree._getTreeItem("44cb730c42048")
|
||||
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
|
||||
nwTree.theProject.projTree["44cb730c42048"]._parent = None
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False
|
||||
nwTree.clearSelection()
|
||||
|
||||
# Add a file with no parent, and fail to find a suitable parent item
|
||||
monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None)
|
||||
|
||||
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False
|
||||
# Add an item that cannot be displayed in the tree
|
||||
nHandle = nwGUI.theProject.newFile("Test", None)
|
||||
assert nwTree.revealNewTreeItem(nHandle) is False
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_TreeItems
|
||||
# END Test testGuiProjTree_NewItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
|
||||
|
||||
nwTree = nwGUI.treeView
|
||||
|
||||
# Try to move item with no project
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
|
||||
# Create a project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
assert nwGUI.newProject({"projPath": prjDir}) is True
|
||||
|
||||
# Move Documents
|
||||
# ==============
|
||||
|
||||
# Add some files
|
||||
nwTree.setSelectedHandle("31489056e0916")
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Move item without focus
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# Move with no selections
|
||||
nwTree.clearSelection()
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
|
||||
# Move second item up twice (should give same result)
|
||||
nwTree.setSelectedHandle("0e17daca5f3e1")
|
||||
assert nwTree.moveTreeItem(-1) is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
assert nwTree.moveTreeItem(-1) is False
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Restore via menu entry
|
||||
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Move fifth item down twice (should give same result)
|
||||
nwTree.setSelectedHandle("031b4af5197ec")
|
||||
assert nwTree.moveTreeItem(1) is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
|
||||
]
|
||||
assert nwTree.moveTreeItem(1) is False
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
|
||||
]
|
||||
|
||||
# Restore via menu entry
|
||||
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Move down again, and restore via undo
|
||||
nwTree.setSelectedHandle("031b4af5197ec")
|
||||
assert nwTree.moveTreeItem(1) is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
|
||||
]
|
||||
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Root Folder
|
||||
# ===========
|
||||
|
||||
nwTree.setSelectedHandle("73475cb40a568")
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
|
||||
|
||||
# Move novel folder up
|
||||
assert nwTree.moveTreeItem(-1) is False
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
|
||||
|
||||
# Move novel folder down
|
||||
assert nwTree.moveTreeItem(1) is True
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1
|
||||
|
||||
# Move novel folder up again
|
||||
assert nwTree.moveTreeItem(-1) is True
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_MoveItems
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
|
||||
"""Test adding and removing items from the project tree.
|
||||
"""
|
||||
# Block message box
|
||||
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
|
||||
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
|
||||
|
||||
nwTree = nwGUI.treeView
|
||||
|
||||
# Try to run with no project
|
||||
assert nwTree.emptyTrash() is False
|
||||
assert nwTree.deleteItem() is False
|
||||
|
||||
# Create a project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
prjDir = os.path.join(fncDir, "project")
|
||||
assert nwGUI.newProject({"projPath": prjDir}) is True
|
||||
|
||||
# Try emptying the trash already now, when there is no trash folder
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Add some files
|
||||
nwTree.setSelectedHandle("31489056e0916")
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.newTreeItem(nwItemType.FILE) is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
|
||||
]
|
||||
|
||||
# Delete File
|
||||
# ===========
|
||||
|
||||
# Delete item without focus -> blocked
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
|
||||
nwTree.setSelectedHandle("41cfc0d1f2d12")
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem() is False
|
||||
assert "blocked" in caplog.text
|
||||
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
|
||||
|
||||
# No selection made
|
||||
nwTree.clearSelection()
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem() is False
|
||||
assert "no item to delete" in caplog.text
|
||||
|
||||
# Not a valid handle
|
||||
nwTree.clearSelection()
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem("0000000000000") is False
|
||||
assert "Could not find tree item" in caplog.text
|
||||
|
||||
# Block adding trash folder
|
||||
funcPointer = nwTree._addTrashRoot
|
||||
nwTree._addTrashRoot = lambda *a: None
|
||||
assert nwTree.deleteItem("41cfc0d1f2d12") is False
|
||||
nwTree._addTrashRoot = funcPointer
|
||||
|
||||
# Delete last two documents, which also adds the trash folder
|
||||
assert nwTree.deleteItem("41cfc0d1f2d12") is True
|
||||
assert nwTree.deleteItem("031b4af5197ec") is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
|
||||
"1a6562590ef19"
|
||||
]
|
||||
trashHandle = nwGUI.theProject.projTree.trashRoot()
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "41cfc0d1f2d12", "031b4af5197ec"
|
||||
]
|
||||
|
||||
# Delete the first file again (permanent), and ask for permission
|
||||
# Also open the document in the editor, which should trigger a close
|
||||
assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
|
||||
assert "41cfc0d1f2d12" in nwGUI.theProject.projTree
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
assert nwGUI.openDocument("41cfc0d1f2d12") is True
|
||||
assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12"
|
||||
assert nwTree.deleteItem("41cfc0d1f2d12") is True
|
||||
assert nwGUI.docEditor.docHandle() is None
|
||||
assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
|
||||
assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "031b4af5197ec"
|
||||
]
|
||||
|
||||
# Try to delete the second document, but block the deletion
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False)
|
||||
assert nwTree.deleteItem("031b4af5197ec") is False
|
||||
|
||||
# Delete proper, and skip asking for permission
|
||||
assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
|
||||
assert "031b4af5197ec" in nwGUI.theProject.projTree
|
||||
assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True
|
||||
assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
|
||||
assert "031b4af5197ec" not in nwGUI.theProject.projTree
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
|
||||
# Delete Folder/Root
|
||||
# ==================
|
||||
|
||||
# Deleting non-empty folders is blocked
|
||||
assert nwTree.deleteItem("31489056e0916") is False # Folder
|
||||
assert nwTree.deleteItem("73475cb40a568") is False # Root
|
||||
|
||||
# Add a folder we can delete
|
||||
nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root
|
||||
assert nwTree.newTreeItem(nwItemType.FOLDER) is True
|
||||
assert "2fca346db6561" in nwGUI.theProject.projTree
|
||||
|
||||
# Try to delete, but block parent item lookup
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None)
|
||||
caplog.clear()
|
||||
assert nwTree.deleteItem("2fca346db6561") is False
|
||||
assert "Could not delete folder" in caplog.text
|
||||
assert "2fca346db6561" in nwGUI.theProject.projTree
|
||||
|
||||
# Delete folder properly
|
||||
assert nwTree.deleteItem("2fca346db6561") is True
|
||||
assert "2fca346db6561" not in nwGUI.theProject.projTree
|
||||
|
||||
# Delete the Character root
|
||||
assert nwTree.deleteItem("71ee45a3c0db9") is True
|
||||
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree
|
||||
|
||||
# Empty Trash
|
||||
# ===========
|
||||
|
||||
# Try to empty trash that is already empty
|
||||
caplog.clear()
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
assert nwTree.emptyTrash() is False
|
||||
assert "already empty" in caplog.text
|
||||
|
||||
# Move the two remaining scene documents to trash
|
||||
assert nwTree.deleteItem("0e17daca5f3e1") is True
|
||||
assert nwTree.deleteItem("1a6562590ef19") is True
|
||||
assert nwTree.getTreeFromHandle("31489056e0916") == [
|
||||
"31489056e0916", "98010bd9270f9"
|
||||
]
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [
|
||||
trashHandle, "0e17daca5f3e1", "1a6562590ef19"
|
||||
]
|
||||
|
||||
# Empty trash, but select no on question
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Empty the trash proper
|
||||
nwTree._setTreeChanged(False)
|
||||
assert nwTree.emptyTrash() is True
|
||||
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
|
||||
assert nwTree._treeChanged is True
|
||||
|
||||
# Clean up
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiProjTree_DeleteItems
|
||||
|
||||
Reference in New Issue
Block a user