""" novelWriter – SharedData Class Tester ===================================== This file is a part of novelWriter Copyright (C) 2023 Veronica Berglyd Olsen and novelWriter contributors 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 . """ # noqa from __future__ import annotations from unittest.mock import MagicMock import pytest from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices from PyQt6.QtWidgets import QFileDialog, QWidget from novelwriter.core.project import NWProject from novelwriter.shared import SharedData, _GuiAlert from tests.mocked import MockGuiMain, MockTheme from tests.tools import buildTestProject @pytest.mark.base def testBaseSharedData_Init(): """Test SharedData class initialisation.""" shared = SharedData() # When not initialised, it should raise exceptions with pytest.raises(RuntimeError): _ = shared.mainGui with pytest.raises(RuntimeError): _ = shared.theme with pytest.raises(RuntimeError): _ = shared.project with pytest.raises(RuntimeError): _ = shared.spelling # Create some mock objects mockGui = MockGuiMain() mockTheme = MockTheme() assert mockGui is not mockTheme # Properly initialise the class shared.initTheme(mockTheme) # type: ignore shared.initSharedData(mockGui) # type: ignore assert shared.mainGui is mockGui assert shared.theme is mockTheme assert isinstance(shared.project, NWProject) assert shared.hasProject is False assert shared.projectIdleTime == 0.0 assert shared.projectLock is None @pytest.mark.base def testBaseSharedData_Functions(monkeypatch): """Test SharedData class functions.""" shared = SharedData() # Open URL with monkeypatch.context() as mp: openUrl = MagicMock() mp.setattr(QDesktopServices, "openUrl", openUrl) shared.openWebsite("http://www.example.com") assert openUrl.called is True assert openUrl.call_args[0][0] == QUrl("http://www.example.com") @pytest.mark.base def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): """Test SharedData handling of projects.""" project = NWProject() buildTestProject(project, fncPath) project.closeProject(0.0) # Clears the lockfile shared = SharedData() assert shared._project is None # Initialise the instance, should create an empty project mockGui = MockGuiMain() mockTheme = MockTheme() shared.initTheme(mockTheme) # type: ignore shared.initSharedData(mockGui) # type: ignore assert isinstance(shared.project, NWProject) assert shared.hasProject is False # Load the test project assert shared.openProject(fncPath) is True assert shared.hasProject is True # We cannot open two projects caplog.clear() assert shared.openProject(fncPath) is False assert caplog.messages[-1] == "A project is already open" assert shared._idleTime == 0.0 # Update idle time refTime = shared._idleRefTime shared.updateIdleTime(refTime + 1.0, False) shared.updateIdleTime(refTime + 2.0, True) shared.updateIdleTime(refTime + 3.0, False) shared.updateIdleTime(refTime + 4.0, True) assert round(shared.projectIdleTime) == 2 # Save project assert shared.saveProject() is True # Close project shared.closeProject() assert shared.hasProject is False # Cannot save a project after it's been closed assert shared.saveProject() is False # Check locked project info project.openProject(fncPath) # First open with our independent project instance assert shared.hasProject is False assert shared.projectLock is None assert shared.openProject(fncPath) is False # Then with our shared instance assert shared.hasProject is False assert isinstance(shared.projectLock, list) # Test browsing for projects with monkeypatch.context() as mp: mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (a[2], "")) assert shared.getProjectPath(QWidget(), fncPath, allowZip=True) == fncPath @pytest.mark.base def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog, mockGUI): """Test SharedData class alert helper functions.""" monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) monkeypatch.setattr(_GuiAlert, "finalState", True) shared = SharedData() mockGui = MockGuiMain() mockTheme = MockTheme() shared.initTheme(mockTheme) # type: ignore shared.initSharedData(mockGui) # type: ignore assert shared.lastAlert == "" # Info box caplog.clear() shared.info("Hello World", info="foo", details="bar") assert shared.lastAlert == "Hello World foo bar" assert caplog.text.strip().startswith("INFO") assert caplog.text.strip().endswith("Hello World foo bar") # Warning box caplog.clear() shared.warn("Oops!", info="foo", details="bar") assert shared.lastAlert == "Oops! foo bar" assert caplog.text.strip().startswith("WARNING") assert caplog.text.strip().endswith("Oops! foo bar") # Error box caplog.clear() shared.error("Oh noes!", info="foo", details="bar") assert shared.lastAlert == "Oh noes! foo bar" assert caplog.text.strip().startswith("ERROR") assert caplog.text.strip().endswith("Oh noes! foo bar") # Error box with exception caplog.clear() shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!")) assert shared.lastAlert == "Oh noes! foo bar" assert caplog.text.strip().startswith("ERROR") assert caplog.text.strip().endswith("Oh noes! foo bar") # Question box assert shared.question("Why?") is True assert shared.lastAlert == "Why?" @pytest.mark.base def testBaseSharedData_GuiAlert(): """Test the _GuiAlert class.""" alert = _GuiAlert(None, MockTheme()) # type: ignore # Default states assert alert.logMessage == "" assert alert.finalState is False # Populate message text = "one" info = "two" details = "three" alert.setMessage(text, info, details) assert alert.logMessage == f"{text} {info} {details}" assert alert.text() == text assert alert.informativeText() == info assert alert.detailedText() == details # Populate exception exc = Exception("oops") alert.setException(exc) assert alert.logMessage == f"{text} {info} {details}" assert alert.informativeText() == f"{info}
Exception: {exc!s}" # Alert: Info alert.setAlertType(_GuiAlert.INFO, False) assert hasattr(alert, "_btnOk") assert alert.windowTitle() == "Information" alert._btnOk.click() assert alert.finalState is True alert._state = False # Alert: Warning alert.setAlertType(_GuiAlert.WARN, False) assert hasattr(alert, "_btnOk") assert alert.windowTitle() == "Warning" alert._btnOk.click() assert alert.finalState is True alert._state = False # Alert: Error alert.setAlertType(_GuiAlert.ERROR, False) assert hasattr(alert, "_btnOk") assert alert.windowTitle() == "Error" alert._btnOk.click() assert alert.finalState is True alert._state = False # Alert: Question alert.setAlertType(_GuiAlert.ASK, True) assert hasattr(alert, "_btnYes") assert hasattr(alert, "_btnNo") assert alert.windowTitle() == "Question" alert._btnYes.click() assert alert.finalState is True alert._btnNo.click() assert alert.finalState is False