1477 lines
54 KiB
Python
1477 lines
54 KiB
Python
"""
|
||
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()
|