Files
UmbraEdit/tests/dialogs/test_preferences.py
B.D. Murphy 026f486b3e
Some checks failed
Syntax / Linting and Type Checking (push) Has been cancelled
Tests / Linux Python 3.11 (push) Has been cancelled
Tests / Linux Python 3.12 (push) Has been cancelled
Tests / Linux Python 3.13 (push) Has been cancelled
Tests / Linux Python 3.14 (push) Has been cancelled
Tests / Linux Python 3.15 (push) Has been cancelled
Tests / MacOS Python 3.14 (push) Has been cancelled
Tests / Windows Python 3.14 (push) Has been cancelled
Tests / PR Coverage Report (push) Has been cancelled
Tests / Action Coverage Report (push) Has been cancelled
Tests / Ubuntu 24.04 & Sys Packages (push) Has been cancelled
Tests / Fedora 44 & Sys Packages (push) Has been cancelled
chore: fix test fixtures and asset names for UmbraEdit rename
- Rename test fixture files: config_novelwriter.toml -> config_umbraedit.toml
- Rename test fixture files: old_novelwriter.conf -> old_umbraedit.conf
- Rename test fixture files: nw_en_GB.qm -> ue_en_GB.qm
- Rename asset files: novelwriter-text-*.svg -> umbraedit-text-*.svg
- Rename asset files: novelwriter.svg/ico -> umbraedit.svg/ico
- Rename asset files: x-novelwriter-project.* -> x-umbraedit-project.*
- Fix test references to _nwLangPath -> _ueLangPath
- Bump version to 1.0.0 (first UmbraEdit release)
- All 735 tests pass, 2 skipped
2026-11-05 07:36:45 -08:00

543 lines
19 KiB
Python

