Improve test coverage
This commit is contained in:
+12
-13
@@ -28,7 +28,7 @@ import logging
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QKeyEvent, QPalette
|
||||
from PyQt5.QtGui import QCursor, QKeyEvent, QPalette
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget, qApp
|
||||
@@ -110,7 +110,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.searchResult.setColumnCount(2)
|
||||
self.searchResult.setIconSize(QSize(iPx, iPx))
|
||||
self.searchResult.setIndentation(iPx)
|
||||
self.searchResult.itemPressed.connect(self._searchResultPressed)
|
||||
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
|
||||
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
|
||||
|
||||
@@ -160,8 +159,15 @@ class GuiProjectSearch(QWidget):
|
||||
"""Process a return keypress forwarded from the main GUI."""
|
||||
if self.searchText.hasFocus():
|
||||
self._processSearch()
|
||||
elif items := self.searchResult.selectedItems():
|
||||
self._searchResultPressed(items[0], 0)
|
||||
elif (
|
||||
self.searchResult.hasFocus()
|
||||
and (items := self.searchResult.selectedItems())
|
||||
and (data := items[0].data(0, self.D_RESULT))
|
||||
and len(data) == 3
|
||||
):
|
||||
self.openDocumentSelectRequest.emit(
|
||||
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
||||
)
|
||||
return
|
||||
|
||||
def beginSearch(self) -> None:
|
||||
@@ -211,6 +217,7 @@ class GuiProjectSearch(QWidget):
|
||||
def _processSearch(self) -> None:
|
||||
"""Perform a search."""
|
||||
if not self._blocked:
|
||||
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||
start = time()
|
||||
self._blocked = True
|
||||
self.searchResult.clear()
|
||||
@@ -222,6 +229,7 @@ class GuiProjectSearch(QWidget):
|
||||
self._appendResultSet(item, results, capped)
|
||||
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
||||
self._time = time()
|
||||
qApp.restoreOverrideCursor()
|
||||
self._blocked = False
|
||||
return
|
||||
|
||||
@@ -235,15 +243,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.selectedItemChanged.emit(str(data))
|
||||
return
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _searchResultPressed(self, item: QTreeWidgetItem, column: int) -> None:
|
||||
"""Process search result pressed."""
|
||||
if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
|
||||
self.openDocumentSelectRequest.emit(
|
||||
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
||||
)
|
||||
return
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
|
||||
"""Process search result double click."""
|
||||
|
||||
@@ -32,9 +32,11 @@ from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
|
||||
from mocked import causeOSError
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwFiles, nwItemClass
|
||||
from novelwriter.constants import nwConst, nwFiles, nwItemClass
|
||||
from novelwriter.core.coretools import (
|
||||
DocDuplicator, DocMerger, DocSearch, DocSplitter, ProjectBuilder
|
||||
)
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -401,6 +403,97 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
|
||||
# END Test testCoreTools_DocDuplicator
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
|
||||
"""Test the DocDuplicator utility."""
|
||||
project = NWProject()
|
||||
mockRnd.reset()
|
||||
buildTestProject(project, fncPath)
|
||||
project.storage.getDocument(C.hSceneDoc).writeDocument(
|
||||
"### New Scene\n\n" + "\n\n".join(ipsumText)
|
||||
)
|
||||
|
||||
search = DocSearch()
|
||||
|
||||
# Defaults
|
||||
# ========
|
||||
|
||||
result = [(i.itemHandle, r, c) for i, r, c in search.iterSearch(project, "Scene")]
|
||||
assert result[0] == (C.hTitlePage, [], False)
|
||||
assert result[1] == (C.hChapterDoc, [], False)
|
||||
assert result[2] == (C.hSceneDoc, [(8, 5, "Scene")], False)
|
||||
|
||||
# Cache
|
||||
assert list(search._cache.keys()) == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
|
||||
|
||||
# Patterns
|
||||
# ========
|
||||
|
||||
# Escape Using QRegularExpression
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x050f00)
|
||||
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
|
||||
|
||||
# Escape Using Custom Implementation
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(CONFIG, "verQtValue", 0x050d00)
|
||||
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
|
||||
|
||||
# Whole Words
|
||||
search.setWholeWords(True)
|
||||
search.setUserRegEx(True)
|
||||
assert search._buildPattern("Hi") == r"\bHi\b"
|
||||
assert search._buildPattern(r"\bHi") == r"\bHi\b"
|
||||
assert search._buildPattern(r"Hi\b") == r"\bHi\b"
|
||||
assert search._buildPattern(r"\bHi\b") == r"\bHi\b"
|
||||
search.setWholeWords(False)
|
||||
search.setUserRegEx(False)
|
||||
|
||||
# Test Settings
|
||||
# =============
|
||||
|
||||
def pruneResult(result, index):
|
||||
temp = [(i.itemHandle, r, c) for i, r, c in result][index][1]
|
||||
return [(s, n, c.split()[0]) for s, n, c in temp]
|
||||
|
||||
# Defaults
|
||||
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
|
||||
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"), (2209, 5, "lorem"),
|
||||
(2425, 5, "lorem"), (2840, 5, "lorem."), (3399, 5, "lorem"),
|
||||
]
|
||||
|
||||
# Whole Words
|
||||
search.setWholeWords(True)
|
||||
assert pruneResult(search.iterSearch(project, "Lor"), 2) == []
|
||||
search.setWholeWords(False)
|
||||
assert pruneResult(search.iterSearch(project, "Lor"), 2) == [
|
||||
(15, 3, "Lorem"), (29, 3, "lor"), (754, 3, "lorem"), (2056, 3, "lorem,"),
|
||||
(2209, 3, "lorem"), (2425, 3, "lorem"), (2840, 3, "lorem."), (3328, 3, "lor."),
|
||||
(3399, 3, "lorem"),
|
||||
]
|
||||
|
||||
# As RegEx
|
||||
search.setWholeWords(False)
|
||||
search.setUserRegEx(True)
|
||||
assert pruneResult(search.iterSearch(project, r"Lor\b"), 2) == [
|
||||
(29, 3, "lor"), (3328, 3, "lor."),
|
||||
]
|
||||
|
||||
# Max Results
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(nwConst, "MAX_SEARCH_RESULT", 3)
|
||||
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
|
||||
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"),
|
||||
]
|
||||
|
||||
# Case Sensitive
|
||||
search.setCaseSensitive(True)
|
||||
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [(15, 5, "Lorem")]
|
||||
search.setCaseSensitive(False)
|
||||
|
||||
# END Test testCoreTools_DocSearch
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
|
||||
"""Test the wrapper function of the project builder."""
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
novelWriter – Main GUI Project Search Tester
|
||||
============================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QAction
|
||||
|
||||
from novelwriter.enum import nwView
|
||||
from novelwriter.gui.search import GuiProjectSearch
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiDocSearch_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
"""Test navigating the novel tree."""
|
||||
nwGUI.openProject(prjLipsum)
|
||||
nwGUI._changeView(nwView.SEARCH)
|
||||
search = nwGUI.projSearch
|
||||
|
||||
def totalCount():
|
||||
nonlocal search
|
||||
res = search.searchResult
|
||||
return sum(
|
||||
int(res.topLevelItem(i).text(GuiProjectSearch.C_COUNT).strip("()"))
|
||||
for i in range(res.topLevelItemCount())
|
||||
)
|
||||
|
||||
# Plain search
|
||||
search.searchText.setText("Lorem")
|
||||
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||
assert search.searchResult.topLevelItemCount() == 14
|
||||
assert totalCount() == 42
|
||||
|
||||
firstDoc = search.searchResult.topLevelItem(0)
|
||||
firstResult = firstDoc.child(0)
|
||||
assert firstDoc is not None
|
||||
handle = firstDoc.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_HANDLE)
|
||||
result = firstResult.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_RESULT)
|
||||
assert result == (handle, 3, 5)
|
||||
|
||||
# Move down
|
||||
qtbot.keyClick(search, Qt.Key.Key_Down)
|
||||
assert firstDoc.isSelected() is True
|
||||
|
||||
# Move up
|
||||
qtbot.keyClick(search, Qt.Key.Key_Up)
|
||||
assert firstDoc.isSelected() is False
|
||||
|
||||
# Move right does nothing
|
||||
qtbot.keyClick(search, Qt.Key.Key_Right)
|
||||
assert firstDoc.isSelected() is False
|
||||
|
||||
# Selecting updates details
|
||||
firstDoc.setSelected(True)
|
||||
assert nwGUI.itemDetails._handle == handle
|
||||
|
||||
# Press return
|
||||
search.searchResult.setFocus()
|
||||
search.searchResult.clearSelection()
|
||||
firstResult.setSelected(True)
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(search.searchResult, "hasFocus", lambda *a: True)
|
||||
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
|
||||
qtbot.keyClick(search, Qt.Key.Key_Return)
|
||||
assert signal.args == [handle, 3, 5, False]
|
||||
|
||||
assert nwGUI.docEditor.docHandle == handle
|
||||
assert nwGUI.docEditor.textCursor().selectedText() == "Lorem"
|
||||
|
||||
# Double-click
|
||||
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
|
||||
search._searchResultDoubleClicked(firstResult, 0)
|
||||
assert signal.args == [handle, 3, 5, True]
|
||||
|
||||
# Case Sensitive
|
||||
search.toggleCase.setChecked(True)
|
||||
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||
assert search.searchResult.topLevelItemCount() == 7
|
||||
assert totalCount() == 17
|
||||
search.toggleCase.setChecked(False)
|
||||
|
||||
# Whole Words
|
||||
search.searchText.setText("dolor")
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(search.searchText, "hasFocus", lambda *a: True)
|
||||
qtbot.keyClick(search, Qt.Key.Key_Return)
|
||||
|
||||
assert search.searchResult.topLevelItemCount() == 10
|
||||
assert totalCount() == 34
|
||||
|
||||
search.toggleWord.setChecked(True)
|
||||
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||
assert search.searchResult.topLevelItemCount() == 10
|
||||
assert totalCount() == 33
|
||||
|
||||
# RegEx
|
||||
search.toggleRegEx.setChecked(True)
|
||||
search.searchText.setText("(dolor|dolorem)")
|
||||
search.searchAction.activate(QAction.ActionEvent.Trigger)
|
||||
assert search.searchResult.topLevelItemCount() == 10
|
||||
assert totalCount() == 34
|
||||
|
||||
# qtbot.stop()
|
||||
nwGUI.closeProject()
|
||||
|
||||
# END Test testGuiDocSearch_Main
|
||||
Reference in New Issue
Block a user