Update tests

This commit is contained in:
Veronica Berglyd Olsen
2026-07-14 22:05:22 +02:00
parent 38639932d9
commit 475a9dd45d
5 changed files with 164 additions and 41 deletions

View File

@@ -189,7 +189,7 @@ class NExpandablePanelGroup(QSplitter):
def _panelToggled(self, state: bool) -> None:
"""Collapse or expand a panel when its state changes."""
panel = self.sender()
if isinstance(panel, NExpandablePanel):
if isinstance(panel, NExpandablePanel): # pragma: no branch
if state:
self._expand(panel)
else:
@@ -225,5 +225,5 @@ class NExpandablePanelGroup(QSplitter):
before = self.widget(i - 1)
after = self.widget(i)
locked = any(isinstance(w, NExpandablePanel) and not w.isExpanded() for w in (before, after))
if isinstance(handle := self.handle(i), NSplitterHandle):
if isinstance(handle := self.handle(i), NSplitterHandle): # pragma: no branch
handle.setResizable(not locked)

View File

@@ -82,13 +82,13 @@ class NToolDialog(NDialog):
def __init__(self, parent: GuiMain) -> None:
super().__init__(parent=parent)
self.setModal(False)
if CONFIG.osDarwin:
if CONFIG.osDarwin: # pragma: no cover
self.setWindowFlag(Qt.WindowType.Tool)
def activateDialog(self) -> None:
"""Activate dialog on various operating systems."""
self.show()
if CONFIG.osWindows:
if CONFIG.osWindows: # pragma: no cover
self.activateWindow()
self.raise_()
QApplication.processEvents()
@@ -104,7 +104,7 @@ class NNonBlockingDialog(NDialog):
def activateDialog(self) -> None:
"""Activate dialog on various operating systems."""
self.show()
if CONFIG.osWindows:
if CONFIG.osWindows: # pragma: no cover
self.activateWindow()
self.raise_()
QApplication.processEvents()

View File

@@ -11,34 +11,6 @@ import sys
from pathlib import Path
def _genCoverageConfig(path: Path) -> None:
"""Generate a config file for coverage.py."""
exclude = [] # ["if TYPE_CHECKING:"]
if sys.platform != "linux":
exclude.append(r'\s*(if|elif) sys\.platform == "linux":')
exclude.append(r"\s*(if|elif) (CONFIG|self)\.osLinux")
if sys.platform != "darwin":
exclude.append(r'\s*(if|elif) sys\.platform == "darwin":')
exclude.append(r"\s*(if|elif) (CONFIG|self)\.osDarwin")
if sys.platform != "win32":
exclude.append(r'\s*(if|elif) sys\.platform == "win32":')
exclude.append(r"\s*(if|elif) (CONFIG|self)\.osWindows")
config = [
"[run]",
"branch = true",
"source = novelwriter",
"",
"[report]",
"precision = 2",
"exclude_also =",
]
config.extend(f" {ex}" for ex in exclude)
path.write_text("\n".join(config), encoding="utf-8")
if __name__ == "__main__":
"""Parse command line options and run the commands."""
parser = argparse.ArgumentParser(usage="run_tests.py [--flags]")
@@ -46,7 +18,6 @@ if __name__ == "__main__":
parser.add_argument("-r", action="store_true", help="Generate reports")
parser.add_argument("-t", action="store_true", help="Generate terminal report")
parser.add_argument("-u", action="store_true", help="Generate uncovered terminal report")
parser.add_argument("-x", action="store_true", help="Ignore OS-specific tests in reports")
parser.add_argument("-lf", action="store_true", help="Re-run failed tests")
parser.add_argument("-sw", action="store_true", help="Run tests stepwise")
parser.add_argument("-m", help="Test modules", metavar="MARKEXPR")
@@ -59,8 +30,6 @@ if __name__ == "__main__":
tmpConf = Path(__file__).parent / ".coveragerc"
if args.r or args.t or args.u:
if args.x:
_genCoverageConfig(tmpConf)
cmd = ["coverage", "run"]
if args.lf or args.sw:
cmd += ["--append"]

View File

