Skip to content

Commit

Permalink
Add test coverage of GuiManuscript tool
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo committed Jul 18, 2023
1 parent 1997872 commit 9721c25
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 19 deletions.
13 changes: 7 additions & 6 deletions novelwriter/tools/manuscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@

from PyQt5.QtGui import QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from PyQt5.QtWidgets import (
QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton,
QSplitter, QTextBrowser, QToolButton, QVBoxLayout, QWidget, qApp
)
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter

from novelwriter import CONFIG
from novelwriter.error import logException
Expand Down Expand Up @@ -300,7 +300,6 @@ def _generatePreview(self):
for step, _ in docBuild.iterBuildHTML(None):
self.docPreview.buildStep(step + 1)
qApp.processEvents()
qApp.thread().msleep(5)

buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml)
Expand Down Expand Up @@ -528,10 +527,11 @@ def setJustify(self, state: bool):

def setTextFont(self, family: str, size: int):
"""Set the text font properties."""
font = QFont()
font.setFamily(family)
font.setPointSize(size)
self.setFont(font)
if family:
font = QFont()
font.setFamily(family)
font.setPointSize(size)
self.setFont(font)
return

##
Expand Down Expand Up @@ -617,6 +617,7 @@ def printPreview(self, printer: QPrinter):
self.document().print(printer)
qApp.restoreOverrideCursor()
return

##
# Private Slots
##
Expand Down
242 changes: 242 additions & 0 deletions tests/test_tools/test_tools_manuscript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""
novelWriter – Manuscript Tool Tester
====================================
This file is a part of novelWriter
Copyright 2018–2023, 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/>.
"""

import pytest

from pathlib import Path
from pytestqt.qtbot import QtBot

from tools import C, buildTestProject, getGuiItem
from mocked import causeOSError

from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtPrintSupport import QPrintPreviewDialog

from novelwriter import CONFIG
from novelwriter.guimain import GuiMain
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings


@pytest.mark.gui
def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd):
"""Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.theProject.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi")
allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *"

manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == ""

# Run the default build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText

manus.close()

# A new dialog should load the old build
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == allText
manus.close()

# But blocking the reload should leave it empty
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == ""
manus.close()

# Finish
# qtbot.stop()

# END Test testManuscript_Init


@pytest.mark.gui
def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
"""Test the handling of builds in the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)

manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()

# Delete the default build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
manus.tbDel.click()
assert manus.buildList.count() == 0

# Create a new build
manus.tbAdd.click()
bSettings = getGuiItem("GuiBuildSettings")
assert isinstance(bSettings, GuiBuildSettings)
bSettings.editBuildName.setText("Test Build")
build = None

@pyqtSlot(BuildSettings)
def _testNewSettingsReady(new: BuildSettings):
nonlocal build
build = new

with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings.dlgButtons.button(QDialogButtonBox.Save).click()

assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
assert manus.buildList.count() == 1

# Edit the new build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
manus.tbEdit.click()

bSettings = getGuiItem("GuiBuildSettings")
assert isinstance(bSettings, GuiBuildSettings)
build = None

with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings.dlgButtons.button(QDialogButtonBox.Apply).click() # Should leave the dialog open

assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
assert manus.buildList.count() == 1

# Close the dialog should also close the child dialogs
manus.btnClose.click()
if isinstance(bSettings, GuiBuildSettings):
# May have been garbage collected, but if it isn't, it should be hidden
assert bSettings.isVisible() is False

# Finish
# qtbot.stop()

# END Test testManuscript_Builds


@pytest.mark.gui
def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
"""Test other featurs of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)

manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()

cacheFile = CONFIG.dataPath("cache") / f"build_{nwGUI.theProject.data.uuid}.json"
manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings)

# Previews
# ========

# No build selected
manus.buildList.clearSelection()
manus.btnPreview.click()
qtbot.wait(200) # Should be enough to run the build
assert manus.docPreview.toPlainText().strip() == ""
assert cacheFile.exists() is False

# Preview the first, but fail to save cache
manus.buildList.setCurrentRow(0)
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert cacheFile.exists() is False

# Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True

# Toggle justify
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute
manus.docPreview.setJustify(True)
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignJustify
manus.docPreview.setJustify(False)
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute

# Tests are too fast to trigger this one, so we trigger it manually to ensure it isn't failing
manus.docPreview._hideProgress()

# Builds
# ======

build._changed = True
manus.buildList.clearSelection()
with monkeypatch.context() as mp:
mp.setattr("novelwriter.tools.manusbuild.GuiManuscriptBuild.exec_", lambda *a: None)

# With no selection, no dialog should be created
manus.btnBuild.click()
for obj in manus.children():
assert not isinstance(obj, GuiManuscriptBuild)

# With a selection, there should be one
manus.buildList.setCurrentRow(0)
manus.btnBuild.click()
for obj in manus.children():
if isinstance(obj, GuiManuscriptBuild):
obj.close()
break
else:
assert False

# Print Preview
# =============

with monkeypatch.context() as mp:
mp.setattr(QPrintPreviewDialog, "exec_", lambda *a: None)
manus.btnPrint.click()
for obj in manus.children():
if isinstance(obj, QPrintPreviewDialog):
obj.show()
obj.close()
break
else:
assert False

# Finish
manus.close()
# qtbot.stop()

# END Test testManuscript_Features
22 changes: 9 additions & 13 deletions tests/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,32 +112,28 @@ def cmpFiles(
return not diffFound


def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
def getGuiItem(name: str):
"""Returns a QtWidget based on its objectName."""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
if qWidget.objectName() == name:
return qWidget
return None


def readFile(fileName):
"""Returns the content of a file as a string.
"""
def readFile(fileName: str | Path):
"""Returns the content of a file as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile:
return inFile.read()


def writeFile(fileName, fileData):
"""Write the contents of a string to a file.
"""
def writeFile(fileName: str | Path, fileData: str):
"""Write the contents of a string to a file."""
with open(fileName, mode="w", encoding="utf-8") as outFile:
outFile.write(fileData)


def cleanProject(path):
"""Delete all generated files in a project.
"""
def cleanProject(path: str | Path):
"""Delete all generated files in a project."""
path = Path(path)
cacheDir = path / "cache"
if cacheDir.is_dir():
Expand Down

0 comments on commit 9721c25

Please sign in to comment.