diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 0d4e90a8..d606b8d3 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -1392,7 +1392,7 @@ class GuiMain(QMainWindow):
userIdle = qApp.applicationState() != Qt.ApplicationActive
self.mainStatus.setUserIdle(editIdle or userIdle)
SHARED.updateIdleTime(currTime, editIdle or userIdle)
- self.mainStatus.updateTime(idleTime=SHARED.idleTime)
+ self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
return
@pyqtSlot()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 07ea2d81..eea5fe26 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class SharedData(QObject):
__slots__ = (
- "_gui", "_theme", "_project", "_leckedBy", "_alert",
+ "_gui", "_theme", "_project", "_lockedBy", "_alert",
"_idleTime", "_idleRefTime",
)
@@ -58,28 +58,28 @@ class SharedData(QObject):
self._lockedBy = None
self._alert = None
self._idleTime = 0.0
- self._idleRedTime = time()
+ self._idleRefTime = time()
return
@property
def mainGui(self) -> GuiMain:
"""Return the Main GUI instance."""
if self._gui is None:
- raise Exception("UserData class not properly initialised")
+ raise Exception("SharedData class not fully initialised")
return self._gui
@property
def theme(self) -> GuiTheme:
"""Return the GUI Theme instance."""
if self._theme is None:
- raise Exception("UserData class not properly initialised")
+ raise Exception("SharedData class not fully initialised")
return self._theme
@property
def project(self) -> NWProject:
"""Return the active NWProject instance."""
if self._project is None:
- raise Exception("UserData class not properly initialised")
+ raise Exception("SharedData class not fully initialised")
return self._project
@property
@@ -92,16 +92,16 @@ class SharedData(QObject):
"""Return cached lock information for the last project."""
return self._lockedBy
+ @property
+ def projectIdleTime(self) -> float:
+ """Return the session idle time."""
+ return self._idleTime
+
@property
def alert(self) -> _GuiAlert | None:
"""Return a pointer to the last alert box."""
return self._alert
- @property
- def idleTime(self) -> float:
- """Return the session idle time."""
- return self._idleTime
-
##
# Methods
##
@@ -154,12 +154,9 @@ class SharedData(QObject):
the last time this function was called. Otherwise, only the
reference time is updated.
"""
- if hasattr(self, "_idleRefTime"):
- # This method is called by a timer from C++, and the
- # instance may not have be initialised
- if userIdle:
- self._idleTime += currTime - self._idleRefTime
- self._idleRefTime = currTime
+ if userIdle:
+ self._idleTime += currTime - self._idleRefTime
+ self._idleRefTime = currTime
return
##
diff --git a/tests/conftest.py b/tests/conftest.py
index a21551a8..4d6e6e72 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -22,6 +22,7 @@ along with this program. If not, see .
import sys
import pytest
import shutil
+import logging
from pathlib import Path
@@ -62,6 +63,7 @@ def resetConfigVars():
@pytest.fixture(scope="session", autouse=True)
def sessionFixture():
"""A session wide fixture to set up the test environment."""
+ logging.root.setLevel(logging.INFO)
if _TMP_ROOT.exists():
shutil.rmtree(_TMP_ROOT)
_TMP_ROOT.mkdir()
@@ -81,6 +83,7 @@ def functionFixture(qtbot):
CONFIG.__init__()
CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF)
resetConfigVars()
+ logging.getLogger("novelwriter").setLevel(logging.INFO)
return
diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py
index b6e09d15..cbc20c51 100644
--- a/tests/test_base/test_base_init.py
+++ b/tests/test_base/test_base_init.py
@@ -30,8 +30,7 @@ from novelwriter import CONFIG, main, logger
@pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, fncPath):
- """Check launching the main GUI.
- """
+ """Check launching the main GUI."""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
# TestMode Launch
@@ -80,8 +79,7 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath):
@pytest.mark.base
def testBaseInit_Options(monkeypatch, fncPath):
- """Test command line options for logging level.
- """
+ """Test command line options for logging level."""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr(sys, "argv", [
"novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}"
@@ -146,8 +144,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
@pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, fncPath):
- """Check import error handling.
- """
+ """Check import error handling."""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0)
diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py
new file mode 100644
index 00000000..80f80954
--- /dev/null
+++ b/tests/test_base/test_base_shared.py
@@ -0,0 +1,187 @@
+"""
+novelWriter – SharedData Class 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 .
+"""
+
+import pytest
+
+from mocked import MockGuiMain, MockTheme
+
+from PyQt5.QtWidgets import QMessageBox
+
+from novelwriter.core.project import NWProject
+from novelwriter.shared import SharedData, _GuiAlert
+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(Exception):
+ shared.mainGui
+ with pytest.raises(Exception):
+ shared.theme
+ with pytest.raises(Exception):
+ shared.project
+
+ # Create some mock objects
+ mockGui = MockGuiMain()
+ mockTheme = MockTheme()
+ assert mockGui is not mockTheme
+
+ # Properly initialise the class
+ shared.initSharedData(mockGui, mockTheme) # 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
+
+ assert shared.alert is None
+
+# END Test testBaseSharedData_Init
+
+
+@pytest.mark.base
+def testBaseSharedData_Projects(fncPath, caplog: pytest.LogCaptureFixture):
+ """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.initSharedData(mockGui, mockTheme) # 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 out shared instance
+ assert shared.hasProject is False
+ assert isinstance(shared.projectLock, list)
+
+# END Test testBaseSharedData_Projects
+
+
+@pytest.mark.base
+def testBaseSharedData_Alerts(monkeypatch, caplog: pytest.LogCaptureFixture):
+ """Test SharedData class alert helper functions."""
+ monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
+ monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
+
+ shared = SharedData()
+
+ mockGui = MockGuiMain()
+ mockTheme = MockTheme()
+ shared.initSharedData(mockGui, mockTheme) # type: ignore
+
+ assert shared.alert is None
+
+ # Info box
+ caplog.clear()
+ shared.info("Hello World", info="foo", details="bar")
+ assert isinstance(shared.alert, _GuiAlert)
+ assert shared.alert.text() == "Hello World"
+ assert shared.alert.informativeText() == "foo"
+ assert shared.alert.detailedText() == "bar"
+ assert caplog.text.strip().startswith("INFO")
+ assert caplog.text.strip().endswith("Hello World foo bar")
+ shared._alert = None
+
+ # Warning box
+ caplog.clear()
+ shared.warn("Oops!", info="foo", details="bar")
+ assert isinstance(shared.alert, _GuiAlert)
+ assert shared.alert.text() == "Oops!"
+ assert shared.alert.informativeText() == "foo"
+ assert shared.alert.detailedText() == "bar"
+ assert caplog.text.strip().startswith("WARNING")
+ assert caplog.text.strip().endswith("Oops! foo bar")
+ shared._alert = None
+
+ # Error box
+ caplog.clear()
+ shared.error("Oh noes!", info="foo", details="bar")
+ assert isinstance(shared.alert, _GuiAlert)
+ assert shared.alert.text() == "Oh noes!"
+ assert shared.alert.informativeText() == "foo"
+ assert shared.alert.detailedText() == "bar"
+ assert caplog.text.strip().startswith("ERROR")
+ assert caplog.text.strip().endswith("Oh noes! foo bar")
+ shared._alert = None
+
+ # Error box with exception
+ caplog.clear()
+ shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!"))
+ assert isinstance(shared.alert, _GuiAlert)
+ assert shared.alert.text() == "Oh noes!"
+ assert shared.alert.informativeText() == "foo
Exception: Boom!"
+ assert shared.alert.detailedText() == "bar"
+ assert caplog.text.strip().startswith("ERROR")
+ assert caplog.text.strip().endswith("Oh noes! foo bar")
+ shared._alert = None
+
+ # Question box
+ assert shared.question("Why?") is True
+ assert isinstance(shared.alert, _GuiAlert)
+ assert shared.alert.text() == "Why?"
+ shared._alert = None
+
+# END Test testBaseSharedData_Alerts