Add tests for sidebar and themes

This commit is contained in:
Veronica Berglyd Olsen
2025-06-03 22:36:53 +02:00
parent 5fee3ecdb8
commit bd2353f7bc
3 changed files with 389 additions and 333 deletions
+21 -22
View File
@@ -240,8 +240,8 @@ class GuiTheme:
return hint.colorScheme() == Qt.ColorScheme.Dark return hint.colorScheme() == Qt.ColorScheme.Dark
palette = QPalette() palette = QPalette()
text = palette.color(QPalette.ColorRole.WindowText) text = palette.windowText().color()
window = palette.color(QPalette.ColorRole.Window) window = palette.window().color()
return text.lightnessF() > window.lightnessF() return text.lightnessF() > window.lightnessF()
def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: def parseColor(self, value: str, default: QColor = QtBlack) -> QColor:
@@ -270,7 +270,7 @@ class GuiTheme:
return QColor(*result) return QColor(*result)
return default return default
def loadTheme(self, force: bool = False) -> bool: def loadTheme(self, force: bool = False) -> None:
"""Load the currently specified GUI theme.""" """Load the currently specified GUI theme."""
match CONFIG.themeMode: match CONFIG.themeMode:
case nwTheme.LIGHT: case nwTheme.LIGHT:
@@ -282,7 +282,7 @@ class GuiTheme:
theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme
if theme not in self._allThemes: 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: if darkMode:
theme = DEF_GUI_DARK theme = DEF_GUI_DARK
CONFIG.darkTheme = DEF_GUI_DARK CONFIG.darkTheme = DEF_GUI_DARK
@@ -292,23 +292,22 @@ class GuiTheme:
if theme == self._currentTheme and not force: if theme == self._currentTheme and not force:
logger.info("Theme '%s' is already loaded", theme) logger.info("Theme '%s' is already loaded", theme)
return False return
entry = self._allThemes.get(theme) entry = self._allThemes.get(theme)
if not entry: if not entry:
logger.error("Could not load GUI theme") logger.error("Could not load GUI theme")
return False return
CONFIG.splashMessage(f"Loading colour theme: {entry.name}") CONFIG.splashMessage(f"Loading colour theme: {entry.name}")
logger.info("Loading GUI theme '%s'", theme) logger.info("Loading GUI theme '%s'", theme)
parser = ConfigParser() parser = ConfigParser()
try: try:
with open(entry.path, mode="r", encoding="utf-8") as fo: parser.read(entry.path, encoding="utf-8")
parser.read_file(fo)
except Exception: except Exception:
logger.error("Could not read file: %s", entry.path) logger.error("Could not read file: %s", entry.path)
logException() logException()
return False return
# Reset Palette # Reset Palette
self._resetTheme() self._resetTheme()
@@ -458,15 +457,15 @@ class GuiTheme:
# Set project override colours # Set project override colours
if (override := CONFIG.iconColTree) != "theme": if (override := CONFIG.iconColTree) != "theme":
color = self._svgColors.get(override, b"#000000") color = self._qColors.get(override, QtBlack)
self._svgColors["root"] = color self._setBaseColor("root", color)
self._svgColors["folder"] = color self._setBaseColor("folder", color)
if not CONFIG.iconColDocs: if not CONFIG.iconColDocs:
self._svgColors["file"] = color self._setBaseColor("file", color)
self._svgColors["title"] = color self._setBaseColor("title", color)
self._svgColors["chapter"] = color self._setBaseColor("chapter", color)
self._svgColors["scene"] = color self._setBaseColor("scene", color)
self._svgColors["note"] = color self._setBaseColor("note", color)
self.isDarkTheme = darkMode self.isDarkTheme = darkMode
self._currentTheme = theme self._currentTheme = theme
@@ -478,7 +477,7 @@ class GuiTheme:
QApplication.setPalette(self._guiPalette) QApplication.setPalette(self._guiPalette)
self._buildStyleSheets(self._guiPalette) self._buildStyleSheets(self._guiPalette)
return True return
def getStyleSheet(self, name: str) -> str: def getStyleSheet(self, name: str) -> str:
"""Load a standard style sheet.""" """Load a standard style sheet."""
@@ -685,7 +684,7 @@ class GuiIcons:
self._scanThemes(icons) self._scanThemes(icons)
return 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 """Update the theme map. This is more of an init, since many of
the GUI icons cannot really be replaced without writing specific the GUI icons cannot really be replaced without writing specific
update functions for the classes where they're used. update functions for the classes where they're used.
@@ -698,7 +697,7 @@ class GuiIcons:
entry = self._allThemes.get(theme) entry = self._allThemes.get(theme)
if not entry: if not entry:
logger.error("Could not load icon theme") logger.error("Could not load icon theme")
return False return
CONFIG.splashMessage(f"Loading icon theme: {entry.name}") CONFIG.splashMessage(f"Loading icon theme: {entry.name}")
logger.info("Loading icon theme '%s'", theme) logger.info("Loading icon theme '%s'", theme)
@@ -722,14 +721,14 @@ class GuiIcons:
except Exception: except Exception:
logger.error("Could not read file: %s", entry.path) logger.error("Could not read file: %s", entry.path)
logException() logException()
return False return
# Populate generated icons cache # Populate generated icons cache
CONFIG.splashMessage("Generating additional icons ...") CONFIG.splashMessage("Generating additional icons ...")
self.getHeaderDecoration(0) self.getHeaderDecoration(0)
self.getHeaderDecorationNarrow(0) self.getHeaderDecorationNarrow(0)
return True return
## ##
# Access Functions # Access Functions
+53
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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]
+315 -311
View File
@@ -20,99 +20,31 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import sys
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, Mock
import pytest 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 import CONFIG
from novelwriter.config import DEF_GUI_LIGHT from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
from novelwriter.gui.theme import _listContent from novelwriter.gui.theme import (
STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme,
ThemeMeta, _listContent
)
from tests.mocked import causeOSError from tests.mocked import causeOSError
from tests.tools import writeFile
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, tstPaths): def testGuiTheme_ParseColor():
"""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):
"""Test the colour parsing.""" """Test the colour parsing."""
theme = SHARED.theme theme = GuiTheme()
# Pre-Populate # Pre-Populate
theme._qColors["red"] = QColor(255, 0, 0) theme._qColors["red"] = QColor(255, 0, 0)
@@ -144,258 +76,333 @@ def testGuiTheme_ParseColor(qtbot, nwGUI):
@pytest.mark.gui @pytest.mark.gui
@pytest.mark.skip def testGuiTheme_ScanThemes(monkeypatch):
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): """Test the themes scanning."""
"""Test the theme part of the class.""" theme = GuiTheme()
theme = SHARED.theme
# 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: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr(ConfigParser, "read", causeOSError)
theme._themeList = [] CONFIG.darkTheme = DEF_GUI_DARK
assert theme.getColourThemes() == [] CONFIG.themeMode = nwTheme.DARK
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Load the theme info, default themes first # Invalid theme, and defaults are missing
themesList = theme.getColourThemes() del theme._allThemes[DEF_GUI_DARK]
assert themesList[0] == ("default_dark", "Default Dark Theme") del theme._allThemes[DEF_GUI_LIGHT]
assert themesList[1] == ("default_light", "Default Light Theme")
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
assert themesList[3] == ("dracula", "Dracula")
# A second call should returned the cached list CONFIG.lightTheme = "not_a_theme"
assert theme.getColourThemes() == theme._themeList CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Check handling of broken theme settings CONFIG.darkTheme = "not_a_theme"
CONFIG.guiTheme = "not_a_theme" CONFIG.themeMode = nwTheme.DARK
availThemes = theme._availThemes theme.loadTheme(force=True)
theme._availThemes = {} assert theme._meta.name == ""
assert theme.loadTheme() is False
theme._availThemes = availThemes
# 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": testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
# Set a mock colour for the window background testTheme.write_text(
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(
"[Main]\n" "[Main]\n"
"name = Test\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" "\n"
"[Palette]\n" "[Palette]\n"
"window = 0, 0, 0\n" "window = #000000\n"
"text = 255, 255, 255\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" # Load theme
assert theme.loadTheme() is True theme.loadTheme()
assert theme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
assert theme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) # Since window is black, a lighter version should be generated
assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) 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.getBaseColor("root").getRgb() == (255, 0, 0, 255)
assert theme.loadTheme() is True 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.getRawBaseColor("root") == b"#ff0000"
assert theme._guiPalette.color( assert theme.getRawBaseColor("folder") == b"#ff0000"
QPalette.ColorRole.Window assert theme.getRawBaseColor("file") == b"#cccccc"
).getRgb() == (239, 239, 239, 255) assert theme.getRawBaseColor("title") == b"#00ff00"
assert theme._guiPalette.color( assert theme.getRawBaseColor("chapter") == b"#ff0000"
QPalette.ColorRole.WindowText assert theme.getRawBaseColor("scene") == b"#0000ff"
).getRgb() == (0, 0, 0, 255) assert theme.getRawBaseColor("note") == b"#ffff00"
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
# Reload with project override to purple, also for docs
CONFIG.iconColTree = "purple" CONFIG.iconColTree = "purple"
assert iconCache.loadTheme("material_rounded_normal") is True CONFIG.iconColDocs = False
assert iconCache._svgColors["root"] == purple theme.loadTheme(force=True)
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
# Change some colours assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255)
iconCache.setIconColor("root", QColor(255, 255, 255)) assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255)
assert iconCache._svgColors["root"] != purple assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255)
assert iconCache._svgColors["root"] == b"#ffffff" 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 assert theme.getRawBaseColor("root") == b"#ff00ff"
# =========== assert theme.getRawBaseColor("folder") == b"#ff00ff"
assert theme.getRawBaseColor("file") == b"#ff00ff"
# Load error returns empty list assert theme.getRawBaseColor("title") == b"#ff00ff"
with monkeypatch.context() as mp: assert theme.getRawBaseColor("chapter") == b"#ff00ff"
mp.setattr("builtins.open", causeOSError) assert theme.getRawBaseColor("scene") == b"#ff00ff"
themes = iconCache.getIconThemes() assert theme.getRawBaseColor("note") == b"#ff00ff"
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()
@pytest.mark.gui @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.""" """Test the icon cache class."""
iconCache = SHARED.theme.iconCache theme = GuiTheme()
assert iconCache.loadTheme("material_rounded_normal") is True theme.initThemes()
iconCache = theme.iconCache
# Load Icons # Load Icons
# ========== # ==========
@@ -493,14 +500,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI):
nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
) == iconCache._noIcon ) == iconCache._noIcon
# qtbot.stop()
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): def testGuiTheme_LoadDecorations(monkeypatch):
"""Test the icon cache class.""" """Test the icon cache class."""
iconCache = SHARED.theme.iconCache theme = GuiTheme()
assert iconCache.loadTheme("material_rounded_normal") is True theme.initThemes()
iconCache = theme.iconCache
# Load Decorations # Load Decorations
# ================ # ================
@@ -557,5 +563,3 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4] assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4]
assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5]
assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5]
# qtbot.stop()