From 1d5c8566d105b4ed16ce72fbf630443f7f77f3c6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 15:11:17 +0200
Subject: [PATCH 1/4] Check the sanity of paths during project wizard run
(issue #1058)
---
novelwriter/tools/projwizard.py | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py
index 8f746ca6..e4cc6862 100644
--- a/novelwriter/tools/projwizard.py
+++ b/novelwriter/tools/projwizard.py
@@ -185,6 +185,9 @@ class ProjWizardFolderPage(QWizardPage):
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.clicked.connect(self._doBrowse)
+ self.errLabel = QLabel("")
+ self.errLabel.setWordWrap(True)
+
self.mainForm = QHBoxLayout()
self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0)
self.mainForm.addWidget(self.projPath, 1)
@@ -198,11 +201,36 @@ class ProjWizardFolderPage(QWizardPage):
self.outerBox.setSpacing(vS)
self.outerBox.addWidget(self.theText)
self.outerBox.addLayout(self.mainForm)
+ self.outerBox.addWidget(self.errLabel)
self.outerBox.addStretch(1)
self.setLayout(self.outerBox)
return
+ def isComplete(self):
+ """Check that the selected path isn't already being used.
+ """
+ self.errLabel.setText("")
+ if not QWizardPage.isComplete(self):
+ return False
+
+ setPath = os.path.abspath(os.path.expanduser(self.projPath.text()))
+ parPath = os.path.dirname(setPath)
+ logger.verbose("Path is: %s", setPath)
+ if parPath and not os.path.isdir(parPath):
+ self.errLabel.setText(self.tr(
+ "Error: A project folder cannot be created using this path."
+ ))
+ return False
+
+ if os.path.exists(setPath):
+ self.errLabel.setText(self.tr(
+ "Error: The selected path already exists."
+ ))
+ return False
+
+ return True
+
##
# Slots
##
From c91f19f9f707f367481b0aafd09954a04dd376cb Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 16:31:51 +0200
Subject: [PATCH 2/4] Update wizard test coverage
---
tests/test_tools/test_tools_projwizard.py | 328 ++++++++++++----------
1 file changed, 183 insertions(+), 145 deletions(-)
diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py
index 53f1cc79..f82e45a6 100644
--- a/tests/test_tools/test_tools_projwizard.py
+++ b/tests/test_tools/test_tools_projwizard.py
@@ -21,12 +21,11 @@ along with this program. If not, see .
import pytest
import os
-import sys
from tools import getGuiItem
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox
+from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox, QDialog
from novelwriter.enum import nwItemClass
from novelwriter.tools.projwizard import (
@@ -40,9 +39,9 @@ stepDelay = 20
@pytest.mark.gui
-@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
-def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
- """Test the new project wizard.
+# @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
+def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal):
+ """Test the launch of the project wizard.
Disabled for macOS because the test segfaults on QWizard.show()
"""
# Block message box
@@ -55,173 +54,212 @@ def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
# New with a project open should cause an error
assert nwGUI.openProject(nwMinimal)
- assert not nwGUI.newProject()
+ with monkeypatch.context() as mp:
+ mp.setattr(nwGUI, "closeProject", lambda *a: False)
+ assert nwGUI.newProject() is False
# Close project, but call with invalid path
assert nwGUI.closeProject()
with monkeypatch.context() as mp:
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: None)
- assert not nwGUI.newProject()
+ assert nwGUI.newProject() is False
# Now, with an empty dictionary
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {})
- assert not nwGUI.newProject()
+ assert nwGUI.newProject() is False
# Now, with a non-empty folder
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": nwMinimal})
- assert not nwGUI.newProject()
+ assert nwGUI.newProject() is False
##
- # Test the Wizard
+ # Test the Wizard Launching
##
- monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
nwGUI.mainConf.lastPath = " "
+ monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
- nwGUI.closeProject()
- nwGUI.showNewProjectDialog()
+ result = nwGUI.showNewProjectDialog()
qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000)
nwWiz = getGuiItem("GuiProjectWizard")
assert isinstance(nwWiz, GuiProjectWizard)
nwWiz.show()
+
qtbot.wait(stepDelay)
+ qtbot.mouseClick(nwWiz.button(QWizard.CancelButton), Qt.LeftButton)
+ assert result is None
- for wStep in range(4):
- # This does not actually create the project, it just generates the
- # dictionary that defines it.
-
- # Intro Page
- introPage = nwWiz.currentPage()
- assert isinstance(introPage, ProjWizardIntroPage)
- assert not nwWiz.button(QWizard.NextButton).isEnabled()
-
- qtbot.wait(stepDelay)
- for c in ("Test Minimal %d" % wStep):
- qtbot.keyClick(introPage.projName, c, delay=typeDelay)
-
- qtbot.wait(stepDelay)
- for c in "Minimal Novel":
- qtbot.keyClick(introPage.projTitle, c, delay=typeDelay)
-
- qtbot.wait(stepDelay)
- for c in "Jane Doe":
- qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay)
-
- # Setting projName should activate the button
- assert nwWiz.button(QWizard.NextButton).isEnabled()
-
- qtbot.wait(stepDelay)
- qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
-
- # Folder Page
- storagePage = nwWiz.currentPage()
- assert isinstance(storagePage, ProjWizardFolderPage)
- assert not nwWiz.button(QWizard.NextButton).isEnabled()
-
- if wStep == 0:
- # Check invalid path first, the first time we reach here
- monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "")
- qtbot.wait(stepDelay)
- qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
- assert storagePage.projPath.text() == ""
-
- # Then, we always return nwMinimal as path
- monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal)
-
- qtbot.wait(stepDelay)
- qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
- projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep)
- assert storagePage.projPath.text() == projPath
-
- # Setting projPath should activate the button
- assert nwWiz.button(QWizard.NextButton).isEnabled()
-
- qtbot.wait(stepDelay)
- qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
-
- # Populate Page
- popPage = nwWiz.currentPage()
- assert isinstance(popPage, ProjWizardPopulatePage)
- assert nwWiz.button(QWizard.NextButton).isEnabled()
-
- qtbot.wait(stepDelay)
- if wStep == 0:
- popPage.popMinimal.setChecked(True)
- elif wStep == 1:
- popPage.popCustom.setChecked(True)
- elif wStep == 2:
- popPage.popCustom.setChecked(True)
- elif wStep == 3:
- popPage.popSample.setChecked(True)
-
- qtbot.wait(stepDelay)
- qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
-
- # Custom Page
- if wStep == 1 or wStep == 2:
- customPage = nwWiz.currentPage()
- assert isinstance(customPage, ProjWizardCustomPage)
- assert nwWiz.button(QWizard.NextButton).isEnabled()
-
- customPage.addPlot.setChecked(True)
- customPage.addChar.setChecked(True)
- customPage.addWorld.setChecked(True)
- customPage.addTime.setChecked(True)
- customPage.addObject.setChecked(True)
- customPage.addEntity.setChecked(True)
-
- if wStep == 2:
- customPage.numChapters.setValue(0)
- customPage.numScenes.setValue(10)
- customPage.chFolders.setChecked(False)
-
- qtbot.wait(stepDelay)
- qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
-
- # Final Page
- finalPage = nwWiz.currentPage()
- assert isinstance(finalPage, ProjWizardFinalPage)
- assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it
-
- # Check Data
- projData = nwGUI._assembleProjectWizardData(nwWiz)
- assert projData["projName"] == "Test Minimal %d" % wStep
- assert projData["projTitle"] == "Minimal Novel"
- assert projData["projAuthors"] == "Jane Doe"
- assert projData["projPath"] == projPath
- assert projData["popMinimal"] == (wStep == 0)
- assert projData["popCustom"] == (wStep == 1 or wStep == 2)
- assert projData["popSample"] == (wStep == 3)
- if wStep == 1 or wStep == 2:
- assert projData["addRoots"] == [
- nwItemClass.PLOT,
- nwItemClass.CHARACTER,
- nwItemClass.WORLD,
- nwItemClass.TIMELINE,
- nwItemClass.OBJECT,
- nwItemClass.ENTITY,
- ]
- if wStep == 1:
- assert projData["numChapters"] == 5
- assert projData["numScenes"] == 5
- assert projData["chFolders"]
- else:
- assert projData["numChapters"] == 0
- assert projData["numScenes"] == 10
- assert not projData["chFolders"]
- else:
- assert projData["addRoots"] == []
- assert projData["numChapters"] == 0
- assert projData["numScenes"] == 0
- assert not projData["chFolders"]
-
- # Restart the wizard for next iteration
- nwWiz.restart()
+ with monkeypatch.context() as mp:
+ mp.setattr(GuiProjectWizard, "result", lambda *a: QDialog.Accepted)
+ result = nwGUI.showNewProjectDialog()
+ nwWiz.button(QWizard.CancelButton).click()
+ assert isinstance(result, dict)
nwWiz.reject()
nwWiz.close()
# qtbot.stopForInteraction()
-# END Test testToolProjectWizard_Main
+# END Test testToolProjectWizard_Handling
+
+
+@pytest.mark.gui
+@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"])
+def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
+ """Test the new project wizard with a set of selection scenarios.
+ """
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
+
+ nwGUI.mainConf.lastPath = " "
+ nwWiz = GuiProjectWizard(nwGUI)
+ nwWiz.show()
+ qtbot.wait(stepDelay)
+
+ # Intro Page
+ # ==========
+
+ introPage = nwWiz.currentPage()
+ assert isinstance(introPage, ProjWizardIntroPage)
+ assert not nwWiz.button(QWizard.NextButton).isEnabled()
+
+ introPage.projName.setText("Test Wizard")
+ introPage.projTitle.setText("My Novel")
+ introPage.projAuthors.setPlainText("Jane Doe")
+
+ # Setting projName should activate the button
+ assert nwWiz.button(QWizard.NextButton).isEnabled()
+
+ qtbot.wait(stepDelay)
+ qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
+
+ # Folder Page
+ # ===========
+
+ storagePage = nwWiz.currentPage()
+ assert isinstance(storagePage, ProjWizardFolderPage)
+ assert not nwWiz.button(QWizard.NextButton).isEnabled()
+ assert storagePage.errLabel.text() == ""
+
+ # Set an invalid path
+ storagePage.projPath.setText(os.path.join(fncDir, "not", "a", "path"))
+ assert not nwWiz.button(QWizard.NextButton).isEnabled()
+ assert storagePage.errLabel.text().startswith("Error")
+
+ # Set an existing path
+ storagePage.projPath.setText(fncDir)
+ assert not nwWiz.button(QWizard.NextButton).isEnabled()
+ assert storagePage.errLabel.text().startswith("Error")
+
+ # Return a non-result from browse
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "")
+ qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
+ assert storagePage.errLabel.text() == ""
+
+ # Let the browse feature handle it
+ projPath = os.path.join(fncDir, "Test Wizard")
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncDir)
+ qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
+
+ assert storagePage.projPath.text() == projPath
+ assert storagePage.errLabel.text() == ""
+
+ # Setting projPath should activate the button
+ assert nwWiz.button(QWizard.NextButton).isEnabled()
+
+ qtbot.wait(stepDelay)
+ qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
+
+ # Populate Page
+ # =============
+
+ popPage = nwWiz.currentPage()
+ assert isinstance(popPage, ProjWizardPopulatePage)
+ assert nwWiz.button(QWizard.NextButton).isEnabled()
+
+ qtbot.wait(stepDelay)
+ if prjType.startswith("minimal"):
+ popPage.popMinimal.setChecked(True)
+ elif prjType.startswith("custom"):
+ popPage.popCustom.setChecked(True)
+ elif prjType.startswith("sample"):
+ popPage.popSample.setChecked(True)
+
+ qtbot.wait(stepDelay)
+ qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
+
+ # Custom Page
+ # ===========
+ if prjType.startswith("custom"):
+
+ customPage = nwWiz.currentPage()
+ assert isinstance(customPage, ProjWizardCustomPage)
+ assert nwWiz.button(QWizard.NextButton).isEnabled()
+
+ customPage.addPlot.setChecked(True)
+ customPage.addChar.setChecked(True)
+ customPage.addWorld.setChecked(True)
+ customPage.addTime.setChecked(True)
+ customPage.addObject.setChecked(True)
+ customPage.addEntity.setChecked(True)
+
+ if prjType == "custom2":
+ customPage.numChapters.setValue(0)
+ customPage.numScenes.setValue(10)
+ customPage.chFolders.setChecked(False)
+
+ qtbot.wait(stepDelay)
+ qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
+
+ # Final Page
+ # ==========
+
+ finalPage = nwWiz.currentPage()
+ assert isinstance(finalPage, ProjWizardFinalPage)
+ assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it
+
+ # Check Data
+ # ==========
+
+ projData = nwGUI._assembleProjectWizardData(nwWiz)
+ assert projData["projName"] == "Test Wizard"
+ assert projData["projTitle"] == "My Novel"
+ assert projData["projAuthors"] == "Jane Doe"
+ assert projData["projPath"] == projPath
+ assert projData["popMinimal"] == prjType.startswith("minimal")
+ assert projData["popCustom"] == prjType.startswith("custom")
+ assert projData["popSample"] == prjType.startswith("sample")
+ if prjType.startswith("custom"):
+ assert projData["addRoots"] == [
+ nwItemClass.PLOT,
+ nwItemClass.CHARACTER,
+ nwItemClass.WORLD,
+ nwItemClass.TIMELINE,
+ nwItemClass.OBJECT,
+ nwItemClass.ENTITY,
+ ]
+ if prjType == "custom1":
+ assert projData["numChapters"] == 5
+ assert projData["numScenes"] == 5
+ assert projData["chFolders"]
+ else:
+ assert projData["numChapters"] == 0
+ assert projData["numScenes"] == 10
+ assert not projData["chFolders"]
+ else:
+ assert projData["addRoots"] == []
+ assert projData["numChapters"] == 0
+ assert projData["numScenes"] == 0
+ assert not projData["chFolders"]
+
+ # Cleanup
+ nwWiz.reject()
+ nwWiz.close()
+
+ # qtbot.stopForInteraction()
+
+# END Test testToolProjectWizard_Run
From e2c10f52d2ba259690fbd9f1ef2cf0c1cd558138 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 16:37:09 +0200
Subject: [PATCH 3/4] Disabled wizard test on macOS as it still segfaults
---
tests/test_tools/test_tools_projwizard.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py
index f82e45a6..62fae5f0 100644
--- a/tests/test_tools/test_tools_projwizard.py
+++ b/tests/test_tools/test_tools_projwizard.py
@@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import pytest
import os
+import sys
+import pytest
from tools import getGuiItem
@@ -39,7 +40,7 @@ stepDelay = 20
@pytest.mark.gui
-# @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
+@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the launch of the project wizard.
Disabled for macOS because the test segfaults on QWizard.show()
From 7c7e4c2297f7bbf61f7a12a8bbeec055ef16c1d4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 16:39:41 +0200
Subject: [PATCH 4/4] Disabled second wizard test on macOS as it too segfaults
---
tests/test_tools/test_tools_projwizard.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py
index 62fae5f0..ef4c31cd 100644
--- a/tests/test_tools/test_tools_projwizard.py
+++ b/tests/test_tools/test_tools_projwizard.py
@@ -107,6 +107,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal):
@pytest.mark.gui
@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"])
+@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
"""Test the new project wizard with a set of selection scenarios.
"""