Files
novelWriter/tests/test_gui/test_gui_projtree.py
T
2023-07-20 23:25:49 +02:00

995 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Main GUI Project Tree Class Tester
================================================
This file is a part of novelWriter
Copyright 20182023, 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/>.
"""
import pytest
from pathlib import Path
from tools import C, buildTestProject
from mocked import causeOSError
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog
from novelwriter import CONFIG
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
from novelwriter.guimain import GuiMain
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
@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
theProject = nwGUI.theProject
# Try to add item with no project
assert projView.projTree.newTreeItem(nwItemType.FILE) is False
# Create a project
buildTestProject(nwGUI, projPath)
# No itemType set
projView.projTree.clearSelection()
assert projView.projTree.newTreeItem(None) is False
# Root Items
# ==========
# No class set
assert projView.projTree.newTreeItem(nwItemType.ROOT) is False
# Create root item
assert projView.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
assert "0000000000010" in theProject.tree
# File/Folder Items
# =================
# No location selected for new item
projView.projTree.clearSelection()
caplog.clear()
assert projView.projTree.newTreeItem(nwItemType.FILE) is False
assert projView.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 projView.projTree.newTreeItem(nwItemType.FOLDER) is True
assert theProject.tree["0000000000011"].itemParent == C.hNovelRoot
assert theProject.tree["0000000000011"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL
# Add a new file in the new folder
projView.setSelectedHandle("0000000000011")
assert projView.projTree.newTreeItem(nwItemType.FILE) is True
assert theProject.tree["0000000000012"].itemParent == "0000000000011"
assert theProject.tree["0000000000012"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL
# Add a new chapter next to the other new file
projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
assert theProject.tree["0000000000013"].itemParent == "0000000000011"
assert theProject.tree["0000000000013"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
# Add a new scene next to the other new file
projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
assert theProject.tree["0000000000014"].itemParent == "0000000000011"
assert theProject.tree["0000000000014"].itemRoot == C.hNovelRoot
assert theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000014")
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
# Add a new file to the characters folder
projView.setSelectedHandle(C.hCharRoot)
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
assert theProject.tree["0000000000015"].itemParent == C.hCharRoot
assert theProject.tree["0000000000015"].itemRoot == C.hCharRoot
assert theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER
assert nwGUI.openDocument("0000000000015")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
# Make sure the sibling folder bug trap works
projView.setSelectedHandle("0000000000013")
theProject.tree["0000000000013"].setParent(None) # This should not happen
caplog.clear()
assert projView.projTree.newTreeItem(nwItemType.FILE) is False
assert "Internal error" in caplog.text
theProject.tree["0000000000013"].setParent("0000000000011")
# Cancel during creation
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
projView.setSelectedHandle("0000000000013")
assert projView.projTree.newTreeItem(nwItemType.FILE) is False
# Get the trash folder
projView.projTree._addTrashRoot()
trashHandle = theProject.trashFolder()
projView.setSelectedHandle(trashHandle)
assert projView.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) is True
assert theProject.tree[C.hPlotRoot].itemName == "Stuff"
# Rename invalid folder
projTree.renameTreeItem("0000000000000") is False
# Other Checks
# ============
# Also check error handling in reveal function
assert projView.projTree.revealNewTreeItem("abc") is False
# Add an item that cannot be displayed in the tree
nHandle = theProject.newFile("Test", None)
assert projView.projTree.revealNewTreeItem(nHandle) is False
# Adding an invalid item directly to the tree should also fail
assert projView.projTree._addTreeItem(None) is None
# Clean up
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiProjTree_NewItems
@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 projView.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, and restore via undo
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.undoLastMove() is True
assert projTree.getTreeFromHandle(C.hChapterDir) == [
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Root Folder
# ===========
projView.setSelectedHandle(C.hNovelRoot)
assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Move novel folder up
assert projTree.moveTreeItem(-1) is False
assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Move novel folder down
assert projTree.moveTreeItem(1) is True
assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 1
# Move novel folder up again
assert projTree.moveTreeItem(-1) is True
assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0
# Clean up
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiProjTree_MoveItems
@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 = nwGUI.theProject.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()
# END Test testGuiProjTree_RequestDeleteItem
@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))
theProject = nwGUI.theProject
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 theProject.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, "question", lambda *a: QMessageBox.No)
assert projTree.moveItemToTrash(C.hTitlePage) is False
assert theProject.tree.isTrash(C.hTitlePage) is False
# Move a document to Trash
assert projTree.moveItemToTrash(C.hTitlePage) is True
assert theProject.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()
# END Test testGuiProjTree_MoveItemToTrash
@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))
theProject = nwGUI.theProject
projTree = nwGUI.projView.projTree
# Create a project
buildTestProject(nwGUI, projPath)
# Invalid item
caplog.clear()
assert projTree.permanentlyDeleteItem(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.permanentlyDeleteItem(C.hNovelRoot) is False
assert "Root folders can only be deleted when they are empty" in caplog.text
assert C.hNovelRoot in theProject.tree
# Deleting unused root item is allowed
caplog.clear()
assert projTree.permanentlyDeleteItem(C.hPlotRoot) is True
assert C.hPlotRoot not in theProject.tree
# User cancels action
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert projTree.permanentlyDeleteItem(C.hTitlePage) is False
assert C.hTitlePage in theProject.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.permanentlyDeleteItem(C.hTitlePage) is True
assert C.hTitlePage not in theProject.tree
assert nwGUI.docEditor.docHandle() is None
# Deleting folder + files recursively is ok
assert projTree.permanentlyDeleteItem(C.hChapterDir) is True
assert C.hChapterDir not in theProject.tree
assert C.hChapterDoc not in theProject.tree
assert C.hSceneDoc not in theProject.tree
nwGUI.closeProject()
# END Test testGuiProjTree_PermanentlyDeleteItem
@pytest.mark.gui
def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test emptying Trash."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
theProject = nwGUI.theProject
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 theProject.tree.isTrash(C.hTitlePage) is True
assert theProject.tree.isTrash(C.hChapterDir) is True
assert theProject.tree.isTrash(C.hChapterDoc) is True
assert theProject.tree.isTrash(C.hSceneDoc) is True
# User cancels
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert projTree.emptyTrash() is False
assert C.hTitlePage in theProject.tree
assert C.hChapterDir in theProject.tree
assert C.hChapterDoc in theProject.tree
assert C.hSceneDoc in theProject.tree
# Run again to empty all items
assert projTree.emptyTrash() is True
assert C.hTitlePage not in theProject.tree
assert C.hChapterDir not in theProject.tree
assert C.hChapterDoc not in theProject.tree
assert C.hSceneDoc not in theProject.tree
# Running Empty Trash again is cancelled due to empty folder
assert projTree.emptyTrash() is False
nwGUI.closeProject()
# END Test testGuiProjTree_EmptyTrash
@pytest.mark.gui
def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the building of the project tree context menu. All this does
is test that the menu builds. It doesn't open the actual menu,
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
# Create a project
buildTestProject(nwGUI, projPath)
# Handles for new objects
hCharNote = "0000000000011"
hNovelNote = "0000000000012"
hSubNote = "0000000000013"
hNewFolderOne = "0000000000014"
hNewFolderTwo = "0000000000016"
projView = nwGUI.projView
projTree = nwGUI.projView.projTree
projTree.setExpandedFromHandle(None, True)
projTree._addTrashRoot()
hTrashRoot = projTree.theProject.tree.trashRoot()
projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
nwGUI.theProject.newFile("SubNote", hNovelNote)
projTree.revealNewTreeItem(hSubNote)
assert nwGUI.theProject.tree[hSubNote].itemParent == hNovelNote
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
# No item under menu
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
# Generate the possible menu combinations
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
assert projTree._openContextMenu(itemPos(C.hTitlePage)) is True
assert projTree._openContextMenu(itemPos(C.hChapterDir)) is True
assert projTree._openContextMenu(itemPos(C.hChapterDoc)) is True
assert projTree._openContextMenu(itemPos(C.hCharRoot)) is True
assert projTree._openContextMenu(itemPos(hCharNote)) is True
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(C.hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
# Direct Edit Functions
# =====================
# Trigger the dedicated functions the menu entries connect to
nwItem = projTree.theProject.tree[hNovelNote]
# Toggle active flag
assert nwItem.isActive is True
projTree._toggleItemActive(hNovelNote)
assert nwItem.isActive is False
# Change item status
assert nwItem.itemStatus == "s000000"
projTree._changeItemStatus(hNovelNote, "s000001")
assert nwItem.itemStatus == "s000001"
# Change item importance
assert nwItem.itemImport == "i000004"
projTree._changeItemImport(hNovelNote, "i000005")
assert nwItem.itemImport == "i000005"
# Change item layout
assert nwItem.itemLayout == nwItemLayout.NOTE
projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT)
assert nwItem.itemLayout == nwItemLayout.DOCUMENT
projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE)
assert nwItem.itemLayout == nwItemLayout.NOTE
# Convert Folders to Documents
# ============================
projView.setSelectedHandle(hNovelNote)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderOne)
assert projView.projTree.newTreeItem(nwItemType.FILE) is True
projView.setSelectedHandle(hNovelNote)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderTwo)
assert projView.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
# Click no on the dialog
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert nwGUI.theProject.tree[hNewFolderOne].isFolderType()
# Convert the first folder to a document
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert nwGUI.theProject.tree[hNewFolderOne].isFileType()
assert nwGUI.theProject.tree[hNewFolderOne].isDocumentLayout()
# Convert the second folder to a note
projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE)
assert nwGUI.theProject.tree[hNewFolderTwo].isFileType()
assert nwGUI.theProject.tree[hNewFolderTwo].isNoteLayout()
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
@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, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData)
# Create a project
buildTestProject(nwGUI, projPath)
theProject = nwGUI.theProject
projTree = nwGUI.projView.projTree
mergedDoc1 = "0000000000014"
# Create File to Merge
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
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
theProject.writeNewFile(hChapter1, 2, True, docText1)
theProject.writeNewFile(hSceneOne11, 3, True, docText2)
theProject.writeNewFile(hSceneOne12, 3, True, docText3)
theProject.writeNewFile(hSceneOne13, 3, True, docText4)
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: QDialog.Rejected)
assert projTree._mergeDocuments(hChapter1, True) is False
# The merge goes through
assert projTree._mergeDocuments(hChapter1, True) is True
assert len(theProject.storage.getDocument(mergedDoc1).readDocument()) > lenAll
# 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(theProject.storage.getDocument(hChapter1).readDocument()) < lenAll
assert projTree._mergeDocuments(hChapter1, False) is True
assert len(theProject.storage.getDocument(hChapter1).readDocument()) > lenAll
assert theProject.tree.isTrash(hSceneOne11)
assert theProject.tree.isTrash(hSceneOne12)
assert theProject.tree.isTrash(hSceneOne13)
# qtbot.stop()
# END Test testGuiProjTree_MergeDocuments
@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, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText))
# Create a project
buildTestProject(nwGUI, projPath)
theProject = nwGUI.theProject
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 = theProject.newFile("Split Doc", C.hNovelRoot)
theProject.writeNewFile(hSplitDoc, 1, True, docText)
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 theProject.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 theProject.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 theProject.tree # The folder
for tHandle in trdSet:
assert tHandle in theProject.tree
assert (projPath / "content" / f"{tHandle}.nwd").is_file()
assert theProject.tree.isTrash(hSplitDoc) is True
# Cancelled by user
with monkeypatch.context() as mp:
mp.setattr(GuiDocSplit, "result", lambda *a: QDialog.Rejected)
assert projTree._splitDocument(hSplitDoc) is False
# qtbot.stop()
# END Test testGuiProjTree_SplitDocument
@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(nwGUI.theProject.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(nwGUI.theProject.tree) == 8
# Duplicate title page, but select no
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert projTree._duplicateFromHandle(C.hTitlePage) is False
assert len(nwGUI.theProject.tree) == 8
# Duplicate title page
assert projTree._duplicateFromHandle(C.hTitlePage) is True
assert len(nwGUI.theProject.tree) == 9
# Duplicate folder
assert projTree._duplicateFromHandle(C.hChapterDir) is True
assert len(nwGUI.theProject.tree) == 12
# Duplicate novel root
assert projTree._duplicateFromHandle(C.hNovelRoot) is True
assert len(nwGUI.theProject.tree) == 21
# Check tree order that all items are next to eachother
assert nwGUI.theProject.tree._treeOrder == [
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 = nwGUI.theProject.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(nwGUI.theProject.tree) == 22
# qtbot.stop()
# END Test testGuiProjTree_Duplicate
@pytest.mark.gui
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test various parts of the project tree class not covered by
other tests.
"""
# Create a project
buildTestProject(nwGUI, projPath)
projView = nwGUI.projView
projTree = 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 = nwGUI.theProject.newFile("Test", C.hNovelRoot)
nwGUI.theProject.tree[nHandle].setParent(None)
assert projTree.revealNewTreeItem(nHandle) is False
# Try to add an item with unknown parent to the tree
nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
nwGUI.theProject.tree[nHandle].setParent(C.hInvalid)
assert projTree.revealNewTreeItem(nHandle) is False
# Method: undoLastMove
# ====================
# Nothing to move
assert projTree.undoLastMove() is False
projTree._lastMove["item"] = QTreeWidgetItem()
projTree._lastMove["parent"] = QTreeWidgetItem()
projTree._lastMove["index"] = 0
assert projTree.undoLastMove() is False
projTree._lastMove["item"] = projTree._treeMap[C.hTitlePage]
projTree._lastMove["parent"] = QTreeWidgetItem()
projTree._lastMove["index"] = 0
assert projTree.undoLastMove() 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)
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)
# A non-file item should be expanded instead
projTree._getTreeItem(C.hNovelRoot).setExpanded(False)
projTree._getTreeItem(C.hNovelRoot).setSelected(True)
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
assert nwGUI.docEditor.docHandle() == C.hTitlePage
assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True
# qtbot.stop()
# END Test testGuiProjTree_Other