Add novel view test coverage

This commit is contained in:
Veronica Berglyd Olsen
2025-03-30 22:28:37 +02:00
parent 1090a160de
commit 95bef4c6c2
5 changed files with 116 additions and 147 deletions
+10
View File
@@ -26,6 +26,7 @@ from __future__ import annotations
import logging
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QComboBox, QWidget
from novelwriter import SHARED
@@ -46,6 +47,7 @@ class NovelSelector(QComboBox):
self._includeAll = False
self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged)
self.updateTheme()
return
##
@@ -86,6 +88,14 @@ class NovelSelector(QComboBox):
self._listFormat = value
return
def updateTheme(self) -> None:
"""Update theme colours."""
palette = self.palette()
palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text())
self.setPalette(palette)
self.refreshNovelList()
return
##
# Public Slots
##
-1
View File
@@ -115,7 +115,6 @@ class GuiDocEditor(QPlainTextEdit):
editedStatusChanged = pyqtSignal(bool)
itemHandleChanged = pyqtSignal(str)
loadDocumentTagRequest = pyqtSignal(str, Enum)
novelStructureChanged = pyqtSignal()
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
requestNewNoteCreation = pyqtSignal(str, nwItemClass)
requestNextDocument = pyqtSignal(str, bool)
+33 -21
View File
@@ -3,9 +3,11 @@ novelWriter GUI Novel Tree
============================
File History:
Created: 2020-12-20 [1.1rc1] GuiNovelTree
Created: 2022-06-12 [2.0rc1] GuiNovelView
Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
Created: 2020-12-20 [1.1rc1] GuiNovelTree
Created: 2022-06-12 [2.0rc1] GuiNovelView
Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
Rewritten: 2025-02-22 [2.7b1] GuiNovelView
Rewritten: 2025-02-22 [2.7b1] GuiNovelToolBar
This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
@@ -40,7 +42,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax, qtAddAction, qtAddMenu, qtLambda
from novelwriter.constants import nwKeyWords, nwLabels, trConst
from novelwriter.core.novelmodel import NovelModel
from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwNovelExtra, nwOutline
from novelwriter.enum import nwChange, nwDocMode, nwNovelExtra, nwOutline
from novelwriter.extensions.modified import NIconToolButton, NTreeView
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
@@ -105,9 +107,6 @@ class GuiNovelView(QWidget):
def openProjectTasks(self) -> None:
"""Run open project tasks."""
lastNovel = SHARED.project.data.getLastHandle("novel")
if lastNovel and lastNovel not in SHARED.project.tree:
lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting novel tree to root item '%s'", lastNovel)
lastCol = SHARED.project.options.getEnum(
@@ -129,13 +128,17 @@ class GuiNovelView(QWidget):
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
logger.debug("Saving State: GuiNovelView")
lastColType = self.novelTree.lastColType
lastColSize = self.novelTree.lastColSize
logger.debug("Saving State: GuiNovelView")
options = SHARED.project.options
options.setValue("GuiNovelView", "lastCol", lastColType)
options.setValue("GuiNovelView", "lastColSize", lastColSize)
self.clearNovelView()
return
def setTreeFocus(self) -> None:
@@ -154,9 +157,7 @@ class GuiNovelView(QWidget):
@pyqtSlot(str)
def setCurrentNovel(self, rootHandle: str | None) -> None:
"""Set the current novel to display."""
if rootHandle and (model := SHARED.project.index.getNovelModel(rootHandle)):
self.novelTree.setModel(model)
self.novelTree.resizeColumns()
self.novelTree.setNovelModel(rootHandle)
return
@pyqtSlot(str)
@@ -270,7 +271,7 @@ class GuiNovelToolBar(QWidget):
"QComboBox {border-style: none; padding-left: 0;} "
"QComboBox::drop-down {border-style: none}"
)
self.novelValue.refreshNovelList()
self.novelValue.updateTheme()
self.tbNovel.setVisible(self.novelValue.count() > 1)
self._forceRefreshNovelTree()
@@ -286,12 +287,13 @@ class GuiNovelToolBar(QWidget):
def buildNovelRootMenu(self) -> None:
"""Build the novel root menu."""
self.novelValue.refreshNovelList()
self.novelView.setCurrentNovel(self.novelValue.handle)
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
if rootHandle is None:
if rootHandle is None or rootHandle not in SHARED.project.tree:
rootHandle = self.novelValue.firstHandle
self.novelValue.setHandle(rootHandle)
SHARED.project.data.setLastHandle(rootHandle, "novel")
@@ -328,6 +330,7 @@ class GuiNovelToolBar(QWidget):
def _forceRefreshNovelTree(self) -> None:
"""Rebuild the current tree."""
if tHandle := self.novelValue.handle:
self.novelView.setCurrentNovel(tHandle)
SHARED.project.index.refreshNovelModel(tHandle)
self._refresh[tHandle] = False
return
@@ -441,8 +444,7 @@ class GuiNovelTree(NTreeView):
"""Get the currently selected or active handle. If multiple
items are selected, return the first.
"""
if model := self._getModel():
index = self.currentIndex()
if (model := self._getModel()) and (index := self.currentIndex()).isValid():
return model.handle(index), model.key(index)
return None, None
@@ -450,6 +452,16 @@ class GuiNovelTree(NTreeView):
# Setters
##
def setNovelModel(self, tHandle: str | None) -> None:
"""Set the current novel model."""
if tHandle and (model := SHARED.project.index.getNovelModel(tHandle)):
if model is not self.model():
self.setModel(model)
self.resizeColumns()
else:
self.clearContent()
return
def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted."""
self._actHandle = tHandle
@@ -560,18 +572,18 @@ class GuiNovelTree(NTreeView):
def _popMetaBox(self, qPos: QPoint, tHandle: str, sTitle: str) -> None:
"""Show the novel meta data box."""
def appendTags(refs: dict, key: str, lines: list[str]) -> None:
"""Generate a reference list for a given reference key."""
if tags := ", ".join(refs.get(key, [])):
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
return
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
if synopsis := head.synopsis:
label = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
synopsis = f"<p><b>{label}:</b> {synopsis}</p>"
def appendTags(refs: dict, key: str, lines: list[str]) -> None:
"""Generate a reference list for a given reference key."""
if tags := ", ".join(refs.get(key, [])):
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
return
lines = []
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
tags = head.getReferences()
+72 -125
View File
@@ -24,22 +24,19 @@ from pathlib import Path
import pytest
from PyQt6.QtCore import QEvent, QPoint, Qt
from PyQt6.QtGui import QFocusEvent
from PyQt6.QtCore import QModelIndex, QPoint, Qt
from PyQt6.QtWidgets import QInputDialog, QToolTip
from novelwriter import CONFIG, SHARED
from novelwriter.core.novelmodel import NovelModel
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwFocus, nwItemType, nwNovelExtra
from novelwriter.gui.noveltree import GuiNovelTree
from novelwriter.types import QtMouseLeft, QtMouseMiddle
from novelwriter.enum import nwFocus, nwItemType, nwNovelExtra, nwView
from tests.tools import C, buildTestProject
@pytest.mark.gui
@pytest.mark.skip
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
def testGuiNovelView_Content(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test navigating the novel tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
@@ -52,20 +49,22 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
contentPath = SHARED.project.storage.contentPath
assert isinstance(contentPath, Path)
cHandle = "0000000000010"
(contentPath / "0000000000010.nwd").write_text(
(contentPath / f"{cHandle}.nwd").write_text(
"# Jane Doe\n\n@tag: Jane\n\n", encoding="utf-8"
)
(contentPath / "000000000000f.nwd").write_text((
(contentPath / f"{C.hSceneDoc}.nwd").write_text((
"### Scene One\n\n"
"@pov: Jane\n"
"@focus: Jane\n\n"
"% Synopsis: This is a scene."
"% Synopsis: This is a scene.\n\n"
"This is some text in the edited scene."
), encoding="utf-8")
novelView = nwGUI.novelView
novelTree = novelView.novelTree
novelBar = novelView.novelBar
novelTree = nwGUI.novelView.novelTree
novelBar = nwGUI.novelView.novelBar
# Show/Hide Scrollbars
# ====================
@@ -84,153 +83,101 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Populate Tree
# =============
root = QModelIndex()
novelView.setTreeFocus()
nwGUI._changeView(nwView.NOVEL)
nwGUI.projStack.setCurrentWidget(nwGUI.novelView)
nwGUI.rebuildIndex()
novelTree._populateTree(rootHandle=None)
assert novelTree.topLevelItemCount() == 3
# Clear tree
novelView.setCurrentNovel(None)
assert novelTree._getModel() is None
# Rebuild should preserve selection
topItem = novelTree.topLevelItem(0)
assert not topItem.isSelected()
topItem.setSelected(True)
assert novelTree.selectedItems()[0] == topItem
assert novelView.getSelectedHandle() == (C.hTitlePage, "T0001")
# Reload
novelBar._forceRefreshNovelTree()
model = novelTree._getModel()
assert isinstance(model, NovelModel)
# Refresh using the slot for the button
novelBar._refreshNovelTree()
assert novelTree.topLevelItem(0).isSelected()
# Check the items
assert model.rowCount(root) == 3
assert model.columnCount(root) == 3
assert model.data(model.createIndex(2, 1), Qt.ItemDataRole.DisplayRole) == "2" # Word Count
nwGUI.rebuildIndex() # This should update the word count to the edited scene
assert model.data(model.createIndex(2, 1), Qt.ItemDataRole.DisplayRole) == "10" # Word Count
# Extra Column
# ============
novelBar.setLastColType(nwNovelExtra.POV)
assert model.rowCount(root) == 3
assert model.columnCount(root) == 4
# Scene column should contain the POV character
assert model.data(model.createIndex(2, 2), Qt.ItemDataRole.DisplayRole) == "Jane"
# Resize the last column
assert novelTree.lastColSize == 25
with monkeypatch.context() as mp:
mp.setattr(QInputDialog, "getInt", lambda *a, **k: (40, True))
novelBar._selectLastColumnSize()
assert novelTree.lastColSize == 40
# Open Items
# ==========
# Clear selection
novelTree.clearSelection()
scItem = novelTree.topLevelItem(2)
scItem.setSelected(True)
assert scItem.isSelected()
assert novelView.getSelectedHandle() == (None, None)
# Clear selection with mouse
vPort = novelTree.viewport()
qtbot.mouseClick(vPort, QtMouseLeft, pos=vPort.rect().center(), delay=10)
assert not scItem.isSelected()
# Select scene
novelTree.setCurrentIndex(model.createIndex(2, 0))
assert novelView.getSelectedHandle() == (C.hSceneDoc, "T0001")
# Double-click item
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docEditor.docHandle is None
novelTree._treeDoubleClick(scItem, 0)
novelTree._onDoubleClick(model.createIndex(2, 0))
assert nwGUI.docEditor.docHandle == C.hSceneDoc
# Open item with middle mouse button
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docViewer.docHandle is None
qtbot.mouseClick(vPort, QtMouseMiddle, pos=vPort.rect().center(), delay=10)
assert nwGUI.docViewer.docHandle is None
scRect = novelTree.visualItemRect(scItem)
oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE)
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None)
qtbot.mouseClick(vPort, QtMouseMiddle, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle is None
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, QtMouseMiddle, pos=scRect.center(), delay=10)
# Middle-click item
novelTree._onMiddleClick(model.createIndex(2, 0))
assert nwGUI.docViewer.docHandle == C.hSceneDoc
# Last Column
# ===========
novelBar.setLastColType(nwNovelExtra.HIDDEN)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
assert novelTree.lastColType == nwNovelExtra.HIDDEN
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "")
novelBar.setLastColType(nwNovelExtra.PLOT)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == nwNovelExtra.PLOT
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"", ""
)
novelBar.setLastColType(nwNovelExtra.FOCUS)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == nwNovelExtra.FOCUS
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"Jane", "Focus: Jane"
)
novelBar.setLastColType(nwNovelExtra.POV)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == nwNovelExtra.POV
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"Jane", "Point of View: Jane"
)
novelTree._lastCol = None
assert novelTree._getLastColumnText("0000000000000", "T0000") == ("", "")
# This forces the resizeEvent function to process labels
spSize = nwGUI.splitMain.sizes()
nwGUI.splitMain.setSizes([spSize[0] + 10, spSize[1] - 10])
# Resize the last column
with monkeypatch.context() as mp:
mp.setattr(QInputDialog, "getInt", lambda *a, **k: (40, True))
novelBar._selectLastColumnSize()
# Item Meta
# =========
ttText = ""
toolTip = ""
def showText(pos, text):
nonlocal ttText
ttText = text
nonlocal toolTip
toolTip = text
mIndex = novelTree.model().index(2, novelTree.C_MORE)
with monkeypatch.context() as mp:
mp.setattr(QToolTip, "showText", showText)
ttText = ""
novelTree._treeItemClicked(mIndex)
assert ttText == (
"<p><b>Point of View</b>: Jane<br><b>Focus</b>: Jane</p>"
"<p><b>Synopsis</b>: This is a scene.</p>"
toolTip = ""
novelTree._onSingleClick(model.createIndex(2, 3))
assert toolTip == (
"<p><b>Point of View:</b> Jane<br><b>Focus:</b> Jane</p>"
"<p><b>Synopsis:</b> This is a scene.</p>"
)
ttText = ""
toolTip = ""
novelTree._popMetaBox(QPoint(1, 1), C.hInvalid, "T0001")
assert ttText == ""
assert toolTip == ""
# Set Default Root
# ================
SHARED.project.data.setLastHandle(C.hInvalid, "novel")
novelView.openProjectTasks()
assert novelBar.novelValue.handle == C.hNovelRoot
# Active Status
# =============
assert novelBar._refresh == {C.hNovelRoot: False}
# Tree Focus
# ==========
with monkeypatch.context() as mp:
mp.setattr(GuiNovelTree, "hasFocus", lambda *a: False)
assert novelView.treeHasFocus() is False
mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True)
assert novelView.treeHasFocus() is True
# Add a document while tree in focus
nwGUI._changeView(nwView.PROJECT)
assert novelBar._active is False
nwGUI.projView.projTree.setSelectedHandle(C.hChapterDir)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3)
assert novelBar._refresh == {C.hNovelRoot: True}
# Other Checks
# ============
scItem = novelTree.topLevelItem(2)
scItem.setSelected(True)
assert scItem.isSelected()
novelTree.focusOutEvent(QFocusEvent(QEvent.Type.None_, Qt.FocusReason.MouseFocusReason))
assert not scItem.isSelected()
# Switch back and check that the refresh status is reset
nwGUI._changeView(nwView.NOVEL)
assert novelBar._refresh == {C.hNovelRoot: False}
# Close
# =====
# qtbot.stop()
nwGUI.closeProject()
+1
View File
@@ -216,6 +216,7 @@ def buildTestProject(obj: object, projPath: Path) -> None:
project.setProjectChanged(True)
project.saveProject(autoSave=True)
project._valid = True
project._tree._ready = True
if nwGUI is not None:
nwGUI.projView.openProjectTasks()