@@ -23,9 +23,11 @@ from __future__ import annotations
import pytest
from PyQt6.QtWidgets import QLabel, QVBoxLayout
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QWIDGETSIZE_MAX, QLabel, QVBoxLayout
from novelwriter.extensions.expandpanel import NExpandablePanel
from novelwriter.extensions.expandpanel import NExpandablePanel, NExpandablePanelGroup
from novelwriter.extensions.modified import NSplitterHandle
from novelwriter.types import QtMouseLeft
from tests.helpers import SimpleDialog
@@ -59,18 +61,27 @@ def testNExpandablePanel_Main(qtbot, mockGUI):
panel.setContentLayout(content)
assert panel._ep_widget.layout() is content
# Click the toggle button to collapse the panel
# Set the header background role
panel.setHeaderBackgroundRole(QPalette.ColorRole.Highlight)
assert panel._ep_headerBox.backgroundRole() == QPalette.ColorRole.Highlight
assert panel._ep_headerBox.autoFillBackground() is True
# Click the toggle button to collapse the panel, which locks its
# height to that of the header
qtbot.mouseClick(panel._ep_toggle, QtMouseLeft)
assert panel.isExpanded() is False
assert panel._ep_widget.isVisible() is False
assert panel._ep_toggle.isChecked() is False
assert panel.maximumHeight() == panel.minimumSizeHint().height()
assert changes == [False]
# Click the title label to expand the panel again
# Click the title label to expand the panel again, which lifts the
# height restriction
qtbot.mouseClick(panel._ep_label, QtMouseLeft)
assert panel.isExpanded() is True
assert panel._ep_widget.isVisible() is True
assert panel._ep_toggle.isChecked() is True
assert panel.maximumHeight() == QWIDGETSIZE_MAX
assert changes == [False, True]
# Set the expanded state directly, which must also update the toggle
@@ -101,3 +112,82 @@ def testNExpandablePanel_Main(qtbot, mockGUI):
# Update the theme
panel.updateTheme()
@pytest.mark.gui
def testNExpandablePanelGroup_Main(qtbot, mockGUI):
"""Test the NExpandablePanelGroup class."""
dialog = SimpleDialog()
group = NExpandablePanelGroup(dialog)
dialog.addWidget(group)
panel = NExpandablePanel()
panel.setTitle("Panel")
content = QVBoxLayout()
content.addWidget(QLabel("Some content that needs space", panel))
panel.setContentLayout(content)
other = QLabel("Other pane")
group.addWidget(panel)
group.addWidget(other)
dialog.resize(400, 600)
dialog.show()
qtbot.addWidget(dialog)
qtbot.wait(10)
# Adding a panel connects its state change, tracks its size, and
# applies the group's header background role
assert panel in group._expandedSizes
assert panel._ep_headerBox.backgroundRole() == QPalette.ColorRole.AlternateBase
assert panel._ep_headerBox.autoFillBackground() is True
# Both panes start expanded, so the handle between them is resizable
handle = group.handle(1)
assert isinstance(handle, NSplitterHandle)
assert handle._resizable is True
# A size of zero or less does not overwrite the remembered expanded
# size, since it does not represent a meaningful size to restore to
beforeSize = group._expandedSizes[panel]
group.setPanelSizes([0, 200])
assert group._expandedSizes[panel] == beforeSize
# Setting sizes is remembered as the panel's expanded size. The actual
# on-screen pixel sizes are not checked here, as QSplitter distributes
# them proportionally to fit the available space, not verbatim.
group.setPanelSizes([150, 200])
assert group._expandedSizes[panel] == 150
qtbot.wait(10)
expandedHeight = group.sizes()[0]
# Collapsing the panel locks it to its header height, remembers the
# actual size it had, and disables the now-useless handle
panel.setExpanded(False)
qtbot.wait(10)
collapsedHeight = panel.minimumSizeHint().height()
assert group.sizes()[0] == collapsedHeight
assert group.panelSizes()[0] == expandedHeight
assert handle._resizable is False
# Expanding it again grows the panel back out and re-enables the handle.
# The exact pixel value may differ from what was remembered, since the
# other pane may since have grown to fill the space that was freed up,
# leaving less than requested for the splitter to redistribute back
panel.setExpanded(True)
qtbot.wait(10)
assert group.sizes()[0] > collapsedHeight
assert handle._resizable is True
# Adding a panel that starts out collapsed keeps the handle disabled
# and does not reserve space it cannot actually use
collapsed = NExpandablePanel()
collapsed.setTitle("Collapsed")
collapsed.setExpanded(False)
group.addWidget(collapsed)
qtbot.wait(10)
lastHandle = group.handle(2)
assert isinstance(lastHandle, NSplitterHandle)
assert lastHandle._resizable is False
assert group.sizes()[2] == collapsed.minimumSizeHint().height()

