diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 8d35c8fc..44235f9e 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -310,6 +310,7 @@ class _OpenProjectPage(QWidget): def _openContextMenu(self, pos: QPoint) -> None: """Open the custom context menu.""" ctxMenu = QMenu(self) + ctxMenu.setObjectName("ContextMenu") # Used for testing action = ctxMenu.addAction(self.tr("Open Project")) action.triggered.connect(self.openSelectedItem) action = ctxMenu.addAction(self.tr("Remove Project")) @@ -399,7 +400,10 @@ class _ProjectListModel(QAbstractListModel): def data(self, index: QModelIndex, role: int = 0) -> tuple[str, str, str]: """Return data for an individual item.""" - return self._data[index.row()] if index.isValid() else ("", "", "") + try: + return self._data[index.row()] if index.isValid() else ("", "", "") + except IndexError: + return "", "", "" def removeEntry(self, index: QModelIndex) -> bool: """Remove an entry in the model.""" @@ -525,8 +529,6 @@ class _NewProjectForm(QWidget): self.projLang.addItem(language, tag) langIdx = self.projLang.findData(CONFIG.guiLocale) - if langIdx == -1: - langIdx = self.projLang.findData("en_GB") if langIdx != -1: self.projLang.setCurrentIndex(langIdx) diff --git a/tests/conftest.py b/tests/conftest.py index be3c243f..4a87c3c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -160,7 +160,6 @@ def nwGUI(qtbot, monkeypatch, functionFixture): nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) qtbot.addWidget(nwGUI) resetConfigVars() - SHARED._alert = None nwGUI.docEditor.initEditor() nwGUI.show() diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index fc85e05a..037f820b 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -278,6 +278,19 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setMainWinSize(1200, 650) + # Welcome Window Size + tstConf.guiScale = 2.0 + tstConf.setWelcomeWinSize(70, 70) + assert tstConf.welcomeWinSize == [70, 70] + assert tstConf._welcomeSize == [35, 35] + + tstConf.guiScale = 1.0 + tstConf.setWelcomeWinSize(70, 70) + assert tstConf.welcomeWinSize == [70, 70] + assert tstConf._welcomeSize == [70, 70] + + tstConf.setWelcomeWinSize(800, 500) + # Preferences Size tstConf.guiScale = 2.0 tstConf.setPreferencesWinSize(70, 70) diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py new file mode 100644 index 00000000..27c326fe --- /dev/null +++ b/tests/test_tools/test_tools_welcome.py @@ -0,0 +1,268 @@ +""" +novelWriter – Welcome Window 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 . +""" +from __future__ import annotations + +import pytest + +from pathlib import Path +from datetime import datetime +from pytestqt.qtbot import QtBot + +from PyQt5.QtCore import QPoint, Qt +from PyQt5.QtWidgets import QAction, QFileDialog, QMenu + +from novelwriter import CONFIG, SHARED +from novelwriter.constants import nwFiles +from novelwriter.enum import nwItemClass +from novelwriter.tools.welcome import GuiWelcome + + +@pytest.mark.gui +def testToolWelcome_Main(qtbot: QtBot, monkeypatch, nwGUI, fncPath): + """Test the main Welcome window.""" + welcome = GuiWelcome(nwGUI) + with qtbot.waitExposed(welcome): + # This ensures the paint event is executed + welcome.show() + + # By default, the project list is shown + assert welcome.mainStack.currentIndex() == 0 + + # Show the new project form + welcome.newButton.click() + assert welcome.mainStack.currentIndex() == 1 + + # Revert to project lits + welcome.tabNew.cancelNewProject.emit() + assert welcome.mainStack.currentIndex() == 0 + + # Open a project + with monkeypatch.context() as mp: + mp.setattr(SHARED, "getProjectPath", lambda *a, **k: fncPath) + with qtbot.waitSignal(welcome.openProjectRequest) as signal: + welcome.browseButton.click() + assert signal.args and signal.args[0] == fncPath + + # qtbot.stop() + welcome.close() + +# END Test testToolWelcome_Main + + +@pytest.mark.gui +def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): + """Test the open tab in the Welcome window.""" + monkeypatch.setattr(QMenu, "exec_", lambda *a: None) + monkeypatch.setattr(QMenu, "deleteLater", lambda *a: None) + + CONFIG.recentProjects.update("/stuff/project_one", "Project One", 12345, 1690000000) + CONFIG.recentProjects.update("/stuff/project_two", "Project Two", 54321, 1700000000) + dateOne = datetime.fromtimestamp(1700000000).strftime("%x") + dateTwo = datetime.fromtimestamp(1690000000).strftime("%x") + + welcome = GuiWelcome(nwGUI) + with qtbot.waitExposed(welcome): + welcome.show() + + assert welcome.mainStack.currentIndex() == 0 + tabOpen = welcome.tabOpen + listModel = tabOpen.listModel + vPort = tabOpen.listWidget.viewport() + + posOne = tabOpen.listWidget.rectForIndex(listModel.createIndex(0, 0)).center() + posTwo = tabOpen.listWidget.rectForIndex(listModel.createIndex(1, 0)).center() + + # Check items, which should be in opposite order + assert listModel.data(listModel.createIndex(0, 0)) == ( + "Project Two", "/stuff/project_two", f"Last Opened: {dateOne}, Word Count: 54.3\u2009k" + ) + assert listModel.data(listModel.createIndex(1, 0)) == ( + "Project One", "/stuff/project_one", f"Last Opened: {dateTwo}, Word Count: 12.3\u2009k" + ) + + # Single click item + assert tabOpen.selectedPath.text() == "Path: /stuff/project_two" + qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) + assert tabOpen.selectedPath.text() == "Path: /stuff/project_one" + + # Double Click item + qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) + with monkeypatch.context() as mp: + mp.setattr(welcome, "close", lambda *a: None) + with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal: + qtbot.mouseDClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) + assert signal.args and signal.args[0] == Path("/stuff/project_one") + + # Context Menu + def getMenuForPos(pos: QPoint) -> QMenu | None: + nonlocal tabOpen + tabOpen._openContextMenu(pos) + for obj in tabOpen.children(): + if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu": + return obj + return None + + qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posOne, delay=10) + ctxMenu = getMenuForPos(posOne) + assert isinstance(ctxMenu, QMenu) + assert ctxMenu.actions()[0].text() == "Open Project" + assert ctxMenu.actions()[1].text() == "Remove Project" + + # Open item from context menu + with monkeypatch.context() as mp: + mp.setattr(welcome, "close", lambda *a: None) + with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal: + ctxMenu.actions()[0].activate(QAction.ActionEvent.Trigger) + assert signal.args and signal.args[0] == Path("/stuff/project_two") + + ctxMenu.setObjectName("") + ctxMenu.deleteLater() + + # Delete item from context menu + ctxMenu = getMenuForPos(posOne) + assert isinstance(ctxMenu, QMenu) + ctxMenu.actions()[1].activate(QAction.ActionEvent.Trigger) + assert len(CONFIG.recentProjects.listEntries()) == 1 + + ctxMenu.setObjectName("") + ctxMenu.deleteLater() + + # Check removing entry error handling + assert listModel.data(listModel.createIndex(1, 0)) == ("", "", "") + assert listModel.removeEntry(listModel.createIndex(1, 0)) is False + with monkeypatch.context() as mp: + mp.setattr(listModel, "data", lambda *a: ("a", "b", "c")) + assert listModel.removeEntry(listModel.createIndex(1, 0)) is False + + # Delete last entry with keypress + qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posOne, delay=10) + qtbot.keyClick(tabOpen, Qt.Key.Key_Delete, delay=10) + assert len(CONFIG.recentProjects.listEntries()) == 0 + + # qtbot.stop() + welcome.close() + +# END Test testToolWelcome_Open + + +@pytest.mark.gui +def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath): + """Test the new project tab in the Welcome window.""" + welcome = GuiWelcome(nwGUI) + with qtbot.waitExposed(welcome): + welcome.show() + + welcome.newButton.click() + assert welcome.mainStack.currentIndex() == 1 + tabNew = welcome.tabNew + newForm = tabNew.projectForm + + # Populate the path + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncPath) + newForm.browsePath.click() + assert newForm.projPath.text() == str(fncPath) + + # Turning off root folders should disable notes switch + newForm.addNotes.setChecked(True) + newForm.addPlot.setChecked(False) + newForm.addChar.setChecked(False) + newForm.addWorld.setChecked(False) + newForm._syncSwitches() + assert newForm.addNotes.isChecked() is False + + # Change fill info to sample + newForm.fillSample.trigger() + assert newForm._fillMode == newForm.FILL_SAMPLE + assert newForm.projFill.text() == "Example Project" + assert newForm.addNotes.isEnabled() is False + assert newForm.addPlot.isEnabled() is False + assert newForm.addChar.isEnabled() is False + assert newForm.addWorld.isEnabled() is False + assert newForm.numChapters.isEnabled() is False + assert newForm.numScenes.isEnabled() is False + + # Change fill info to template + with monkeypatch.context() as mp: + mp.setattr(SHARED, "getProjectPath", lambda *a, **k: fncPath) + newForm.fillCopy.trigger() + assert newForm._fillMode == newForm.FILL_COPY + assert newForm.projFill.text() == f"Template: {fncPath}" + assert newForm.addNotes.isEnabled() is False + assert newForm.addPlot.isEnabled() is False + assert newForm.addChar.isEnabled() is False + assert newForm.addWorld.isEnabled() is False + assert newForm.numChapters.isEnabled() is False + assert newForm.numScenes.isEnabled() is False + + # Change back to fill blank using the menu + newForm.browseFill.click() + assert newForm.fillMenu.isVisible() is True + newForm.fillMenu.actions()[0].activate(QAction.ActionEvent.Trigger) + newForm.fillMenu.close() + assert newForm._fillMode == newForm.FILL_BLANK + assert newForm.projFill.text() == "Fresh Project" + assert newForm.addNotes.isEnabled() is True + assert newForm.addPlot.isEnabled() is True + assert newForm.addChar.isEnabled() is True + assert newForm.addWorld.isEnabled() is True + assert newForm.numChapters.isEnabled() is True + assert newForm.numScenes.isEnabled() is True + + # Creating a project without a name, pops an error + caplog.clear() + tabNew.createButton.click() + assert "A project name is required." in caplog.text + + # Set some more values, and extract data + projPath = fncPath / "Test Project" + newForm.projName.setText("Test Project") + newForm.projAuthor.setText("Jane Smith") + newForm.addPlot.setChecked(True) + newForm.addChar.setChecked(True) + newForm.addWorld.setChecked(True) + newForm.addNotes.setChecked(True) + newForm.numChapters.setValue(10) + newForm.numScenes.setValue(6) + assert newForm.getProjectData() == { + "name": "Test Project", + "author": "Jane Smith", + "language": "en_GB", + "path": str(projPath), + "blank": True, + "sample": False, + "template": None, + "chapters": 10, + "scenes": 6, + "roots": [nwItemClass.PLOT, nwItemClass.CHARACTER, nwItemClass.WORLD], + "notes": True, + } + + # Create a project with these values + with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal: + tabNew.createButton.click() + assert signal.args and signal.args[0] == projPath + assert (projPath / nwFiles.PROJ_FILE).exists() + + # qtbot.stop() + welcome.close() + +# END Test testToolWelcome_New