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. """