View File

@@ -27,7 +27,7 @@ import pytest
from PyQt6.QtCore import QEvent, QPoint, QPointF, QSize, Qt
from PyQt6.QtGui import QFont, QKeyEvent, QMouseEvent, QStandardItem, QStandardItemModel, QWheelEvent
from PyQt6.QtWidgets import QFontDialog, QWidget
from PyQt6.QtWidgets import QFontDialog, QSplitter, QWidget
from novelwriter.extensions.modified import (
NClickableLabel,
@@ -39,6 +39,7 @@ from novelwriter.extensions.modified import (
NIconToolButton,
NNonBlockingDialog,
NSpinBox,
NSplitterHandle,
NToolDialog,
NTreeView,
)
@@ -290,3 +291,66 @@ def testNToolButtons_Main(qtbot, mockGUI):
assert button1.isCheckable() is True
button1.setCheckable(False)
assert button1.isCheckable() is False
@pytest.mark.gui
def testNSplitterHandle_Main(qtbot):
"""Test the NSplitterHandle class."""
splitter = QSplitter(Qt.Orientation.Vertical)
handle = NSplitterHandle(Qt.Orientation.Vertical, splitter)
dialog = SimpleDialog(splitter)
dialog.show()
qtbot.addWidget(dialog)
qtbot.wait(10)
# Starts out resizable, with the vertical split cursor, and inactive
assert handle._resizable is True
assert handle.cursor().shape() == Qt.CursorShape.SplitVCursor
assert handle._active is False
# Paint while inactive draws nothing but must not fail
handle.repaint()
# Hovering activates the highlight, and leaving drops it again
handle.event(QEvent(QEvent.Type.HoverEnter))
assert handle._active is True
handle.event(QEvent(QEvent.Type.HoverLeave))
assert handle._active is False
# A mouse press keeps it highlighted while dragging, and the highlight
# is now actually painted
position = QPointF(handle.rect().center())
press = QMouseEvent(QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModNone)
handle.mousePressEvent(press)
assert handle._active is True
handle.repaint()
# On release, the highlight follows whether the cursor is still over it
release = QMouseEvent(QEvent.Type.MouseButtonRelease, position, QtMouseLeft, QtMouseLeft, QtModNone)
handle.mouseReleaseEvent(release)
assert handle._active is handle.underMouse()
# Marking it non-resizable drops any active highlight, switches to a
# plain arrow cursor, and suppresses further hover/press highlighting
handle.setResizable(False)
assert handle._resizable is False
assert handle._active is False
assert handle.cursor().shape() == Qt.CursorShape.ArrowCursor
handle.event(QEvent(QEvent.Type.HoverEnter))
assert handle._active is False
handle.mousePressEvent(press)
assert handle._active is False
# Marking it resizable again restores the highlight cursor
handle.setResizable(True)
assert handle._resizable is True
assert handle.cursor().shape() == Qt.CursorShape.SplitVCursor
# The horizontal orientation uses the horizontal split cursor instead
hSplitter = QSplitter(Qt.Orientation.Horizontal)
hHandle = NSplitterHandle(Qt.Orientation.Horizontal, hSplitter)
assert hHandle.cursor().shape() == Qt.CursorShape.SplitHCursor
hHandle.setResizable(False)
hHandle.setResizable(True)
assert hHandle.cursor().shape() == Qt.CursorShape.SplitHCursor