Files
2025-10-26 00:12:37 +02:00

257 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 <https://www.gnu.org/licenses/>.
""" # noqa
from __future__ import annotations
import sys
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}<br><b>Exception</b>: {exc!s}"
# Alert: Info
alert.setAlertType(_GuiAlert.INFO, False)
assert hasattr(alert, "_btnOk")
if sys.platform != "darwin": # Not set on MacOS
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")
if sys.platform != "darwin": # Not set on MacOS
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")
if sys.platform != "darwin": # Not set on MacOS
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")
if sys.platform != "darwin": # Not set on MacOS
assert alert.windowTitle() == "Question"
alert._btnYes.click()
assert alert.finalState is True
alert._btnNo.click()
assert alert.finalState is False