From fc096ddb0bb60d9fe19f0eb2b6f64a00044592a9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:17:14 +0200 Subject: [PATCH] Significant improvements to dialog tests --- nw/__init__.py | 2 +- nw/config.py | 2 +- nw/core/project.py | 13 ++-- nw/error.py | 2 +- nw/gui/build.py | 4 +- nw/gui/docsplit.py | 2 +- nw/gui/mainmenu.py | 8 +-- nw/gui/preferences.py | 4 +- nw/gui/projload.py | 4 +- nw/gui/projsettings.py | 16 ++++- nw/gui/projtree.py | 8 +-- nw/gui/writingstats.py | 4 +- nw/guimain.py | 138 ++++++++++++++++++++++------------------- tests/conftest.py | 84 +++++++++++++++++++++++-- tests/test_dialogs.py | 100 ++++++++++++++++++++++++----- tests/test_gui.py | 4 ++ tests/test_project.py | 4 +- 17 files changed, 286 insertions(+), 113 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 95f15281..e9432711 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -195,7 +195,7 @@ def main(sysArgs=None): testMode = True # Set Config Options - CONFIG.blockGUI = not testMode + CONFIG.showGUI = not testMode CONFIG.debugInfo = debugLevel < logging.INFO CONFIG.cmdOpen = cmdOpen diff --git a/nw/config.py b/nw/config.py index 93141fa5..db8a6c17 100644 --- a/nw/config.py +++ b/nw/config.py @@ -56,7 +56,7 @@ class Config: self.appHandle = self.appName.lower() # Debug Settings - self.blockGUI = True # Allow blocking the GUI (disabled for testing) + self.showGUI = True # Allow blocking the GUI (disabled for testing) self.debugInfo = False # True if log level is DEBUG or VERBOSE # Config Error Handling diff --git a/nw/core/project.py b/nw/core/project.py index 5efce6ad..f7c18bdd 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -382,6 +382,7 @@ class NWProject(): # ========================== if not self.ensureFolderStructure(): + self.clearProject() return False self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) @@ -475,6 +476,7 @@ class NWProject(): "Project file does not appear to be a novelWriterXML file.", nwAlert.ERROR ) + self.clearProject() return False # Check Project Storage Version @@ -489,7 +491,7 @@ class NWProject(): # parser will lose the autoReplace settings if allowed to # read the file. Introduced in version 0.10. - if fileVersion == "1.0" and self.mainConf.blockGUI: + if fileVersion == "1.0" and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( "The project file and data is created by a novelWriter version " @@ -499,9 +501,10 @@ class NWProject(): "any more, so make sure you have a recent backup." )) if msgRes != QMessageBox.Yes: + self.clearProject() return False - elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.blockGUI: + elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI: self.makeAlert(( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " @@ -509,12 +512,13 @@ class NWProject(): ).format( vers = appVersion, ), nwAlert.ERROR) + self.clearProject() return False # Check novelWriter Version # ========================= - if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.blockGUI: + if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( "This project was saved by a newer version of novelWriter, version %s. " @@ -525,6 +529,7 @@ class NWProject(): appVersion, nw.__version__ )) if msgRes != QMessageBox.Yes: + self.clearProject() return False # Start Parsing the XML @@ -927,7 +932,7 @@ class NWProject(): return False if path.isdir(projPath): - if self.mainConf.blockGUI and listdir(self.projPath): + if self.mainConf.showGUI and listdir(self.projPath): self.theParent.makeAlert(( "New project folder is not empty. " "Each project requires a dedicated project folder." diff --git a/nw/error.py b/nw/error.py index fee8fab6..a8f3c149 100644 --- a/nw/error.py +++ b/nw/error.py @@ -160,7 +160,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False): errMsg = NWErrorMessage(nwGUI) errMsg.setMessage(exType, exValue, exTrace) - if nw.CONFIG.blockGUI: + if nw.CONFIG.showGUI: errMsg.exec_() try: diff --git a/nw/gui/build.py b/nw/gui/build.py index 52b32ddb..f2c15221 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -634,7 +634,7 @@ class GuiBuildNovel(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -746,7 +746,7 @@ class GuiBuildNovel(QDialog): errMsg = "Unknown format" # Report to user - if self.mainConf.blockGUI: + if self.mainConf.showGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 766d84f5..786ac8fa 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -163,7 +163,7 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Split Document", ( diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 5dbbede3..6e8e8cf7 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -192,7 +192,7 @@ class GuiMainMenu(QMenuBar): self.aOpenProject = QAction("Open Project", self) self.aOpenProject.setStatusTip("Open project") self.aOpenProject.setShortcut("Ctrl+Shift+O") - self.aOpenProject.triggered.connect(lambda: self.theParent.manageProjects()) + self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog()) self.projMenu.addAction(self.aOpenProject) # Project > Save Project @@ -213,7 +213,7 @@ class GuiMainMenu(QMenuBar): self.aProjectSettings = QAction("Project Settings", self) self.aProjectSettings.setStatusTip("Project settings") self.aProjectSettings.setShortcut("Ctrl+Shift+,") - self.aProjectSettings.triggered.connect(lambda: self.theParent.editProjectDialog()) + self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) self.projMenu.addAction(self.aProjectSettings) # Project > Separator @@ -829,7 +829,7 @@ class GuiMainMenu(QMenuBar): self.aBuildProject = QAction("Build Novel Project", self) self.aBuildProject.setStatusTip("Launch the Build novel project tool") self.aBuildProject.setShortcut("F5") - self.aBuildProject.triggered.connect(lambda: self.theParent.buildProjectDialog()) + self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) self.toolsMenu.addAction(self.aBuildProject) # Tools > Writing Stats @@ -843,7 +843,7 @@ class GuiMainMenu(QMenuBar): self.aPreferences = QAction("Preferences", self) self.aPreferences.setStatusTip("Preferences") self.aPreferences.setShortcut("Ctrl+,") - self.aPreferences.triggered.connect(lambda: self.theParent.editConfigDialog()) + self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) self.toolsMenu.addAction(self.aPreferences) return diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 121d2fd1..5cbfbc72 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -104,7 +104,7 @@ class GuiPreferences(PagedDialog): validEntries &= retA needsRestart |= retB - if needsRestart and self.mainConf.blockGUI: + if needsRestart: msgBox = QMessageBox() msgBox.information( self, "Preferences", @@ -120,7 +120,7 @@ class GuiPreferences(PagedDialog): """Close the preferences without saving the changes. """ logger.verbose("ConfigEditor close button clicked") - self.close() + self.reject() return # END Class GuiPreferences diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 54e40a11..804a4c2e 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -178,7 +178,7 @@ class GuiProjectLoad(QDialog): """Browse for a folder path. """ logger.verbose("GuiProjectLoad browse button clicked") - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog projFile, _ = QFileDialog.getOpenFileName( @@ -219,7 +219,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: doRemove = False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Remove Entry", diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index f5afa35e..998ec83c 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -124,13 +124,25 @@ class GuiProjectSettings(PagedDialog): newList = self.tabReplace.getNewList() self.theProject.setAutoReplace(newList) - self._doClose() + self._saveGuiSettings() + self.accept() return def _doClose(self): """Save settings and close the dialog. """ + self._saveGuiSettings() + self.reject() + return + + ## + # Internal Functions + ## + + def _saveGuiSettings(self): + """Save GUI settings. + """ winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0)) @@ -139,8 +151,6 @@ class GuiProjectSettings(PagedDialog): self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) - self.close() - return # END Class GuiProjectSettings diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index f4280f30..b5f8443b 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -261,7 +261,7 @@ class GuiProjectTree(QTreeWidget): """Move an item up or down in the tree, but only if the treeView has focus. This also applies when the menu is used. """ - hasFocus = qApp.focusWidget() == self or not self.mainConf.blockGUI + hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI if hasFocus and self.theParent.hasProject: tHandle = self.getSelectedHandle() @@ -362,7 +362,7 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) return False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( @@ -416,7 +416,7 @@ class GuiProjectTree(QTreeWidget): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False - if self.mainConf.blockGUI and not alreadyAsked: + if self.mainConf.showGUI and not alreadyAsked: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName @@ -445,7 +445,7 @@ class GuiProjectTree(QTreeWidget): # The file is not already in the trash folder, so we # move it there. doTrash = False - if self.mainConf.blockGUI and askForTrash: + if self.mainConf.showGUI and askForTrash: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index c356f7f4..d4a4e5d6 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -326,7 +326,7 @@ class GuiWritingStats(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -380,7 +380,7 @@ class GuiWritingStats(QDialog): errMsg = str(e) # Report to user - if self.mainConf.blockGUI: + if self.mainConf.showGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/guimain.py b/nw/guimain.py index 06828f2e..483f095d 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -195,8 +195,8 @@ class GuiMain(QMainWindow): self.setStatus = self.statusBar.setStatus self.setProjectStatus = self.statusBar.setProjectStatus - if self.mainConf.blockGUI: - self.show() + # Force a show of the GUI + self.show() # Check that config loaded fine self.reportConfErr() @@ -218,7 +218,8 @@ class GuiMain(QMainWindow): logger.debug("Opening project from additional command line option") self.openProject(self.mainConf.cmdOpen) else: - self.manageProjects() + if self.mainConf.showGUI: + self.showProjectLoadDialog() logger.debug("novelWriter is ready ...") self.statusBar.setStatus("novelWriter is ready ...") @@ -245,24 +246,6 @@ class GuiMain(QMainWindow): # Project Actions ## - def manageProjects(self): - """Opens the projects dialog for selecting either existing - projects from a cache of recently opened projects, or provide a - browse button for projects not yet cached. - """ - if not self.mainConf.blockGUI: - return False - - dlgProj = GuiProjectLoad(self) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: - if dlgProj.openState == GuiProjectLoad.OPEN_STATE: - self.openProject(dlgProj.openPath) - elif dlgProj.openState == GuiProjectLoad.NEW_STATE: - self.newProject() - - return True - def newProject(self, projData=None, forceNew=False): """Create new project with a few default files and folders. The variable forceNew is used for testing. @@ -275,8 +258,8 @@ class GuiMain(QMainWindow): ) return False - if projData is None and self.mainConf.blockGUI: - projData = self.newProjectDialog() + if projData is None and self.mainConf.showGUI: + projData = self.showNewProjectDialog() if projData is None: return False @@ -316,7 +299,7 @@ class GuiMain(QMainWindow): # There is no project loaded, everything OK return True - if self.mainConf.blockGUI and not isYes: + if self.mainConf.showGUI and not isYes: msgBox = QMessageBox() msgRes = msgBox.question( self, "Close Project", "Save changes and close current project?" @@ -332,7 +315,7 @@ class GuiMain(QMainWindow): doBackup = False if self.theProject.doBackup and self.mainConf.backupOnClose: doBackup = True - if self.mainConf.blockGUI and self.mainConf.askBeforeBackup: + if self.mainConf.showGUI and self.mainConf.askBeforeBackup: msgBox = QMessageBox() msgRes = msgBox.question( self, "Backup Project", "Backup current project?" @@ -379,7 +362,7 @@ class GuiMain(QMainWindow): # reason handled by the project class. return False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: try: lockDetails = ( "

