Files
novelWriter/tests/test_gui/test_gui_projtree.py
T
Veronica Berglyd Olsen 5bdbd7c195 Update linting for tests
2025-08-27 21:04:32 +02:00

1477 lines
54 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 (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
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/>.
""" # noqa
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF
from PyQt6.QtGui import QMouseEvent
from PyQt6.QtWidgets import QMenu, QMessageBox
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.projtree import _TreeContextMenu
from novelwriter.types import (
QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected,
QtScrollAlwaysOff, QtScrollAsNeeded
)
from tests.mocked import causeOSError
from tests.tools import C, buildTestProject
@pytest.mark.gui
def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test adding items to the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
# Try to add item with no project
projTree.newTreeItem(nwItemType.FILE)
assert len(tree) == 0
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Root Items
# ==========
# No class set
projTree.newTreeItem(nwItemType.ROOT)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Create Objects root item after Locations
hObjectRoot = "0000000000011"
projView.setSelectedHandle(C.hWorldRoot)
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.OBJECT)
assert hObjectRoot in tree
item = tree[hObjectRoot]
assert item is not None
assert item.itemName == "Objects"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Objects", "Trash",
]
# File/Folder Items
# =================
# No location selected for new item
projTree._clearSelection()
caplog.clear()
projTree.newTreeItem(nwItemType.FILE)
assert "Did not find anywhere" in caplog.text
caplog.clear()
projTree.newTreeItem(nwItemType.FOLDER)
assert "Did not find anywhere" in caplog.text
# Try to add them to Trash
projView.setSelectedHandle(trash.item.itemHandle)
caplog.clear()
projTree.newTreeItem(nwItemType.FILE)
assert "Cannot add new files or folders" in caplog.text
caplog.clear()
projTree.newTreeItem(nwItemType.FOLDER)
assert "Cannot add new files or folders" in caplog.text
# Create new folder as child of Novel folder
hNewFolder = "0000000000012"
projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
projTree.newTreeItem(nwItemType.FOLDER)
assert hNewFolder in tree
item = tree[hNewFolder]
assert item is not None
assert item.itemName == "New Folder"
assert item.itemParent == C.hNovelRoot
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "Plot", "Characters", "Locations", "Objects", "Trash",
]
# Add a new file in the new folder
hNewFile = "0000000000013"
projView.setSelectedHandle(hNewFolder, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=0)
assert hNewFile in tree
item = tree[hNewFile]
assert item is not None
assert item.itemName == "New Document"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "Plot", "Characters", "Locations",
"Objects", "Trash",
]
# Add a new partition next to the other new file
hNewPart = "0000000000014"
projView.setSelectedHandle(hNewFile, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=1)
assert hNewPart in tree
item = tree[hNewPart]
assert item is not None
assert item.itemName == "New Part"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument(hNewPart)
assert nwGUI.docEditor.getText() == "# New Part\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "Plot", "Characters",
"Locations", "Objects", "Trash",
]
# Add a new chapter next to the other new file
hNewChapter = "0000000000015"
projView.setSelectedHandle(hNewPart, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=2)
assert hNewChapter in tree
item = tree[hNewChapter]
assert item is not None
assert item.itemName == "New Chapter"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument(hNewChapter)
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "Plot",
"Characters", "Locations", "Objects", "Trash",
]
# Add a new scene next to the other new file
hNewScene = "0000000000016"
projView.setSelectedHandle(hNewChapter, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
assert hNewScene in tree
item = tree[hNewScene]
assert item is not None
assert item.itemName == "New Scene"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument(hNewScene)
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Objects", "Trash",
]
# Add a new scene with the content copied from the previous
nwGUI.openDocument(hNewScene)
nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
nwGUI.saveDocument()
hNewSceneCopy = "0000000000017"
projView.setSelectedHandle(hNewScene, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene)
assert hNewSceneCopy in tree
item = tree[hNewSceneCopy]
assert item is not None
assert item.itemName == "New Scene"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument(hNewSceneCopy)
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "Plot", "Characters", "Locations", "Objects", "Trash",
]
# Add a new file to the characters folder
hNewCharacter = "0000000000018"
projView.setSelectedHandle(C.hCharRoot, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
assert hNewCharacter in tree
item = tree[hNewCharacter]
assert item is not None
assert item.itemName == "New Note"
assert item.itemParent == C.hCharRoot
assert item.itemRoot == C.hCharRoot
assert item.itemClass == nwItemClass.CHARACTER
assert nwGUI.openDocument(hNewCharacter)
assert nwGUI.docEditor.getText() == "# New Note\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "Plot", "Characters", "New Note", "Locations",
"Objects", "Trash",
]
# Cancel during creation
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
projView.setSelectedHandle(hNewFile, doScroll=True)
projTree.newTreeItem(nwItemType.FILE)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "Plot", "Characters", "New Note", "Locations",
"Objects", "Trash",
]
# From Template
# =============
# Create template folder
hTemplateRoot = "0000000000019"
projView.setSelectedHandle(hObjectRoot)
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
assert hTemplateRoot in tree
item = tree[hTemplateRoot]
assert item is not None
assert item.itemName == "Templates"
assert item.itemParent is None
assert item.itemRoot == hTemplateRoot
assert item.itemClass == nwItemClass.TEMPLATE
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "Plot", "Characters", "New Note", "Locations",
"Objects", "Templates", "Trash",
]
# Create scene template
hSceneTemplate = "000000000001a"
projView.setSelectedHandle(hTemplateRoot, doScroll=True)
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
assert hSceneTemplate in tree
item = tree[hSceneTemplate]
assert item is not None
assert item.itemName == "New Scene"
assert item.itemParent == hTemplateRoot
assert item.itemRoot == hTemplateRoot
assert item.itemClass == nwItemClass.TEMPLATE
item.setName("New Scene Template")
nwGUI.openDocument(hSceneTemplate)
nwGUI.docEditor.setPlainText("### New Scene Template\n\nWith Stuff\n\n")
nwGUI.saveDocument()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "Plot", "Characters", "New Note", "Locations",
"Objects", "Templates", "New Scene Template", "Trash",
]
# Create from template
hNewFromTemplate = "000000000001b"
projView.setSelectedHandle(hNewSceneCopy, doScroll=True)
projView.createFileFromTemplate(hSceneTemplate)
assert hNewFromTemplate in tree
item = tree[hNewFromTemplate]
assert item is not None
assert item.itemName == "New Scene Template"
assert item.itemParent == hNewFolder
assert item.itemRoot == C.hNovelRoot
assert item.itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument(hNewFromTemplate)
assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n"
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
"New Scene", "New Scene Template", "Plot", "Characters", "New Note",
"Locations", "Objects", "Templates", "New Scene Template", "Trash",
]
# Rename Item
# ===========
# Rename plot folder
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True))
projView.setSelectedHandle(C.hPlotRoot, doScroll=True)
projView.renameTreeItem()
item = tree[C.hPlotRoot]
assert item is not None
assert item.itemName == "Stuff"
# Clean up
# qtbot.stop()
nwGUI.closeProject()
@pytest.mark.gui
def testGuiProjTree_SimpleOperations(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test simple operations in the project tree like internal move,
change selection and expand/collapse nodes.
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
model = tree.model
# The default model is empty
assert projView.getSelectedHandle() is None
assert projTree._getModel() is None
assert projTree._selectedRows() == []
assert projTree._getNode(QModelIndex()) is None
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Add some scenes
hFolder = "0000000000011"
hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
projTree.newTreeItem(nwItemType.FOLDER)
assert hFolder in tree
item = tree[hFolder]
assert item is not None
item.setName("Chapter Folder")
projView.setSelectedHandle(hFolder, doScroll=True)
for n, hScene in enumerate(hScenes):
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
assert hScene in tree
item = tree[hScene]
assert item is not None
item.setName(f"Scene {n+1}")
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Locations", "Trash",
]
# Expansion
# =========
# Default State
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
C.hNovelRoot, hFolder
]
# Expand Novel
projTree.expandFromIndex(model.indexFromHandle(C.hNovelRoot))
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
C.hNovelRoot, C.hChapterDir, hFolder
]
# Collapse Novel
projTree.collapseFromIndex(model.indexFromHandle(C.hNovelRoot))
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
C.hNovelRoot
]
# Move Up
# =======
# Already first
projView.setSelectedHandle(hScenes[0], doScroll=True)
projTree.moveItemUp()
assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
]
# Proper move
projView.setSelectedHandle(hScenes[1], doScroll=True)
projTree.moveItemUp()
assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
"Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
]
# Move Down
# =========
# Already last
projView.setSelectedHandle(hScenes[3], doScroll=True)
projTree.moveItemDown()
assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
"Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
]
# Proper move
projView.setSelectedHandle(hScenes[1], doScroll=True)
projTree.moveItemDown()
assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
]
# Select Up
# =========
# Sibling
projView.setSelectedHandle(hFolder, doScroll=True)
assert projView.getSelectedHandle() == hFolder
projTree.goToSiblingUp()
assert projView.getSelectedHandle() == C.hChapterDir
projTree.goToSiblingUp()
assert projView.getSelectedHandle() == C.hTitlePage
projTree.goToSiblingUp()
assert projView.getSelectedHandle() == C.hTitlePage
# Parent
projTree.goToParent()
assert projView.getSelectedHandle() == C.hNovelRoot
projTree.goToParent()
assert projView.getSelectedHandle() == C.hNovelRoot
# Select Down
# =========
# Child
projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
assert projView.getSelectedHandle() == C.hNovelRoot
projTree.goToFirstChild()
assert projView.getSelectedHandle() == C.hTitlePage
# Sibling
projTree.goToSiblingDown()
assert projView.getSelectedHandle() == C.hChapterDir
projTree.goToSiblingDown()
assert projView.getSelectedHandle() == hFolder
projTree.goToSiblingDown()
assert projView.getSelectedHandle() == hFolder
@pytest.mark.gui
def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test mouse clicks in the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
model = tree.model
# The default model is empty
assert projView.getSelectedHandle() is None
assert projTree._getModel() is None
assert projTree._selectedRows() == []
assert projTree._getNode(QModelIndex()) is None
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
# Single click emits a signal
with qtbot.waitSignal(projView.selectedItemChanged) as signal:
projTree._onSelectionChange(model.indexFromHandle(C.hNovelRoot), QModelIndex())
assert signal.args[0] == C.hNovelRoot
# Double click on folder expands/collapses it
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []
projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
C.hNovelRoot
]
projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []
# Double click on file opens it
with qtbot.waitSignal(projView.openDocumentRequest) as signal:
projTree._onDoubleClick(model.indexFromHandle(C.hChapterDoc))
assert signal.args[0] == C.hChapterDoc
assert signal.args[1] == nwDocMode.EDIT
assert signal.args[2] == ""
assert signal.args[3] is True
# Mouse Button Clicks
# ===================
eType = QEvent.Type.MouseButtonPress
modifier = QtModNone
# Trigger the viewer
pos = QPointF(projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center())
button = QtMouseMiddle
event = QMouseEvent(eType, pos, button, button, modifier)
projTree.mousePressEvent(event)
assert nwGUI.docViewer.docHandle == C.hChapterDoc
# Trigger the left click clear
pos = QPointF(5000.0, 5000.0)
button = QtMouseLeft
event = QMouseEvent(eType, pos, button, button, modifier)
projTree.setSelectedHandle(C.hChapterDoc)
assert projView.getSelectedHandle() == C.hChapterDoc
projTree.mousePressEvent(event)
assert projView.getSelectedHandle() is None
@pytest.mark.gui
def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test delete requests in the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Add some scenes
hFolder = "0000000000011"
hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
projTree.newTreeItem(nwItemType.FOLDER)
assert hFolder in tree
item = tree[hFolder]
assert item is not None
item.setName("Chapter Folder")
projView.setSelectedHandle(hFolder, doScroll=True)
for n, hScene in enumerate(hScenes):
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
assert hScene in tree
item = tree[hScene]
assert item is not None
item.setName(f"Scene {n+1}")
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Locations", "Trash",
]
# Calling it with no selection, does nothing
projTree._clearSelection()
projTree.processDeleteRequest()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Locations", "Trash",
]
# Deleting a used root raises an error
projTree.processDeleteRequest([C.hNovelRoot])
assert SHARED.lastAlert == "Root folders can only be deleted when they are empty."
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Locations", "Trash",
]
# Deleting a unused root is fine
projTree.processDeleteRequest([C.hWorldRoot])
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Trash",
]
# User can cancel move to trash
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
projTree.processDeleteRequest(hScenes, askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
"Plot", "Characters", "Trash",
]
# Items not already in trash can be moved there
projTree.processDeleteRequest(hScenes, askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Plot", "Characters", "Trash",
"Scene 1", "Scene 2", "Scene 3", "Scene 4",
]
# User can block permanent deletion
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Plot", "Characters", "Trash",
"Scene 1", "Scene 2", "Scene 3", "Scene 4",
]
# Items in trash can be permanently deleted
projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter Folder", "Plot", "Characters", "Trash",
"Scene 3", "Scene 4",
]
# Moving a parent item to trash, includes children
projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
"Scene 3", "Scene 4", "New Folder", "New Chapter", "New Scene",
]
# Permanently delete in trash is recursive
projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
"Scene 3", "Scene 4",
]
# Trash can be completely emptied, but user can block it
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
projTree.emptyTrash()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
"Scene 3", "Scene 4",
]
# Opening documents before delete, will close them
nwGUI.openDocument(hScenes[2])
nwGUI.viewDocument(hScenes[3])
assert nwGUI.docEditor.docHandle == hScenes[2]
assert nwGUI.docViewer.docHandle == hScenes[3]
# Trash can be completely emptied
projTree.emptyTrash()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
]
assert nwGUI.docEditor.docHandle is None
assert nwGUI.docViewer.docHandle is None
# Emptying empty trash pops an alert
projTree.emptyTrash()
assert SHARED.lastAlert == "The Trash folder is already empty."
# Trash can be deleted if empty
projTree.processDeleteRequest([trash.item.itemHandle])
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters"
]
# Emptying trash when it doesn't exist, recreates it
projTree.emptyTrash()
assert SHARED.lastAlert == "The Trash folder is already empty."
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
]
@pytest.mark.gui
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, "softDelete", lambda *a: None)
monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted)
monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData)
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Create File to Merge
hChapter1 = project.newFile("Chapter 1", C.hNovelRoot)
assert hChapter1 == "0000000000011"
hSceneOne11 = project.newFile("Scene 1.1", hChapter1)
hSceneOne12 = project.newFile("Scene 1.2", hChapter1)
hSceneOne13 = project.newFile("Scene 1.3", hChapter1)
assert hSceneOne11 == "0000000000012"
assert hSceneOne12 == "0000000000013"
assert hSceneOne13 == "0000000000014"
mergedDoc1 = "0000000000015"
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
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)
project.writeNewFile(hSceneOne11, 3, True, docText2)
project.writeNewFile(hSceneOne12, 3, True, docText3)
project.writeNewFile(hSceneOne13, 3, True, docText4)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3",
"Plot", "Characters", "Locations", "Trash",
]
# Invalid file handle
assert projTree.mergeDocuments(C.hInvalid, False) is False
# 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
mergedText = project.storage.getDocument(mergedDoc1).readDocument()
assert mergedText is not None
assert len(mergedText) > lenAll
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3", "[Merged] Chapter 1",
"Plot", "Characters", "Locations", "Trash",
]
# Merge to Existing Doc
# =====================
# 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
mergedText = project.storage.getDocument(hChapter1).readDocument()
assert mergedText is not None
assert len(mergedText) < lenAll
assert projTree.mergeDocuments(hChapter1, False) is True
mergedText = project.storage.getDocument(hChapter1).readDocument()
assert mergedText is not None
assert len(mergedText) > lenAll
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Chapter 1", "[Merged] Chapter 1",
"Plot", "Characters", "Locations", "Trash",
"Scene 1.1", "Scene 1.2", "Scene 1.3",
]
# qtbot.stop()
@pytest.mark.gui
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, "softDelete", lambda *a: None)
monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted)
monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText))
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
docText = (
"Text\n\n"
"##! 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)
assert hSplitDoc is not None
project.writeNewFile(hSplitDoc, 1, True, docText)
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 = [
"0000000000012", "0000000000013", "0000000000014", "0000000000015",
"0000000000016", "0000000000017", "0000000000018", "0000000000019",
"000000000001a", "000000000001b", "000000000001c", "000000000001d",
]
sndSet = [
"000000000001e", "000000000001f", "0000000000020", "0000000000021",
"0000000000022", "0000000000023", "0000000000024", "0000000000025",
"0000000000026", "0000000000027", "0000000000028", "0000000000029",
]
trdSet = [
"000000000002b", "000000000002c", "000000000002d", "000000000002e",
"000000000002f", "0000000000030", "0000000000031", "0000000000032",
"0000000000033", "0000000000034", "0000000000035", "0000000000036",
]
# Try to split an invalid document and a non-document
assert projTree.splitDocument(C.hInvalid) is False
assert projTree.splitDocument(C.hNovelRoot) is False
# 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 trash.allChildren() == [tree.nodes[hSplitDoc]]
# Cancelled by user
with monkeypatch.context() as mp:
mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected)
assert projTree.splitDocument(hSplitDoc) is False
# qtbot.stop()
@pytest.mark.gui
def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the duplicate items function."""
# Create a project
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
projTree.expandAll()
# Nothing to do
projTree.duplicateFromHandle(C.hInvalid)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Duplicate title page, but select no
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
projTree.duplicateFromHandle(C.hTitlePage)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Duplicate title page
projTree.duplicateFromHandle(C.hTitlePage)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
"New Scene", "Plot", "Characters", "Locations", "Trash",
]
# Duplicate folder
projTree.duplicateFromHandle(C.hChapterDir)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
"New Scene", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Duplicate novel root
projTree.duplicateFromHandle(C.hNovelRoot)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
"New Scene", "New Folder", "New Chapter", "New Scene",
"Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
"New Scene", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# qtbot.stop()
@pytest.mark.gui
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test various parts of the project tree class not covered by
other tests.
"""
buildTestProject(nwGUI, projPath)
projView = nwGUI.projView
projTree = projView.projTree
# Method: initSettings
# ====================
# Test that the scrollbar setting works
CONFIG.hideVScroll = True
CONFIG.hideHScroll = True
projView.initSettings()
assert projTree.verticalScrollBarPolicy() == QtScrollAlwaysOff
assert projTree.horizontalScrollBarPolicy() == QtScrollAlwaysOff
CONFIG.hideVScroll = False
CONFIG.hideHScroll = False
projView.initSettings()
assert projTree.verticalScrollBarPolicy() == QtScrollAsNeeded
assert projTree.horizontalScrollBarPolicy() == QtScrollAsNeeded
# 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))
QtSelect = QItemSelectionModel.SelectionFlag.Select
# Create a project
projView = nwGUI.projView
projTree = projView.projTree
project = SHARED.project
tree = project.tree
model = tree.model
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Handles for new objects
hCharNote = "0000000000011"
hNovelNote = "0000000000012"
hTrashDoc = "0000000000013"
hSubNote = "0000000000014"
hNewFolderOne = "0000000000015"
hNewFolderTwo = "0000000000017"
hTrashRoot = trash.item.itemHandle
projTree.expandAll()
projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(hTrashDoc)
projTree.processDeleteRequest()
project.newFile("SubNote", hNovelNote)
item = tree[hSubNote]
assert item is not None
assert item.itemName == "SubNote"
assert item.itemParent == hNovelNote
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"New Note", "SubNote", "Plot", "Characters", "New Note", "Locations",
"Trash", "New Part",
]
# Pop the menu in various positions and check for success
with monkeypatch.context() as mp:
mockMenu = MagicMock()
mp.setattr(QMenu, "exec", mockMenu)
projTree._clearSelection()
# No item under menu
projTree.openContextMenu(projTree.viewport().rect().bottomRight())
assert mockMenu.call_count == 0
# Open Trash Menu
projView.setSelectedHandle(hTrashRoot)
projTree.openContextMenu()
assert mockMenu.call_count == 1
# Open Single Select Menu
projView.setSelectedHandle(C.hNovelRoot)
projTree.openContextMenu()
assert mockMenu.call_count == 2
# Open Multi-Select Menu
projTree.selectionModel().select(model.indexFromHandle(hNovelNote), QtSelect)
projTree.selectionModel().select(model.indexFromHandle(hSubNote), QtSelect)
projTree.openContextMenu()
assert mockMenu.call_count == 3
projTree.clearSelection()
# Menu Builders
# =============
# Context Menu on Root Item
node = tree.nodes[C.hNovelRoot]
indices = [model.indexFromHandle(C.hNovelRoot)]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Create New ...", "Rename", "Set Status to ...", "Expand All",
"Collapse All", "Duplicate", "Delete Permanently",
]
# Context Menu on Folder Item
node = tree.nodes[C.hChapterDir]
indices = [model.indexFromHandle(C.hChapterDir)]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All",
"Collapse All", "Duplicate", "Move to Trash",
]
def getTransformSubMenu(menu: QMenu) -> list[str]:
for action in menu.actions():
if action.text() == "Transform ...":
submenu = action.menu()
assert submenu is not None
return [x.text() for x in submenu.actions() if x.text()]
return []
# Context Menu on Document File Item
node = tree.nodes[C.hChapterDoc]
indices = [model.indexFromHandle(C.hChapterDoc)]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
"Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Convert to Project Note", "Split Document by Headings"
]
# Context Menu on Note File Item in Character Folder
node = tree.nodes[hCharNote]
indices = [model.indexFromHandle(hCharNote)]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
"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
node = tree.nodes[hNovelNote]
indices = [model.indexFromHandle(hNovelNote)]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
"Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
"Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Convert to Novel Document", "Merge Child Items into Self",
"Merge Child Items into New", "Split Document by Headings",
]
# Context Menu on Multiple Items, Clicked on Document
node = tree.nodes[hNovelNote]
indices = [
model.indexFromHandle(hCharNote),
model.indexFromHandle(hNovelNote),
model.indexFromHandle(hSubNote),
]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildMultiSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Status to ...", "Move to Trash",
]
# Context Menu on Multiple Items, Clicked on Note
node = tree.nodes[hCharNote]
indices = [
model.indexFromHandle(hCharNote),
model.indexFromHandle(hNovelNote),
model.indexFromHandle(hSubNote),
]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildMultiSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Importance to ...", "Move to Trash",
]
# Direct Edit Functions, Single
# =============================
node = tree.nodes[hNovelNote]
indices = [model.indexFromHandle(hNovelNote)]
# Toggle active flag
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildSingleSelectMenu()
assert node.item.isActive is True
ctxMenu._toggleItemActive()
assert node.item.isActive is False
# Change item status
assert node.item.itemStatus == "s000000"
ctxMenu._changeItemStatus("s000001")
assert node.item.itemStatus == "s000001"
# Change item importance
assert node.item.itemImport == "i000004"
ctxMenu._changeItemImport("i000005")
assert node.item.itemImport == "i000005"
# Change item layout
assert node.item.itemLayout == nwItemLayout.NOTE
ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT)
assert node.item.itemLayout == nwItemLayout.DOCUMENT
ctxMenu._changeItemLayout(nwItemLayout.NOTE)
assert node.item.itemLayout == nwItemLayout.NOTE
# Convert Folders to Documents
# ============================
projView.setSelectedHandle(hNovelNote)
projTree.newTreeItem(nwItemType.FOLDER)
projView.setSelectedHandle(hNewFolderOne)
projTree.newTreeItem(nwItemType.FILE)
projView.setSelectedHandle(hNovelNote)
projTree.newTreeItem(nwItemType.FOLDER)
projView.setSelectedHandle(hNewFolderTwo)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
assert hNewFolderOne in tree
assert hNewFolderTwo in tree
nodeOne = tree.nodes[hNewFolderOne]
nodeTwo = tree.nodes[hNewFolderTwo]
# Select Folder One
indices = [model.indexFromHandle(hNewFolderOne)]
ctxMenu = _TreeContextMenu(projTree, model, nodeOne, indices)
ctxMenu.buildSingleSelectMenu()
# Click no on the dialog
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
assert nodeOne.item.isFolderType()
# Convert the first folder to a document
assert nodeOne.item.isFolderType()
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
assert nodeOne.item.isFileType()
assert nodeOne.item.isDocumentLayout()
# Select Folder Two
indices = [model.indexFromHandle(hNewFolderTwo)]
ctxMenu = _TreeContextMenu(projTree, model, nodeTwo, indices)
ctxMenu.buildSingleSelectMenu()
# Convert the second folder to a note
ctxMenu._convertFolderToFile(nwItemLayout.NOTE)
assert nodeTwo.item.isFileType()
assert nodeTwo.item.isNoteLayout()
# Direct Edit Functions, Multi
# ============================
node = tree.nodes[hCharNote]
indices = [
model.indexFromHandle(hCharNote),
model.indexFromHandle(hNovelNote),
model.indexFromHandle(hSubNote),
]
ctxMenu = _TreeContextMenu(projTree, model, node, indices)
ctxMenu.buildMultiSelectMenu()
# projTree.clearSelection()
# projTree._getTreeItem(hCharNote).setSelected(True)
# projTree._getTreeItem(hNovelNote).setSelected(True)
# projTree._getTreeItem(hSubNote).setSelected(True)
nodeCNote = tree.nodes[hCharNote]
nodeNNote = tree.nodes[hNovelNote]
nodeSNote = tree.nodes[hSubNote]
# Item Active
assert nodeCNote.item.isActive is True
assert nodeNNote.item.isActive is False
assert nodeSNote.item.isActive is True
ctxMenu._iterItemActive(False)
assert nodeCNote.item.isActive is False
assert nodeNNote.item.isActive is False
assert nodeSNote.item.isActive is False
ctxMenu._iterItemActive(True)
assert nodeCNote.item.isActive is True
assert nodeNNote.item.isActive is True
assert nodeSNote.item.isActive is True
# Item Status
assert nodeCNote.item.itemStatus == "s000000"
assert nodeNNote.item.itemStatus == "s000001"
assert nodeSNote.item.itemStatus == "s000000"
ctxMenu._iterSetItemStatus("s000003")
assert nodeCNote.item.itemStatus == "s000000"
assert nodeNNote.item.itemStatus == "s000003"
assert nodeSNote.item.itemStatus == "s000003"
# Item Importance
assert nodeCNote.item.itemImport == "i000004"
assert nodeNNote.item.itemImport == "i000005"
assert nodeSNote.item.itemImport == "i000004"
ctxMenu._iterSetItemImport("i000007")
assert nodeCNote.item.itemImport == "i000007"
assert nodeNNote.item.itemImport == "i000005"
assert nodeSNote.item.itemImport == "i000004"
# 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
projView = nwGUI.projView
projTree = projView.projTree
projBar = projView.projBar
project = SHARED.project
tree = project.tree
# Create a project
buildTestProject(nwGUI, projPath)
trash = tree.trash
assert trash is not None
assert len(tree) == 9
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
"Plot", "Characters", "Locations", "Trash",
]
# Handles for new objects
hTemplatesRoot = "0000000000011"
hSceneTemplate = "0000000000012"
hNoteTemplate = "0000000000013"
hNewScene = "0000000000014"
hNewCharacter = "0000000000015"
# Add template folder
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
nwTemplateRoot = tree[hTemplatesRoot]
assert nwTemplateRoot is not None
assert nwTemplateRoot.itemName == "Templates"
# Add a scene template
projTree.setSelectedHandle(hTemplatesRoot)
projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
nwSceneTemplate = tree[hSceneTemplate]
assert nwSceneTemplate is not None
assert nwSceneTemplate.itemName == "New Scene"
assert projBar.mTemplates.actions()[0].text() == "New Scene"
# Rename the scene template
projView.renameTreeItem(hSceneTemplate, name="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
projView.renameTreeItem(hNoteTemplate, name="Note")
assert projBar.mTemplates.actions()[1].text() == "Note"
# Add new content to template files
(projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
(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() == "# Note\n\n@tag: Jane\n\n"
# Clearing the menu and rebuilding it should work
projBar.mTemplates.clearMenu()
assert len(projBar.mTemplates.actions()) == 0
projBar.buildTemplatesMenu()
assert len(projBar.mTemplates.actions()) == 2
# Remove the note template
assert projBar.mTemplates.menuAction().isVisible() is True
assert len(projBar.mTemplates.actions()) == 2
assert trash.childCount() == 0
projTree.processDeleteRequest([hNoteTemplate])
assert trash.childCount() == 1
assert len(projBar.mTemplates.actions()) == 1
projTree.processDeleteRequest([hSceneTemplate])
assert len(projBar.mTemplates.actions()) == 0
assert projBar.mTemplates.menuAction().isVisible() is False
# qtbot.stop()