diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index ed8e0485..85fbf183 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -21,8 +21,8 @@ buttontext = 204, 204, 204 brighttext = 62, 62, 62 highlight = 44, 152, 247 highlightedtext = 255, 255, 255 -link = 44, 152, 247 -linkvisited = 44, 152, 247 +link = 102, 153, 204 +linkvisited = 102, 153, 204 [GUI] helptext = 164, 164, 164 diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index 6c28f979..9c67488b 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -21,8 +21,8 @@ buttontext = 0, 0, 0 brighttext = 255, 255, 255 highlight = 48, 135, 198 highlightedtext = 255, 255, 255 -link = 0, 84, 255 -linkvisited = 0, 84, 255 +link = 66, 113, 174 +linkvisited = 66, 113, 174 [GUI] helptext = 92, 92, 92 diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 4b205441..3cd6abc3 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -25,8 +25,6 @@ from __future__ import annotations import logging -from datetime import datetime - from PyQt5.QtGui import QCloseEvent, QColor from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( @@ -34,10 +32,10 @@ from PyQt5.QtWidgets import ( QWidget ) -from novelwriter import CONFIG, SHARED, __version__, __date__ -from novelwriter.common import formatVersion, readTextFile -from novelwriter.constants import nwConst, nwUnicode +from novelwriter import CONFIG, SHARED +from novelwriter.common import readTextFile from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.versioninfo import VersionInfoWidget logger = logging.getLogger(__name__) @@ -68,12 +66,7 @@ class GuiAbout(QDialog): self.nwLabel = QLabel(self) self.nwLabel.setPixmap(self.nwImage) - self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2} {1} {3}").format( - formatVersion(__version__), nwUnicode.U_ENDASH, - datetime.strptime(__date__, "%Y-%m-%d").strftime("%x"), - "{1}".format(nwConst.URL_RELEASES, self.tr("Release Notes")) - )) - self.nwInfo.setOpenExternalLinks(True) + self.nwInfo = VersionInfoWidget(self) self.nwLicence = QLabel(self.tr("This application is licenced under {0}".format( "GPL v3.0" @@ -82,8 +75,7 @@ class GuiAbout(QDialog): # Credits self.lblCredits = NColourLabel( - self.tr("Credits"), SHARED.theme.helpText, - scale=NColourLabel.HEADER_SCALE, parent=self + self.tr("Credits"), scale=1.6, parent=self, bold=True ) self.txtCredits = QTextBrowser(self) @@ -100,6 +92,7 @@ class GuiAbout(QDialog): self.innerBox.addSpacing(hB) self.innerBox.addWidget(self.nwLabel) self.innerBox.addWidget(self.nwInfo) + self.innerBox.addSpacing(hA) self.innerBox.addWidget(self.nwLicence) self.innerBox.addSpacing(hA) self.innerBox.addWidget(self.lblCredits) @@ -156,33 +149,10 @@ class GuiAbout(QDialog): def _setStyleSheet(self) -> None: """Set stylesheet for all browser tabs.""" - colHead = SHARED.theme.colHead - colKey = SHARED.theme.colKey - styleSheet = ( - "h1, h2, h3, h4 {{" - " color: rgb({hColR}, {hColG}, {hColB});" - "}}\n" - "a {{" - " color: rgb({hColR}, {hColG}, {hColB});" - "}}\n" - ".alt {{" - " color: rgb({kColR}, {kColG}, {kColB});" - "}}\n" - ).format( - hColR=colHead.red(), - hColG=colHead.green(), - hColB=colHead.blue(), - kColR=colKey.red(), - kColG=colKey.green(), - kColB=colKey.blue(), - ) - self.txtCredits.document().setDefaultStyleSheet(styleSheet) - baseCol = self.palette().window().color() self.txtCredits.setStyleSheet(( "QTextBrowser {{border: none; background: rgb({r},{g},{b});}} " ).format(r=baseCol.red(), g=baseCol.green(), b=baseCol.blue())) - return # END Class GuiAbout diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py deleted file mode 100644 index ef0b0c1f..00000000 --- a/novelwriter/dialogs/updates.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -novelWriter – GUI Updates -========================= - -File History: -Created: 2021-08-21 [1.5b1] GuiUpdates - -This file is a part of novelWriter -Copyright 2018–2024, 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 json -import logging - -from datetime import datetime -from urllib.request import Request, urlopen - -from PyQt5.QtGui import QCloseEvent, QCursor -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import ( - QWidget, qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel -) - -from novelwriter import CONFIG, SHARED, __version__, __date__ -from novelwriter.common import logException -from novelwriter.constants import nwConst - -logger = logging.getLogger(__name__) - - -class GuiUpdates(QDialog): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - logger.debug("Create: GuiUpdates") - self.setObjectName("GuiUpdates") - self.setWindowTitle(self.tr("Check for Updates")) - - nPx = CONFIG.pxInt(96) - sPx = CONFIG.pxInt(16) - tPx = CONFIG.pxInt(8) - mPx = CONFIG.pxInt(4) - - # Left Box - self.nwIcon = QLabel() - self.nwIcon.setPixmap(SHARED.theme.getPixmap("novelwriter", (nPx, nPx))) - - self.leftBox = QVBoxLayout() - self.leftBox.addWidget(self.nwIcon) - self.leftBox.addStretch(1) - - # Right Box - self.currentLabel = QLabel(self.tr("Current Release")) - self.currentValue = QLabel(self.tr( - "novelWriter {0} released on {1}" - ).format( - "v%s" % __version__, - datetime.strptime(__date__, "%Y-%m-%d").strftime("%x")) - ) - - self.latestLabel = QLabel(self.tr("Latest Release")) - self.latestValue = QLabel(self.tr("Checking ...")) - self.latestLink = QLabel("") - self.latestLink.setOpenExternalLinks(True) - - self.rightBox = QVBoxLayout() - self.rightBox.addWidget(self.currentLabel) - self.rightBox.addWidget(self.currentValue) - self.rightBox.addSpacing(tPx) - self.rightBox.addWidget(self.latestLabel) - self.rightBox.addWidget(self.latestValue) - self.rightBox.addSpacing(tPx) - self.rightBox.addWidget(self.latestLink) - self.rightBox.setSpacing(mPx) - - hFont = self.currentLabel.font() - hFont.setBold(True) - self.currentLabel.setFont(hFont) - self.latestLabel.setFont(hFont) - - # Buttons - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) - self.buttonBox.rejected.connect(self.close) - - # Assemble - self.innerBox = QHBoxLayout() - self.innerBox.addLayout(self.leftBox) - self.innerBox.addLayout(self.rightBox) - self.innerBox.setSpacing(sPx) - - self.outerBox = QVBoxLayout() - self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(self.buttonBox) - self.outerBox.setSpacing(sPx) - - self.setLayout(self.outerBox) - - logger.debug("Ready: GuiUpdates") - - return - - def __del__(self) -> None: # pragma: no cover - logger.debug("Delete: GuiUpdates") - return - - def checkLatest(self) -> None: - """Check for latest release.""" - qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) - - urlReq = Request("https://api.github.com/repos/vkbo/novelwriter/releases/latest") - urlReq.add_header("User-Agent", nwConst.USER_AGENT) - urlReq.add_header("Accept", "application/vnd.github.v3+json") - - rawData = {} - try: - urlData = urlopen(urlReq, timeout=10) - rawData = json.loads(urlData.read().decode()) - except Exception: - logger.error("Failed to contact GitHub API") - logException() - - relVersion = rawData.get("tag_name", "Unknown") - relDate = rawData.get("created_at", "") - - try: - relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x") - except Exception: - relDate = "Unknown" - logException() - - self.latestValue.setText(self.tr( - "novelWriter {0} released on {1}" - ).format( - relVersion, relDate - )) - - self.latestLink.setText(self.tr( - "Download: {0}" - ).format( - f'{nwConst.URL_WEB}' - )) - - qApp.restoreOverrideCursor() - - return - - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the user closing the window.""" - event.accept() - self.deleteLater() - return - -# END Class GuiUpdates diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index fb4cd37d..aa168778 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -26,7 +26,7 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtGui import QColor, QPalette +from PyQt5.QtGui import QColor, QFont, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QAbstractButton, QFrame, QHBoxLayout, QLabel, QLayout, QScrollArea, @@ -198,7 +198,7 @@ class NScrollableForm(QScrollArea): if helpText: qHelp = NColourLabel( - str(helpText), self._helpCol, parent=self, + str(helpText), color=self._helpCol, parent=self, scale=self._fontScale, wrap=True, indent=self._indent ) labelBox = QVBoxLayout() @@ -249,16 +249,19 @@ class NColourLabel(QLabel): HELP_SCALE = DEFAULT_SCALE HEADER_SCALE = 1.25 - def __init__(self, text: str, color: QColor, parent: QWidget | None = None, - scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None: + def __init__(self, text: str, color: QColor | None = None, parent: QWidget | None = None, + scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0, + bold: bool = False) -> None: super().__init__(text, parent=parent) font = self.font() font.setPointSizeF(scale*font.pointSizeF()) - colour = self.palette() - colour.setColor(QPalette.WindowText, color) + font.setWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal) + if color: + colour = self.palette() + colour.setColor(QPalette.WindowText, color) + self.setPalette(colour) - self.setPalette(colour) self.setFont(font) self.setIndent(indent) self.setWordWrap(wrap) diff --git a/novelwriter/extensions/versioninfo.py b/novelwriter/extensions/versioninfo.py new file mode 100644 index 00000000..08cbc74e --- /dev/null +++ b/novelwriter/extensions/versioninfo.py @@ -0,0 +1,153 @@ +""" +novelWriter – Custom Widget: Version Info +========================================= + +File History: +Created: 2024-02-14 [2.3b1] VersionInfoWidget + +This file is a part of novelWriter +Copyright 2018–2024, 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 json +import logging + +from time import sleep +from datetime import datetime +from urllib.error import HTTPError +from urllib.request import Request, urlopen + +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtCore import QObject, QRunnable, QUrl, pyqtSignal, pyqtSlot +from PyQt5.QtWidgets import QLabel, QVBoxLayout, QWidget + +from novelwriter import CONFIG, SHARED, __version__, __date__, __domain__ +from novelwriter.common import formatVersion +from novelwriter.constants import nwConst + +logger = logging.getLogger(__name__) + +API_URL = "https://api.github.com/repos/vkbo/novelwriter/releases/latest" + + +class VersionInfoWidget(QWidget): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + # Label Strings + self._trLatest = self.tr("Latest Version: {0}") + self._trChecking = self.tr("Checking ...") + self._trDownload = self.tr("Download from {0}") + + # Labels + self._lblInfo = QLabel("{0} {1} \u2013 {2} {3} \u2013 {4}".format( + self.tr("Version"), formatVersion(__version__), + self.tr("Released on"), datetime.strptime(__date__, "%Y-%m-%d").strftime("%x"), + "{0}".format(self.tr("Release Notes")), + ), self) + self._lblInfo.linkActivated.connect(self._processLink) + self._lblRelease = QLabel(self._trLatest.format( + "{0}".format(self.tr("Check Now")) + ), self) + self._lblRelease.linkActivated.connect(self._processLink) + + # Assemble + self._layout = QVBoxLayout() + self._layout.addWidget(self._lblInfo) + self._layout.addWidget(self._lblRelease) + self._layout.setSpacing(CONFIG.pxInt(2)) + self._layout.setContentsMargins(0, 0, 0, 0) + + self.setLayout(self._layout) + + return + + ## + # Private Slots + ## + + @pyqtSlot(str) + def _processLink(self, link: str) -> None: + """Process an activated link.""" + if link == "#notes": + QDesktopServices.openUrl(QUrl(nwConst.URL_RELEASES)) + elif link == "#website": + QDesktopServices.openUrl(QUrl(nwConst.URL_WEB)) + elif link == "#update": + self._lblRelease.setText(self._trLatest.format(self._trChecking)) + lookup = _Retriever() + lookup.signals.dataReady.connect(self._updateReleaseInfo) + SHARED.runInThreadPool(lookup) + return + + ## + # Private Slots + ## + + @pyqtSlot(str, str) + def _updateReleaseInfo(self, tag: str, reason: str) -> None: + """Update the widget release info.""" + if version := tag.lstrip("v"): + download = f"{__domain__}" + self._lblRelease.setText(self._trLatest.format( + f"{version} \u2013 {self._trDownload.format(download)}" + )) + else: + self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed"))) + return + +# END Class VersionInfoWidget + + +class _Retriever(QRunnable): + + def __init__(self) -> None: + super().__init__() + self.signals = _RetrieverSignal() + return + + @pyqtSlot() + def run(self) -> None: + """Poll the GitHub API in the background. + Note: The GitHub API is rate limited at 60 requests per hour. + """ + logger.info("Contacting: %s", API_URL) + req = Request(API_URL) + req.add_header("User-Agent", nwConst.USER_AGENT) + req.add_header("Accept", "application/vnd.github.v3+json") + sleep(0.2) + try: + with urlopen(req, timeout=15) as ret: + if isinstance(raw := json.loads(ret.read().decode()), dict): + self.signals.dataReady.emit(raw.get("tag_name", ""), "") + except HTTPError as e: + reason = f"{e.reason.capitalize()} (HTTP {e.code})" + logger.error(reason) + self.signals.dataReady.emit("", reason) + except Exception as e: + logger.error("Failed to retrieve release info") + self.signals.dataReady.emit("", str(e)) + return + +# END Class _Retriever + + +class _RetrieverSignal(QObject): + dataReady = pyqtSignal(str, str) + +# END Class _RetrieverSignal diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 0465bed5..ae99f6c2 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -964,13 +964,6 @@ class GuiMainMenu(QMenuBar): self.aWebsite = self.helpMenu.addAction(self.tr("The novelWriter Website")) self.aWebsite.triggered.connect(lambda: self._openWebsite(nwConst.URL_WEB)) - # Help > Separator - self.helpMenu.addSeparator() - - # Document > Check for Updates - self.aUpdates = self.helpMenu.addAction(self.tr("Check for New Release")) - self.aUpdates.triggered.connect(self.mainGui.showUpdatesDialog) - return # END Class GuiMainMenu diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c14b7576..2f99ed86 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -51,7 +51,6 @@ from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.docviewerpanel import GuiDocViewerPanel from novelwriter.dialogs.about import GuiAbout -from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projectsettings import GuiProjectSettings @@ -884,17 +883,6 @@ class GuiMain(QMainWindow): msgBox.aboutQt(self, "About Qt") return - @pyqtSlot() - def showUpdatesDialog(self) -> None: - """Show the check for updates dialog.""" - dialog = GuiUpdates(self) - dialog.setModal(True) - dialog.show() - dialog.raise_() - qApp.processEvents() - dialog.checkLatest() - return - @pyqtSlot() def showDictionariesDialog(self) -> None: """Show the download dictionaries dialog.""" diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 25386771..d0085f38 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -40,13 +40,14 @@ from PyQt5.QtWidgets import ( QToolButton, QVBoxLayout, QWidget, qApp ) -from novelwriter import CONFIG, SHARED, __version__, __date__ +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass -from novelwriter.common import formatInt, formatVersion, makeFileNameSafe -from novelwriter.constants import nwFiles, nwUnicode +from novelwriter.common import formatInt, makeFileNameSafe +from novelwriter.constants import nwFiles from novelwriter.core.coretools import ProjectBuilder from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.modified import NComboBox, NSpinBox +from novelwriter.extensions.versioninfo import VersionInfoWidget logger = logging.getLogger(__name__) @@ -90,10 +91,7 @@ class GuiWelcome(QDialog): self.nwLabel = QLabel(self) self.nwLabel.setPixmap(self.nwImage) - self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2}").format( - formatVersion(__version__), nwUnicode.U_ENDASH, - datetime.strptime(__date__, "%Y-%m-%d").strftime("%x") - )) + self.nwInfo = VersionInfoWidget(self) self.tabOpen = _OpenProjectPage(self) self.tabOpen.openProjectRequest.connect(self._openProjectPath) diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 94d488ed..7cf9d9f6 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -23,10 +23,9 @@ from __future__ import annotations import pytest from PyQt5.QtCore import QItemSelectionModel -from PyQt5.QtWidgets import QAction, QListWidgetItem, QDialog +from PyQt5.QtWidgets import QListWidgetItem, QDialog from novelwriter.dialogs.quotes import GuiQuoteSelect -from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.editlabel import GuiEditLabel @@ -66,44 +65,6 @@ def testDlgOther_QuoteSelect(qtbot, monkeypatch, nwGUI): # END Test testDlgOther_QuoteSelect -@pytest.mark.gui -def testDlgOther_Updates(qtbot, monkeypatch, nwGUI): - """Test the check for updates dialog.""" - nwUpdate = GuiUpdates(nwGUI) - nwUpdate.show() - - class mockData: - def decode(self): - return '{"tag_name": "v1.0", "created_at": "2021-01-01T12:00:00Z"}' - - class mockPayload: - def read(self): - return mockData() - - def mockUrlopenA(*a, **k): - return None - - def mockUrlopenB(*a, **k): - return mockPayload() - - # Faulty Return - monkeypatch.setattr("novelwriter.dialogs.updates.urlopen", mockUrlopenA) - nwUpdate.checkLatest() - - # Valid Return - monkeypatch.setattr("novelwriter.dialogs.updates.urlopen", mockUrlopenB) - nwUpdate.checkLatest() - assert nwUpdate.latestValue.text().startswith("novelWriter v1.0") - - # Trigger from Menu - nwGUI.mainMenu.aUpdates.activate(QAction.Trigger) - - # qtbot.stop() - nwUpdate.close() - -# END Test testDlgOther_Updates - - @pytest.mark.gui def testDlgOther_EditLabel(qtbot, monkeypatch): """Test the label editor dialog.""" diff --git a/tests/test_ext/test_ext_modified.py b/tests/test_ext/test_ext_modified.py index c1d0e3c2..7ea40755 100644 --- a/tests/test_ext/test_ext_modified.py +++ b/tests/test_ext/test_ext_modified.py @@ -1,6 +1,6 @@ """ -novelWriter – Wheel Event Filter Tester -======================================= +novelWriter – Modified Widgets Tester +===================================== This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen diff --git a/tests/test_ext/test_ext_versioninfo.py b/tests/test_ext/test_ext_versioninfo.py new file mode 100644 index 00000000..b30fb19c --- /dev/null +++ b/tests/test_ext/test_ext_versioninfo.py @@ -0,0 +1,155 @@ +""" +novelWriter – Version Info Widget Tester +======================================== + +This file is a part of novelWriter +Copyright 2018–2024, 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 urllib.error import HTTPError + +from tools import SimpleDialog + +from PyQt5.QtCore import QUrl + +from novelwriter import SHARED +from novelwriter.constants import nwConst +from novelwriter.extensions.versioninfo import VersionInfoWidget, _Retriever, _RetrieverSignal + + +class MockRetriever: + signals = _RetrieverSignal() + + +class MockDesktopServices: + + url = None + + @staticmethod + def openUrl(url): + MockDesktopServices.url = url + return + + +class MockData: + pass + + +class MockPayload: + + def __enter__(self): + return self + + def __exit__(self, *args): + return + + def read(self): + data = MockData() + data.decode = lambda *a: '{"tag_name": "v1.0"}' + return data + + +class MockHTTPError: + + def __enter__(self): + raise HTTPError("http://example.com", 403, "Rate limit", None, None) + + def __exit__(self, *args): + return + + +class MockException: + + def __enter__(self): + raise Exception("Oh noes!") + + def __exit__(self, *args): + return + + +@pytest.mark.gui +def testExtVersionInfo_Main(qtbot, monkeypatch): + """Test the VersionInfoWidget class.""" + version = VersionInfoWidget(None) + dialog = SimpleDialog(version) + dialog.show() + + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo.QDesktopServices", MockDesktopServices) + version._processLink("#notes") + assert MockDesktopServices.url == QUrl(nwConst.URL_RELEASES) + + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo.QDesktopServices", MockDesktopServices) + version._processLink("#website") + assert MockDesktopServices.url == QUrl(nwConst.URL_WEB) + + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo._Retriever", MockRetriever) + mp.setattr(SHARED, "runInThreadPool", lambda *a: None) + version._processLink("#update") + assert version._lblRelease.text() == "Latest Version: Checking ..." + + version._updateReleaseInfo("v2.0", "") + assert version._lblRelease.text() == ( + "Latest Version: 2.0 – Download from novelwriter.io" + ) + + version._updateReleaseInfo("", "Error") + assert version._lblRelease.text() == "Latest Version: Error" + + version._updateReleaseInfo("", "") + assert version._lblRelease.text() == "Latest Version: Failed" + + dialog.close() + # qtbot.stop() + +# END Test testExtVersionInfo_Main + + +@pytest.mark.gui +def testExtVersionInfo_Retriever(qtbot, monkeypatch): + """Test the _Retriever class.""" + task = _Retriever() + + # Valid Data + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo.urlopen", lambda *a, **k: MockPayload()) + with qtbot.waitSignal(task.signals.dataReady) as signal: + task.run() + assert signal.args == ["v1.0", ""] + + def httpErr(): + raise HTTPError("http://example.com", 403, "Rate limit") + + # HTTP Error + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo.urlopen", lambda *a, **k: MockHTTPError()) + with qtbot.waitSignal(task.signals.dataReady) as signal: + task.run() + assert signal.args == ["", "Rate limit (HTTP 403)"] + + # Other Error + with monkeypatch.context() as mp: + mp.setattr("novelwriter.extensions.versioninfo.urlopen", lambda *a, **k: MockException()) + with qtbot.waitSignal(task.signals.dataReady) as signal: + task.run() + assert signal.args == ["", "Oh noes!"] + +# END Test testExtVersionInfo_Retriever