The project was locked by the computer " @@ -449,7 +432,7 @@ class GuiMain(QMainWindow): # If the project is new, it may not have a path, so we need one if self.theProject.projPath is None: - projPath = self.saveProjectDialog() + projPath = self.selectProjectPath() self.theProject.setProjectPath(projPath) if self.theProject.projPath is None: return False @@ -611,7 +594,7 @@ class GuiMain(QMainWindow): return False if not self.docEditor.isEmpty(): - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self, "Import Document", ( "Importing the file will overwrite the current content of the document. " @@ -629,7 +612,7 @@ class GuiMain(QMainWindow): def mergeDocuments(self): """Merge multiple documents to one single new document. """ - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgMerge = GuiDocMerge(self, self.theProject) dlgMerge.exec_() return True @@ -637,7 +620,7 @@ class GuiMain(QMainWindow): def splitDocument(self): """Split a single document into multiple documents. """ - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgSplit = GuiDocSplit(self, self.theProject) dlgSplit.exec_() return True @@ -684,7 +667,7 @@ class GuiMain(QMainWindow): return logger.verbose("Requesting change to item %s" % tHandle) - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgProj = GuiItemEditor(self, self.theProject, tHandle) dlgProj.exec_() if dlgProj.result() == QDialog.Accepted: @@ -746,7 +729,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() - if self.mainConf.blockGUI and not beQuiet: + if self.mainConf.showGUI and not beQuiet: self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO) return True @@ -763,7 +746,7 @@ class GuiMain(QMainWindow): # Main Dialogs ## - def saveProjectDialog(self): + def selectProjectPath(self): """Select where to save project. """ dlgOpt = QFileDialog.Options() @@ -776,7 +759,22 @@ class GuiMain(QMainWindow): return projPath return None - def newProjectDialog(self): + def showProjectLoadDialog(self): + """Opens the projects dialog for selecting either existing + projects from a cache of recently opened projects, or provide a + browse button for projects not yet cached. + """ + dlgProj = GuiProjectLoad(self) + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: + if dlgProj.openState == GuiProjectLoad.OPEN_STATE: + self.openProject(dlgProj.openPath) + elif dlgProj.openState == GuiProjectLoad.NEW_STATE: + self.newProject() + + return True + + def showNewProjectDialog(self): """Open the wizard and assemble the project options dict. """ newProj = GuiProjectWizard(self) @@ -787,62 +785,76 @@ class GuiMain(QMainWindow): return None - def editConfigDialog(self): + def showPreferencesDialog(self): """Open the preferences dialog. """ dlgConf = GuiPreferences(self, self.theProject) - if dlgConf.exec_() == QDialog.Accepted: + dlgConf.exec_() + + if dlgConf.result() == QDialog.Accepted: logger.debug("Applying new preferences") self.initMain() self.theTheme.updateTheme() self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - return True - def editProjectDialog(self): + return + + def showProjectSettingsDialog(self): """Open the project settings dialog. """ - if self.hasProject: - dlgProj = GuiProjectSettings(self, self.theProject) - dlgProj.exec_() + if not self.hasProject: + logger.error("No project open") + return + + dlgProj = GuiProjectSettings(self, self.theProject) + dlgProj.exec_() + + if dlgProj.result() == QDialog.Accepted: + logger.debug("Applying new project settings") self.docEditor.setDictionaries() self._setWindowTitle(self.theProject.projName) - return True - def buildProjectDialog(self): + return + + def showBuildProjectDialog(self): """Open the build project dialog. """ - if self.hasProject: - dlgBuild = GuiBuildNovel(self, self.theProject) - dlgBuild.setModal(False) - dlgBuild.show() - return True + if not self.hasProject: + logger.error("No project open") + return + + dlgBuild = GuiBuildNovel(self, self.theProject) + dlgBuild.setModal(False) + dlgBuild.show() + return def showWritingStatsDialog(self): """Open the session log dialog. """ - if self.hasProject: - dlgStats = GuiWritingStats(self, self.theProject) - dlgStats.setModal(False) - dlgStats.show() - return True + if not self.hasProject: + logger.error("No project open") + return + + dlgStats = GuiWritingStats(self, self.theProject) + dlgStats.setModal(False) + dlgStats.show() + return def showAboutNWDialog(self): """Show the about dialog for novelWriter. """ - if self.mainConf.blockGUI: - dlgAbout = GuiAbout(self) - dlgAbout.exec_() - return True + dlgAbout = GuiAbout(self) + dlgAbout.exec_() + return def showAboutQtDialog(self): """Show the about dialog for Qt. """ - if self.mainConf.blockGUI: - msgBox = QMessageBox() - msgBox.aboutQt(self, "About Qt") - return True + msgBox = QMessageBox() + msgBox.aboutQt(self, "About Qt") + return def makeAlert(self, theMessage, theLevel=nwAlert.INFO): """Alert both the user and the logger at the same time. Message @@ -871,7 +883,7 @@ class GuiMain(QMainWindow): logger.error(msgLine) # Popup - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() if theLevel == nwAlert.INFO: msgBox.information(self, "Information", popMsg) @@ -902,7 +914,7 @@ class GuiMain(QMainWindow): def closeMain(self): """Save everything, and close novelWriter. """ - if self.mainConf.blockGUI and self.hasProject: + if self.mainConf.showGUI and self.hasProject: msgBox = QMessageBox() msgRes = msgBox.question( self, "Exit", "Do you want to save changes and exit?" diff --git a/tests/conftest.py b/tests/conftest.py index 14988efa..a8945155 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,12 +9,22 @@ import shutil from os import path, mkdir from nwdummy import DummyMain +from PyQt5.QtWidgets import QFileDialog, QMessageBox + sys.path.insert(1, path.abspath(path.join(path.dirname(__file__), path.pardir))) from nw.config import Config # noqa: E402 +## +# Core Test Folders +## + @pytest.fixture(scope="session") def nwTemp(): + """A temporary folder for the test session. This folder is + presistent after the test so that the status of generated files can + be checked. The folder is instead cleared before a new test session. + """ testDir = path.dirname(__file__) tempDir = path.join(testDir, "temp") if path.isdir(tempDir): @@ -25,31 +35,51 @@ def nwTemp(): @pytest.fixture(scope="session") def nwRef(): + """The folder where all the reference files are stored for verifying + the results of tests. + """ testDir = path.dirname(__file__) refDir = path.join(testDir, "reference") return refDir -@pytest.fixture(scope="session") -def nwConf(nwRef, nwTemp): - theConf = Config() - theConf.initConfig(nwRef, nwTemp) - return theConf +## +# novelWriter Objects +## @pytest.fixture(scope="session") def tmpConf(nwTemp): + """Create a temporary novelWriter configuration object. + """ theConf = Config() theConf.initConfig(nwTemp, nwTemp) theConf.setLastPath("") return theConf +@pytest.fixture(scope="session") +def nwConf(nwRef, nwTemp): + """Temporary novelWriter configuration used for the dummy instance + of novelWriter's main GUI. + """ + theConf = Config() + theConf.initConfig(nwRef, nwTemp) + return theConf + @pytest.fixture(scope="session") def nwDummy(nwRef, nwTemp, nwConf): + """Create a dummy instance of novelWriter's main GUI class. + """ theDummy = DummyMain() theDummy.mainConf = nwConf return theDummy +## +# Temporary Test Folders +## + @pytest.fixture(scope="session") def nwTempProj(nwTemp): + """A temporary folder for project tests. + """ projDir = path.join(nwTemp, "proj") if not path.isdir(projDir): mkdir(projDir) @@ -57,6 +87,8 @@ def nwTempProj(nwTemp): @pytest.fixture(scope="session") def nwTempGUI(nwTemp): + """A temporary folder for GUI tests. + """ guiDir = path.join(nwTemp, "gui") if not path.isdir(guiDir): mkdir(guiDir) @@ -64,6 +96,8 @@ def nwTempGUI(nwTemp): @pytest.fixture(scope="session") def nwTempBuild(nwTemp): + """A temporary folder for build tests. + """ buildDir = path.join(nwTemp, "build") if not path.isdir(buildDir): mkdir(buildDir) @@ -71,6 +105,8 @@ def nwTempBuild(nwTemp): @pytest.fixture(scope="function") def nwFuncTemp(nwTemp): + """A temporary folder for a single test function. + """ funcDir = path.join(nwTemp, "ftemp") if path.isdir(funcDir): shutil.rmtree(funcDir) @@ -81,8 +117,14 @@ def nwFuncTemp(nwTemp): shutil.rmtree(funcDir) return +## +# Temp Folders for Projects +## + @pytest.fixture(scope="function") def nwMinimal(nwTemp): + """A minimal novelWriter example project. + """ testDir = path.dirname(__file__) minimalStore = path.join(testDir, "minimal") minimalDir = path.join(nwTemp, "minimal") @@ -102,6 +144,9 @@ def nwMinimal(nwTemp): @pytest.fixture(scope="function") def nwLipsum(nwTemp): + """A medium sized novelWriter example project with a lot of Lorem + Ipsum dummy text. + """ testDir = path.dirname(__file__) lipsumStore = path.join(testDir, "lipsum") lipsumDir = path.join(nwTemp, "lipsum") @@ -121,6 +166,8 @@ def nwLipsum(nwTemp): @pytest.fixture(scope="function") def nwOldProj(nwTemp): + """A minimal movelWriter project using the old folder structure. + """ testDir = path.dirname(__file__) oldProjStore = path.join(testDir, "oldproj") oldProjDir = path.join(nwTemp, "oldproj") @@ -131,3 +178,30 @@ def nwOldProj(nwTemp): if path.isdir(oldProjDir): shutil.rmtree(oldProjDir) return + +## +# Monkey Patch Dialogs +## + +@pytest.fixture(scope="function") +def mnkQtDialogs(monkeypatch, nwTemp): + """Mock Qt dialog functions to prevent GUI blocking while testing. + """ + monkeypatch.setattr( + QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp + ) + + monkeypatch.setattr( + QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes + ) + monkeypatch.setattr( + QMessageBox, "information", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + QMessageBox, "warning", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + QMessageBox, "critical", lambda *args, **kwargs: None + ) + + return diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 1fceaca1..13d41afb 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -7,13 +7,14 @@ import pytest import json from shutil import copyfile -from nwtools import cmpFiles +from nwtools import cmpFiles, getGuiItem from os import path from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( - QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog + QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction, + QMessageBox ) from nw.gui import ( @@ -28,22 +29,34 @@ keyDelay = 2 stepDelay = 20 @pytest.mark.gui -def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testProjectSettings(qtbot, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - # Create new, save, open project + # Check that we cannot open when there is no project + nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) + assert getGuiItem("GuiProjectSettings") is None + + # Create new project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}, True) nwGUI.mainConf.backupPath = nwFuncTemp - projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject) + # Get the dialog object + monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None) + monkeypatch.setattr(GuiProjectSettings, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000) + + projEdit = getGuiItem("GuiProjectSettings") + assert isinstance(projEdit, GuiProjectSettings) projEdit.show() qtbot.addWidget(projEdit) + # Main settings qtbot.wait(stepDelay) projEdit.tabMain.editName.setText("") for c in "Project Name": @@ -199,6 +212,10 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): assert nwGUI.closeProject() qtbot.wait(stepDelay) + # Check that we cannot open when there is no project + nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) + assert getGuiItem("GuiWritingStats") is None + assert nwGUI.openProject(nwFuncTemp) qtbot.wait(stepDelay) @@ -239,8 +256,11 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): qtbot.wait(stepDelay) nwGUI.mainConf.lastPath = nwFuncTemp - sessLog = GuiWritingStats(nwGUI, nwGUI.theProject) - sessLog.show() + nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000) + + sessLog = getGuiItem("GuiWritingStats") + assert isinstance(sessLog, GuiWritingStats) qtbot.wait(stepDelay) assert sessLog._saveData(sessLog.FMT_CSV) @@ -337,17 +357,29 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testAboutBox(qtbot, nwFuncTemp, nwTemp): +def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - msgAbout = GuiAbout(nwGUI) + # NW About + monkeypatch.setattr(GuiAbout, "exec_", lambda *args: None) + nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) + + msgAbout = getGuiItem("GuiAbout") + assert isinstance(msgAbout, GuiAbout) + msgAbout.show() + assert msgAbout.pageAbout.document().characterCount() > 100 assert msgAbout.pageLicense.document().characterCount() > 100 + # Qt About + monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None) + nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger) + # qtbot.stopForInteraction() msgAbout._doClose() nwGUI.closeMain() @@ -361,11 +393,20 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - assert nwGUI.openProject(nwLipsum) + # Check that we cannot open when there is no project + nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) + assert getGuiItem("GuiBuildNovel") is None + # Open a project + assert nwGUI.openProject(nwLipsum) nwGUI.mainConf.lastPath = nwLipsum - nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject) + # Open the tool + nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiBuildNovel") is not None, timeout=1000) + + nwBuild = getGuiItem("GuiBuildNovel") + assert isinstance(nwBuild, GuiBuildNovel) # Default Settings qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) @@ -764,7 +805,7 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testLoadProject(qtbot, nwMinimal, nwTemp): +def testLoadProject(qtbot, monkeypatch, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -774,7 +815,13 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): assert nwGUI.openProject(nwMinimal) assert nwGUI.closeProject() - nwLoad = GuiProjectLoad(nwGUI) + monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *args: None) + monkeypatch.setattr(GuiProjectLoad, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aOpenProject.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectLoad") is not None, timeout=1000) + + nwLoad = getGuiItem("GuiProjectLoad") + assert isinstance(nwLoad, GuiProjectLoad) nwLoad.show() recentCount = nwLoad.listBox.topLevelItemCount() @@ -793,6 +840,7 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): assert nwLoad.openPath == selPath assert nwLoad.openState == nwLoad.OPEN_STATE + # Just create a new project load from scratch for the rest of the test del nwLoad nwLoad = GuiProjectLoad(nwGUI) nwLoad.show() @@ -815,7 +863,7 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): +def testPreferences(qtbot, monkeypatch, mnkQtDialogs, nwMinimal, nwTemp, nwRef, tmpConf): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -823,11 +871,17 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): qtbot.wait(stepDelay) assert nwGUI.openProject(nwMinimal) - nwPrefs = GuiPreferences(nwGUI, nwGUI.theProject) + + monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None) + monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) + + nwPrefs = getGuiItem("GuiPreferences") + assert isinstance(nwPrefs, GuiPreferences) nwPrefs.show() # Override Config - tmpConf.blockGUI = False tmpConf.confPath = nwMinimal nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf @@ -991,3 +1045,17 @@ def testQuotesDialog(qtbot, nwMinimal, nwTemp): nwQuot.close() nwGUI.closeMain() nwGUI.close() + +@pytest.mark.gui +def testDialogsOpenClose(qtbot, mnkQtDialogs, nwMinimal, nwTemp): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + assert nwGUI.selectProjectPath() == nwTemp + + # qtbot.stopForInteraction() + nwGUI.closeMain() + nwGUI.close() diff --git a/tests/test_gui.py b/tests/test_gui.py index ec24d5ec..8cf3a4d4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -223,6 +223,10 @@ def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + # Trigger autosaves before making more changes + nwGUI._autoSaveDocument() + nwGUI._autoSaveProject() + # Select the 'New Scene' file nwGUI.setFocus(1) nwGUI.treeView.clearSelection() diff --git a/tests/test_project.py b/tests/test_project.py index 1ec42348..7e46b8e6 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -580,9 +580,9 @@ def testOrphanedFiles(nwDummy, nwLipsum): assert theProject.closeProject() @pytest.mark.project -def testOldProject(nwDummy, nwOldProj): +def testOldProject(nwDummy, nwOldProj, mnkQtDialogs): theProject = NWProject(nwDummy) - theProject.mainConf.blockGUI = False + theProject.mainConf.showGUI = False # Create dummy files for known legacy files deleteFiles = [