"""
UmbraEdit - Preferences Dialog Test
=====================================
This file is a part of UmbraEdit.
Copyright (C) 2020 Veronica Berglyd Olsen and UmbraEdit 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
from pathlib import Path
import pytest
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
from PyQt6.QtWidgets import QFileDialog
from pytestqt.qtbot import QtBot
from umbraedit import CONFIG, SHARED
from umbraedit.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_TREECOL
from umbraedit.constants import nwUnicode
from umbraedit.core.spellcheck import SpellEnchant
from umbraedit.dialogs.preferences import GuiNeedsUpdate, GuiPreferences
from umbraedit.dialogs.quotes import GuiQuoteSelect
from umbraedit.extensions.modified import NFontDialog
from umbraedit.gui.theme import ThemeEntry
from umbraedit.guimain import GuiMain
from umbraedit.types import QtKeyEscape, QtModNone
from tests.helpers import TstPaths, checkDialogFreedOnClose
KEY_DELAY = 1
@pytest.mark.gui
def testGuiPreferences_Main(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain, tstPaths: TstPaths):
"""Test the preferences dialog loading."""
monkeypatch.setattr(SpellEnchant, "listDictionaries", lambda *a: [("en", "English [en]")])
monkeypatch.setattr(GuiPreferences, "exec", lambda *a: None)
# Load GUI with standard values
nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger)
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiPreferences) is not None, timeout=1000)
prefs = SHARED.findTopLevelWidget(GuiPreferences)
assert isinstance(prefs, GuiPreferences)
prefs.show()
# Check Languages
languages = [prefs.guiLocale.itemData(i) for i in range(prefs.guiLocale.count())]
assert len(languages) > 0
assert "en_GB" in languages
# Check GUI Themes
themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())]
assert DEF_GUI_LIGHT in themes
themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())]
assert DEF_GUI_DARK in themes
# Check Spell Checking
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
assert len(spelling) == 1
assert spelling == ["en"]
prefs.close()
# Check Fallback Values
CONFIG.hasEnchant = False
prefs = GuiPreferences(nwGUI)
prefs.show()
# Check Spell Checking
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
assert len(spelling) == 1
assert spelling == [""]
prefs.close()
# qtbot.stop()
@pytest.mark.gui
def testGuiPreferences_Actions(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain):
"""Test the preferences dialog actions."""
monkeypatch.setattr(SpellEnchant, "listDictionaries", lambda *a: [("en", "English [en]")])
prefs = GuiPreferences(nwGUI)
with qtbot.waitExposed(prefs):
prefs.show()
# Check Navigation
vBar = prefs.mainForm.verticalScrollBar()
assert vBar is not None
old = -1
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(1).click()
assert value.args is not None
assert value.args[0] > old
old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(2).click()
assert value.args is not None
assert value.args[0] > old
old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(3).click()
assert value.args is not None
assert value.args[0] > old
old = value.args[0]
# Check Search
prefs.searchText.setText("Display language")
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs._gotoSearch()
assert value.args is not None
assert value.args[0] < old
# Check Save Button
prefs.show()
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
prefs.btnSave.click()
assert signal.args is not None
assert len(signal.args) == 1
assert isinstance(signal.args[0], GuiNeedsUpdate)
# Check Close Button
prefs.show()
prefs.btnCancel.click()
assert prefs.isHidden() is True
# Close Using Escape Key
prefs.show()
event = QKeyEvent(QEvent.Type.KeyPress, QtKeyEscape, QtModNone)
prefs.keyPressEvent(event)
assert prefs.isHidden() is True
# Other keys do not close the dialog
prefs.show()
event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_A, QtModNone)
prefs.keyPressEvent(event)
assert prefs.isHidden() is False
# qtbot.stop()
@pytest.mark.gui
def testGuiPreferences_Settings(
qtbot: QtBot,
monkeypatch: pytest.MonkeyPatch,
nwGUI: GuiMain,
fncPath: Path,
tstPaths: TstPaths,
):
"""Test the preferences dialog settings."""
spelling = [("en", "English [en]"), ("de", "Deutch [de]")]
monkeypatch.setattr(SpellEnchant, "listDictionaries", lambda *a: spelling)
(fncPath / "ue_en_US.qm").touch()
(fncPath / "project_en_US.json").touch()
CONFIG._ueLangPath = fncPath
SHARED.theme._allThemes = {
"theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath),
"theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath),
"theme3": ThemeEntry(name="Theme 3", dark=True, path=fncPath),
"theme4": ThemeEntry(name="Theme 4", dark=True, path=fncPath),
}
prefs = GuiPreferences(nwGUI)
with qtbot.waitExposed(prefs):
prefs.show()
# Appearance
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1"))
prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3"))
with monkeypatch.context() as mp:
mp.setattr(NFontDialog, "selectFont", lambda *a, **k: (QFont(), False))
prefs.guiFontButton.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(NFontDialog, "selectFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(True) # Use OS font dialog
prefs.guiFontButton.click()
prefs.guiForceRTL.setChecked(True)
prefs.hideVScroll.setChecked(True)
prefs.hideHScroll.setChecked(True)
prefs.useCharCount.setChecked(True)
prefs.incNotesWCount.setChecked(False)
assert CONFIG.guiLocale != "en_US"
assert CONFIG.guiForceRTL is False
assert CONFIG.lightTheme == "default_light"
assert CONFIG.darkTheme == "default_dark"
assert CONFIG.guiFont.family() != ""
assert CONFIG.hideVScroll is False
assert CONFIG.hideHScroll is False
assert CONFIG.useCharCount is False
assert CONFIG.incNotesWCount is True
# Document Style
with monkeypatch.context() as mp:
mp.setattr(NFontDialog, "selectFont", lambda *a, **k: (QFont(), False))
prefs.textFontButton.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(NFontDialog, "selectFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(False) # Use Qt font dialog
prefs.textFontButton.click()
prefs.showFullPath.setChecked(False)
prefs.hideFocusFooter.setChecked(True)
prefs.cleanFocusHeader.setChecked(True)
prefs.goalInFocusMode.setChecked(True)
assert CONFIG.textFont.family() != ""
assert CONFIG.showFullPath is True
assert CONFIG.hideFocusFooter is False
assert CONFIG.cleanFocusHeader is False
assert CONFIG.goalInFocusMode is False
# Project View
prefs.iconColTree.setCurrentData("faded", "default")
prefs.iconColDocs.setChecked(True)
prefs.emphLabels.setChecked(False)
assert CONFIG.iconColTree == DEF_TREECOL
assert CONFIG.iconColDocs is False
assert CONFIG.emphLabels is True
# Behaviour
prefs.autoSaveDoc.stepUp()
prefs.autoSaveProj.stepUp()
prefs.askBeforeExit.setChecked(False)
prefs.moveMainWin.setChecked(False)
assert CONFIG.autoSaveDoc == 30
assert CONFIG.autoSaveProj == 60
assert CONFIG.askBeforeExit is True
assert CONFIG.moveMainWin is True
# Project Backup
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "")
prefs.backupGetPath.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: str(tstPaths.testDir))
prefs.backupGetPath.click()
assert prefs.backupPath == str(tstPaths.testDir)
prefs.backupInterval.setCurrentData("day", "session")
prefs.backupOnClose.setChecked(True)
assert prefs.askBeforeBackup.isEnabled() is True
prefs.askBeforeBackup.setChecked(False)
assert CONFIG._backupPath != tstPaths.testDir
assert CONFIG.backupInterval == "session"
assert CONFIG.backupOnClose is False
assert CONFIG.askBeforeBackup is True
# Session Timer
prefs.stopWhenIdle.setChecked(False)
prefs.userIdleTime.stepUp()
assert CONFIG.stopWhenIdle is True
assert CONFIG.userIdleTime == 300
# Text Flow
prefs.textWidth.stepDown()
prefs.focusWidth.stepDown()
prefs.doJustify.setChecked(True)
prefs.textMargin.stepUp()
prefs.tabSize.stepUp()
prefs.lineHeight.stepUp()
assert CONFIG.textWidth == 700
assert CONFIG.focusWidth == 800
assert CONFIG.doJustify is False
assert CONFIG.textMargin == 40
assert CONFIG.tabSize == 2.0
assert CONFIG.lineHeight == 1.15
# Text Editing
prefs.spellLanguage.setCurrentIndex(prefs.spellLanguage.findData("de"))
prefs.autoSelect.setChecked(False)
prefs.cursorWidth.setValue(5)
prefs.scaleHeadings.setChecked(False)
prefs.singleStarBold.setChecked(True)
prefs.lineHighlight.setChecked(False)
assert CONFIG.spellLanguage != "de"
assert CONFIG.autoSelect is True
assert CONFIG.cursorWidth == 1
assert CONFIG.scaleHeadings is True
assert CONFIG.singleStarBold is False
assert CONFIG.lineHighlight is True
# Editor Scrolling
prefs.scrollPastEnd.setChecked(False)
prefs.autoScroll.setChecked(False)
prefs.autoScrollPos.stepUp()
assert CONFIG.scrollPastEnd is True
assert CONFIG.autoScroll is True
assert CONFIG.autoScrollPos == 30
# Text Highlighting
prefs.dialogStyle.setCurrentData(3, 0)
prefs.allowOpenDialog.setChecked(False)
prefs.dialogLine.setText(nwUnicode.U_EMDASH)
prefs.narratorBreak.setCurrentData(nwUnicode.U_EMDASH, "")
prefs.narratorDialog.setCurrentData(nwUnicode.U_EMDASH, "")
prefs.altDialogOpen.setText("%") # Symbol also tests for #2455
prefs.altDialogClose.setText("%") # Symbol also tests for #2455
prefs.highlightEmph.setChecked(False)
prefs.dottedModCodes.setChecked(True)
prefs.showMultiSpaces.setChecked(True)
actions = {a.data(): a for a in prefs.mnLineSymbols.actions()}
actions[nwUnicode.U_ENDASH].trigger()
assert prefs.dialogLine.text() == f"{nwUnicode.U_ENDASH} {nwUnicode.U_EMDASH}"
assert CONFIG.dialogStyle == 2
assert CONFIG.allowOpenDialog is True
assert CONFIG.dialogLine == ""
assert CONFIG.narratorBreak == ""
assert CONFIG.narratorDialog == ""
assert CONFIG.altDialogOpen == ""
assert CONFIG.altDialogClose == ""
assert CONFIG.highlightEmph is True
assert CONFIG.dottedModCodes is False
assert CONFIG.showMultiSpaces is False
# Text Automation
prefs.doReplaceSQuote.setChecked(False)
prefs.doReplaceDQuote.setChecked(False)
prefs.doReplaceDash.setChecked(False)
prefs.doReplaceDots.setChecked(False)
prefs.doReplace.setChecked(False)
prefs.fmtPadBefore.setText("!?:")
prefs.fmtPadAfter.setText("¡¿")
prefs.fmtPadThin.setChecked(True)
assert prefs.doReplaceSQuote.isEnabled() is False
assert prefs.doReplaceDQuote.isEnabled() is False
assert prefs.doReplaceDash.isEnabled() is False
assert prefs.doReplaceDots.isEnabled() is False
assert prefs.fmtPadThin.isEnabled() is False
assert CONFIG.doReplace is True
assert CONFIG.doReplaceSQuote is True
assert CONFIG.doReplaceDQuote is True
assert CONFIG.doReplaceDash is True
assert CONFIG.doReplaceDots is True
assert CONFIG.fmtPadBefore == ""
assert CONFIG.fmtPadAfter == ""
assert CONFIG.fmtPadThin is False
# Quotation Style
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LSAQUO, False))
prefs.btnSQuoteOpen.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LSAQUO, True))
prefs.btnSQuoteOpen.click()
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RSAQUO, False))
prefs.btnSQuoteClose.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RSAQUO, True))
prefs.btnSQuoteClose.click()
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LAQUO, False))
prefs.btnDQuoteOpen.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LAQUO, True))
prefs.btnDQuoteOpen.click()
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RAQUO, False))
prefs.btnDQuoteClose.click() # Cancelled, so no change
with monkeypatch.context() as mp:
mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RAQUO, True))
prefs.btnDQuoteClose.click()
assert CONFIG.fmtSQuoteOpen == nwUnicode.U_LSQUO
assert CONFIG.fmtSQuoteClose == nwUnicode.U_RSQUO
assert CONFIG.fmtDQuoteOpen == nwUnicode.U_LDQUO
assert CONFIG.fmtDQuoteClose == nwUnicode.U_RDQUO
# Features
assert CONFIG.vimMode is False
prefs.vimMode.setChecked(True)
# Advanced
assert CONFIG.forceUnixLineEndings is False
prefs.forceUnixLineEndings.setChecked(True)
# Save Settings
with monkeypatch.context() as mp:
mp.setattr(QFontDatabase, "families", lambda *a: ["TestFont"])
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
prefs.btnSave.click()
assert signal.args == [
GuiNeedsUpdate(
restart=True,
tree=True,
theme=True,
syntax=True,
spelling=True,
vim=True,
editor=True,
viewport=True,
viewer=True,
)
]
# Check Settings
# ==============
# Appearance
assert CONFIG.guiLocale == "en_US"
assert CONFIG.lightTheme == "theme1"
assert CONFIG.darkTheme == "theme3"
assert CONFIG.guiFont == QFont()
assert CONFIG.guiForceRTL is True
assert CONFIG.hideVScroll is True
assert CONFIG.hideHScroll is True
assert CONFIG.useCharCount is True
assert CONFIG.incNotesWCount is False
# Document Style
assert CONFIG.textFont == QFont()
assert CONFIG.showFullPath is False
assert CONFIG.hideFocusFooter is True
assert CONFIG.cleanFocusHeader is True
assert CONFIG.goalInFocusMode is True
# Project View
assert CONFIG.iconColTree == "faded"
assert CONFIG.iconColDocs is True
assert CONFIG.emphLabels is False
# Behaviour
assert CONFIG.autoSaveDoc == 31
assert CONFIG.autoSaveProj == 61
assert CONFIG.askBeforeExit is False
assert CONFIG.moveMainWin is False
# Project Backup
assert CONFIG._backupPath == tstPaths.testDir
assert CONFIG.backupInterval == "day"
assert CONFIG.backupOnClose is True
assert CONFIG.askBeforeBackup is False
# Session Timer
assert CONFIG.stopWhenIdle is False
assert CONFIG.userIdleTime == 330
# Text Flow
assert CONFIG.textWidth == 690
assert CONFIG.focusWidth == 790
assert CONFIG.doJustify is True
assert CONFIG.textMargin == 41
assert CONFIG.tabSize == 2.1
assert CONFIG.lineHeight == 1.2
# Text Editing
assert CONFIG.spellLanguage == "de"
assert CONFIG.autoSelect is False
assert CONFIG.cursorWidth == 5
assert CONFIG.scaleHeadings is False
assert CONFIG.singleStarBold is True
assert CONFIG.lineHighlight is False
# Editor Scrolling
assert CONFIG.scrollPastEnd is False
assert CONFIG.autoScroll is False
assert CONFIG.autoScrollPos == 31
# Text Highlighting
assert CONFIG.dialogStyle == 3
assert CONFIG.allowOpenDialog is False
assert CONFIG.dialogLine == f"{nwUnicode.U_ENDASH}{nwUnicode.U_EMDASH}"
assert CONFIG.narratorBreak == nwUnicode.U_EMDASH
assert CONFIG.narratorDialog == nwUnicode.U_EMDASH
assert CONFIG.altDialogOpen == "%"
assert CONFIG.altDialogClose == "%"
assert CONFIG.highlightEmph is False
assert CONFIG.dottedModCodes is True
assert CONFIG.showMultiSpaces is True
# Text Automation
assert CONFIG.doReplace is False
assert CONFIG.doReplaceSQuote is False
assert CONFIG.doReplaceDQuote is False
assert CONFIG.doReplaceDash is False
assert CONFIG.doReplaceDots is False
assert CONFIG.fmtPadBefore == "!:?"
assert CONFIG.fmtPadAfter == "¡¿"
assert CONFIG.fmtPadThin is True
# Quotation Style
assert CONFIG.fmtSQuoteOpen == nwUnicode.U_LSAQUO
assert CONFIG.fmtSQuoteClose == nwUnicode.U_RSAQUO
assert CONFIG.fmtDQuoteOpen == nwUnicode.U_LAQUO
assert CONFIG.fmtDQuoteClose == nwUnicode.U_RAQUO
# Features
assert CONFIG.vimMode is True # Note that the question box answers "yes" in tests
assert SHARED.lastAlert == [
"Are you sure you want to enable Vim mode?",
"This changes how the editor accepts input.",
]
# Advanced
assert CONFIG.forceUnixLineEndings is True
# qtbot.stop()
@pytest.mark.gui
def testGuiPreferences_MemoryLeakRegression(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain):
"""Test that the dialog is freed when it is closed."""
monkeypatch.setattr(SpellEnchant, "listDictionaries", lambda *a: [("en", "English [en]")])
checkDialogFreedOnClose(qtbot, lambda: GuiPreferences(nwGUI))