From bd2353f7bce78bae7aa7f7b2c85971ff5dfb7362 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:36:53 +0200 Subject: [PATCH] Add tests for sidebar and themes --- novelwriter/gui/theme.py | 43 +- tests/test_gui/test_gui_sidebar.py | 53 +++ tests/test_gui/test_gui_theme.py | 626 +++++++++++++++-------------- 3 files changed, 389 insertions(+), 333 deletions(-) create mode 100644 tests/test_gui/test_gui_sidebar.py diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 463a8dc0..5c7533ae 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -240,8 +240,8 @@ class GuiTheme: return hint.colorScheme() == Qt.ColorScheme.Dark palette = QPalette() - text = palette.color(QPalette.ColorRole.WindowText) - window = palette.color(QPalette.ColorRole.Window) + text = palette.windowText().color() + window = palette.window().color() return text.lightnessF() > window.lightnessF() def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: @@ -270,7 +270,7 @@ class GuiTheme: return QColor(*result) return default - def loadTheme(self, force: bool = False) -> bool: + def loadTheme(self, force: bool = False) -> None: """Load the currently specified GUI theme.""" match CONFIG.themeMode: case nwTheme.LIGHT: @@ -282,7 +282,7 @@ class GuiTheme: theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme if theme not in self._allThemes: - logger.error("Could not find theme for key '%s'", theme) + logger.error("Could not find theme '%s'", theme) if darkMode: theme = DEF_GUI_DARK CONFIG.darkTheme = DEF_GUI_DARK @@ -292,23 +292,22 @@ class GuiTheme: if theme == self._currentTheme and not force: logger.info("Theme '%s' is already loaded", theme) - return False + return entry = self._allThemes.get(theme) if not entry: logger.error("Could not load GUI theme") - return False + return CONFIG.splashMessage(f"Loading colour theme: {entry.name}") logger.info("Loading GUI theme '%s'", theme) parser = ConfigParser() try: - with open(entry.path, mode="r", encoding="utf-8") as fo: - parser.read_file(fo) + parser.read(entry.path, encoding="utf-8") except Exception: logger.error("Could not read file: %s", entry.path) logException() - return False + return # Reset Palette self._resetTheme() @@ -458,15 +457,15 @@ class GuiTheme: # Set project override colours if (override := CONFIG.iconColTree) != "theme": - color = self._svgColors.get(override, b"#000000") - self._svgColors["root"] = color - self._svgColors["folder"] = color + color = self._qColors.get(override, QtBlack) + self._setBaseColor("root", color) + self._setBaseColor("folder", color) if not CONFIG.iconColDocs: - self._svgColors["file"] = color - self._svgColors["title"] = color - self._svgColors["chapter"] = color - self._svgColors["scene"] = color - self._svgColors["note"] = color + self._setBaseColor("file", color) + self._setBaseColor("title", color) + self._setBaseColor("chapter", color) + self._setBaseColor("scene", color) + self._setBaseColor("note", color) self.isDarkTheme = darkMode self._currentTheme = theme @@ -478,7 +477,7 @@ class GuiTheme: QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) - return True + return def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" @@ -685,7 +684,7 @@ class GuiIcons: self._scanThemes(icons) return - def loadTheme(self, theme: str) -> bool: + def loadTheme(self, theme: str) -> None: """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. @@ -698,7 +697,7 @@ class GuiIcons: entry = self._allThemes.get(theme) if not entry: logger.error("Could not load icon theme") - return False + return CONFIG.splashMessage(f"Loading icon theme: {entry.name}") logger.info("Loading icon theme '%s'", theme) @@ -722,14 +721,14 @@ class GuiIcons: except Exception: logger.error("Could not read file: %s", entry.path) logException() - return False + return # Populate generated icons cache CONFIG.splashMessage("Generating additional icons ...") self.getHeaderDecoration(0) self.getHeaderDecorationNarrow(0) - return True + return ## # Access Functions diff --git a/tests/test_gui/test_gui_sidebar.py b/tests/test_gui/test_gui_sidebar.py new file mode 100644 index 00000000..45e372db --- /dev/null +++ b/tests/test_gui/test_gui_sidebar.py @@ -0,0 +1,53 @@ +""" +novelWriter – Side Bar Class Tester +=================================== + +This file is a part of novelWriter +Copyright (C) 2025 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 . +""" +from __future__ import annotations + +import pytest + +from novelwriter import CONFIG +from novelwriter.constants import nwLabels +from novelwriter.enum import nwTheme + + +@pytest.mark.gui +def testGuiSideBar_CycleColourTheme(nwGUI): + """Test theme cycle feature on the side bar.""" + CONFIG.themeMode = nwTheme.AUTO + sidebar = nwGUI.sideBar + sidebar.mainGui.checkThemeUpdate = lambda *a: None + + # Run 3 Cycles + for _ in range(3): + + # Cycle Light + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.LIGHT + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT] + + # Cycle Dark + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.DARK + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK] + + # Cycle Auto + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.AUTO + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO] diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 84ee736e..107fd125 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -20,99 +20,31 @@ along with this program. If not, see . """ from __future__ import annotations -import sys - from configparser import ConfigParser from pathlib import Path +from unittest.mock import MagicMock, Mock import pytest -from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints -from novelwriter import CONFIG, SHARED -from novelwriter.config import DEF_GUI_LIGHT +from novelwriter import CONFIG +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS from novelwriter.constants import nwLabels -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType -from novelwriter.gui.theme import _listContent +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme +from novelwriter.gui.theme import ( + STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme, + ThemeMeta, _listContent +) from tests.mocked import causeOSError -from tests.tools import writeFile @pytest.mark.gui -def testGuiTheme_Main(qtbot, nwGUI, tstPaths): - """Test the theme class init.""" - theme = SHARED.theme - - # Methods - # ======= - - mSize = theme.getTextWidth("m") - assert mSize > 0 - assert theme.getTextWidth("m", theme.guiFont) == mSize - - # Scan for Themes - # =============== - - result = [] - _listContent(result, Path("not_a_path"), ".conf") - assert result == [] - - themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" - themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" - writeFile(themeOne, "# Stuff") - writeFile(themeTwo, "# Stuff") - - _listContent(result, tstPaths.cnfDir / "themes", ".conf") - assert result == [themeOne, themeTwo] - - # Parse Colours - # ============= - - parser = ConfigParser() - parser["Palette"] = { - "colour1": "100, 150, 200", # Valid - "colour2": "100, 150, 200, 250", # With alpha - "colour3": "100, 150, 200, 250, 300", # Too many values - "colour4": "250, 250", # Missing blue - "colour5": "-10, 127, 300", # Invalid red and blue - "colour6": "bob, 127, 255", # Invalid red - } - - # Test the parser for several valid and invalid values - assert theme._readColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) - assert theme._readColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) - assert theme._readColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) - assert theme._readColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) - assert theme._readColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) - assert theme._readColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) - - # The palette should load with the parsed values - theme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255) - theme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - theme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - theme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255) - theme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) - theme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) - - # Non-existing value should return default colour - theme._setBaseColor("default", QColor(64, 64, 64, 255)) - theme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (64, 64, 64, 255) - - # qtbot.stop() - - -@pytest.mark.gui -def testGuiTheme_ParseColor(qtbot, nwGUI): +def testGuiTheme_ParseColor(): """Test the colour parsing.""" - theme = SHARED.theme + theme = GuiTheme() # Pre-Populate theme._qColors["red"] = QColor(255, 0, 0) @@ -144,258 +76,333 @@ def testGuiTheme_ParseColor(qtbot, nwGUI): @pytest.mark.gui -@pytest.mark.skip -def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): - """Test the theme part of the class.""" - theme = SHARED.theme +def testGuiTheme_ScanThemes(monkeypatch): + """Test the themes scanning.""" + theme = GuiTheme() - # List Themes + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr(ConfigParser, "read", causeOSError) + theme._scanThemes(files) + assert theme.colourThemes == {} + + # Read all themes correctly + theme._scanThemes(files) + assert len(theme.colourThemes) > 0 + + dark = theme.colourThemes[DEF_GUI_DARK] + light = theme.colourThemes[DEF_GUI_LIGHT] + + assert dark.name == "Default Dark Theme" + assert dark.dark is True + assert light.name == "Default Light Theme" + assert light.dark is False + + +@pytest.mark.gui +def testGuiTheme_LoadThemes(monkeypatch): + """Test loading themes.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.darkTheme = DEF_GUI_DARK + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + theme._scanThemes(files) + assert DEF_GUI_LIGHT in theme._allThemes + assert DEF_GUI_DARK in theme._allThemes + assert theme._currentTheme == "" + + # Load light theme + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Load dark theme + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Let auto switch back to light, then dark + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Error Cases # =========== - # Block the reading of the files + # Invalid light theme + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Invalid dark theme + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Clear meta and check exit early cases + theme._meta = ThemeMeta() + assert theme._meta.name == "" + + # Reload dark should not load anything + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._meta.name == "" + + # Force reload, but fail parsing with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - theme._themeList = [] - assert theme.getColourThemes() == [] + mp.setattr(ConfigParser, "read", causeOSError) + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Load the theme info, default themes first - themesList = theme.getColourThemes() - assert themesList[0] == ("default_dark", "Default Dark Theme") - assert themesList[1] == ("default_light", "Default Light Theme") - assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night") - assert themesList[3] == ("dracula", "Dracula") + # Invalid theme, and defaults are missing + del theme._allThemes[DEF_GUI_DARK] + del theme._allThemes[DEF_GUI_LIGHT] - # A second call should returned the cached list - assert theme.getColourThemes() == theme._themeList + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of broken theme settings - CONFIG.guiTheme = "not_a_theme" - availThemes = theme._availThemes - theme._availThemes = {} - assert theme.loadTheme() is False - theme._availThemes = availThemes + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of unreadable file - CONFIG.guiTheme = DEF_GUI_LIGHT - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert theme.loadTheme() is False - # Load Default Theme - # ================== +@pytest.mark.gui +def testGuiTheme_SpecialColors(tstPaths): + """Test handling special cases for colours.""" + theme = GuiTheme() + theme.iconCache = MagicMock() - if sys.platform != "win32": - # Set a mock colour for the window background - theme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) - - # Load the default theme - CONFIG.guiTheme = DEF_GUI_LIGHT - assert theme.loadTheme() is True - - # This should load a standard palette - wCol = QPalette().color(QPalette.ColorRole.Window).getRgb() - assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol - - # Mock Dark Theme - # =============== - - mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" - mockTheme.write_text( + testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" + testTheme.write_text( "[Main]\n" "name = Test\n" + "mode = light\n" + "\n" + "[Base]\n" + "default = #cccccc\n" + "faded = #949494\n" + "red = #ff0000\n" + "orange = #ff7f00\n" + "yellow = #ffff00\n" + "green = #00ff00\n" + "cyan = #00ffff\n" + "blue = #0000ff\n" + "purple = #ff00ff\n" + "\n" + "[Project]\n" + "root = blue\n" + "folder = yellow\n" + "file = default\n" + "title = green\n" + "chapter = red\n" + "scene = blue\n" + "note = yellow\n" "\n" "[Palette]\n" - "window = 0, 0, 0\n" - "text = 255, 255, 255\n" + "window = #000000\n" + "text = #ffffff\n" ) - theme._availThemes["test"] = mockTheme + theme._scanThemes([testTheme]) + assert len(theme.colourThemes) == 1 + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.lightTheme = "test" - CONFIG.guiTheme = "test" - assert theme.loadTheme() is True - assert theme._guiPalette.window().color().getRgb() == (0, 0, 0, 255) - assert theme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) + # Load theme + theme.loadTheme() + + # Since window is black, a lighter version should be generated assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) - assert theme.isDarkTheme is True - # Load Default Light Theme - # ======================== + # Reload with project override to red + CONFIG.iconColTree = "red" + CONFIG.iconColDocs = True + theme.loadTheme(force=True) - CONFIG.guiTheme = "default_light" - assert theme.loadTheme() is True + assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255) + assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255) - # Check a few values - assert theme._guiPalette.color( - QPalette.ColorRole.Window - ).getRgb() == (239, 239, 239, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.WindowText - ).getRgb() == (0, 0, 0, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.Base - ).getRgb() == (255, 255, 255, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.AlternateBase - ).getRgb() == (224, 224, 224, 255) - - # Load Default Dark Theme - # ======================= - - CONFIG.guiTheme = "default_dark" - assert theme.loadTheme() is True - - # Check a few values - assert theme._guiPalette.color( - QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255) - assert theme._guiPalette.color( - QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255) - - # qtbot.stop() - - -@pytest.mark.skip -@pytest.mark.gui -def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): - """Test the syntax part of the class.""" - mainTheme = SHARED.theme - - # List Themes - # =========== - - # Block the reading of the files - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.listThemes() == [] - - # Load the syntax info - syntaxList = mainTheme.listSyntax() - assert syntaxList[0] == ("default_dark", "Default Dark") - assert syntaxList[1] == ("default_light", "Default Light") - - # A second call should returned the cached list - assert mainTheme.listSyntax() == mainTheme._syntaxList - - # Check handling of broken theme settings - availSyntax = mainTheme._availSyntax - mainTheme._availSyntax = {} - CONFIG.guiSyntax = "not_a_syntax" - assert mainTheme.loadSyntax() is False - mainTheme._availSyntax = availSyntax - - # Check handling of unreadable file - CONFIG.guiSyntax = "default_light" - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.loadSyntax() is False - - # Load Default Light Syntax - # ========================= - - # Load the default syntax - CONFIG.guiSyntax = "default_light" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Light" - assert mainTheme.syntaxTheme.back == QColor(255, 255, 255) - assert mainTheme.syntaxTheme.text == QColor(0, 0, 0) - assert mainTheme.syntaxTheme.link == QColor(0, 0, 200) - - # Load Default Dark Theme - # ======================= - - # Load the default syntax - CONFIG.guiSyntax = "default_dark" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Dark" - assert mainTheme.syntaxTheme.back == QColor(42, 42, 42) - assert mainTheme.syntaxTheme.text == QColor(204, 204, 204) - assert mainTheme.syntaxTheme.link == QColor(102, 153, 204) - - # qtbot.stop() - - -@pytest.mark.skip -@pytest.mark.gui -def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): - """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - - # Load Theme - # ========== - - # Invalid theme name - availThemes = iconCache._availThemes - iconCache._availThemes = {} - assert iconCache.loadTheme("not_a_theme") is False - iconCache._availThemes = availThemes - - # Check handling of unreadable file - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.loadTheme("material_rounded_normal") is False - - # Load working theme file - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium" - - # Load with project colour override - purple = iconCache._svgColors["purple"] - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["folder"] != purple - assert iconCache._svgColors["file"] != purple - assert iconCache._svgColors["title"] != purple - assert iconCache._svgColors["chapter"] != purple - assert iconCache._svgColors["scene"] != purple - assert iconCache._svgColors["note"] != purple + assert theme.getRawBaseColor("root") == b"#ff0000" + assert theme.getRawBaseColor("folder") == b"#ff0000" + assert theme.getRawBaseColor("file") == b"#cccccc" + assert theme.getRawBaseColor("title") == b"#00ff00" + assert theme.getRawBaseColor("chapter") == b"#ff0000" + assert theme.getRawBaseColor("scene") == b"#0000ff" + assert theme.getRawBaseColor("note") == b"#ffff00" + # Reload with project override to purple, also for docs CONFIG.iconColTree = "purple" - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache._svgColors["root"] == purple - assert iconCache._svgColors["folder"] == purple - assert iconCache._svgColors["file"] == purple - assert iconCache._svgColors["title"] == purple - assert iconCache._svgColors["chapter"] == purple - assert iconCache._svgColors["scene"] == purple - assert iconCache._svgColors["note"] == purple + CONFIG.iconColDocs = False + theme.loadTheme(force=True) - # Change some colours - iconCache.setIconColor("root", QColor(255, 255, 255)) - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["root"] == b"#ffffff" + assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255) - # List Themes - # =========== - - # Load error returns empty list - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - themes = iconCache.getIconThemes() - assert themes == [] - - # Successful read - themes = iconCache.getIconThemes() - assert len(themes) > 1 - assert "material_rounded_normal" in dict(themes) - - # Load error doesn't matter on second read since list is cached - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.getIconThemes() == themes - - # qtbot.stop() + assert theme.getRawBaseColor("root") == b"#ff00ff" + assert theme.getRawBaseColor("folder") == b"#ff00ff" + assert theme.getRawBaseColor("file") == b"#ff00ff" + assert theme.getRawBaseColor("title") == b"#ff00ff" + assert theme.getRawBaseColor("chapter") == b"#ff00ff" + assert theme.getRawBaseColor("scene") == b"#ff00ff" + assert theme.getRawBaseColor("note") == b"#ff00ff" @pytest.mark.gui -def testGuiTheme_LoadIcons(qtbot, nwGUI): +def testGuiTheme_Methods(monkeypatch): + """Test other themes methods.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + + # Init theme + assert theme.colourThemes == {} + theme.initThemes() + assert theme._meta.name == "Default Dark Theme" + + # Text width + theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) + assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM") + font = QFont(theme.guiFont) + font.setPointSizeF(0.5*font.pointSizeF()) + assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM") + + # Detect desktop mode Qt 6.5+ + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + assert theme.isDesktopDarkMode() is True + + # Detect desktop mode Qt 6.4 + mockWhite = Mock() + mockWhite.color.return_value = QColor(255, 255, 255) + + mockBlack = Mock() + mockBlack.color.return_value = QColor(0, 0, 0) + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060400) + + mp.setattr(QPalette, "window", lambda *a: mockWhite) + mp.setattr(QPalette, "windowText", lambda *a: mockBlack) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QPalette, "window", lambda *a: mockBlack) + mp.setattr(QPalette, "windowText", lambda *a: mockWhite) + assert theme.isDesktopDarkMode() is True + + # Stylesheets + assert theme.getStyleSheet(STYLES_FLAT_TABS) != "" + assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != "" + assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != "" + assert theme.getStyleSheet("stuff") == "" + + +@pytest.mark.gui +def testGuiTheme_ScanIcons(monkeypatch): + """Test the icon theme scanning.""" + theme = GuiTheme() + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("icons"), ".icons") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache._scanThemes(files) + assert theme.iconCache.iconThemes == {} + + # Read all themes correctly + theme.iconCache._scanThemes(files) + assert len(theme.iconCache.iconThemes) > 0 + + +@pytest.mark.gui +def testGuiTheme_IconThemes(monkeypatch): + """Test loading icon theme.""" + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.iconTheme = DEF_ICONS + + theme = GuiTheme() + + # Init should load default theme + theme.initThemes() + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + assert DEF_ICONS in theme.iconCache.iconThemes + + # Load default theme directly + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + + # Failed loading should load nothing + theme.iconCache._meta = ThemeMeta() + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "" + + # Reload with non-existent theme should reload default + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium" + + # If default theme is missing, load nothing + del theme.iconCache._allThemes[DEF_ICONS] + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "" + + +@pytest.mark.gui +def testGuiTheme_LoadIcons(): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Icons # ========== @@ -493,14 +500,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI): nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" ) == iconCache._noIcon - # qtbot.stop() - @pytest.mark.gui -def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): +def testGuiTheme_LoadDecorations(monkeypatch): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Decorations # ================ @@ -557,5 +563,3 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4] assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5] - - # qtbot.stop()