Files
novelWriter/tests/test_dialogs/test_dlg_projectsettings.py
T
Veronica Berglyd Olsen 5bdbd7c195 Update linting for tests
2025-08-27 21:04:32 +02:00

468 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Project Settings Dialog Class Tester
==================================================
This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" # noqa
from __future__ import annotations
import pytest
from PyQt6.QtGui import QAction, QColor
from PyQt6.QtWidgets import QColorDialog, QFileDialog
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.enum import nwItemType, nwStatusShape
from novelwriter.types import QtAccepted, QtMouseLeft
from tests.mocked import causeOSError
from tests.tools import C, buildTestProject
KEY_DELAY = 1
@pytest.mark.gui
def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
"""Test the main dialog class. Saving settings is not tested in this
test, but are instead tested in the individual tab tests.
"""
# Block the GUI blocking thread
monkeypatch.setattr(GuiProjectSettings, "exec", lambda *a: None)
monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QtAccepted)
# Check that we cannot open when there is no project
nwGUI.mainMenu.aProjectSettings.activate(QAction.ActionEvent.Trigger)
assert SHARED.findTopLevelWidget(GuiProjectSettings) is None
# Pretend we have a project
SHARED.project._valid = True
SHARED.project.data.setSpellLang("en")
# Get the dialog object
nwGUI.mainMenu.aProjectSettings.activate(QAction.ActionEvent.Trigger)
qtbot.waitUntil(
lambda: SHARED.findTopLevelWidget(GuiProjectSettings) is not None, timeout=1000
)
projSettings = SHARED.findTopLevelWidget(GuiProjectSettings)
assert isinstance(projSettings, GuiProjectSettings)
projSettings.show()
qtbot.addWidget(projSettings)
# Switch Tabs
projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click()
assert projSettings.mainStack.currentWidget() == projSettings.settingsPage
projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click()
assert projSettings.mainStack.currentWidget() == projSettings.statusPage
projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click()
assert projSettings.mainStack.currentWidget() == projSettings.importPage
projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click()
assert projSettings.mainStack.currentWidget() == projSettings.replacePage
# Clean Up
projSettings.close()
# qtbot.stop()
@pytest.mark.gui
def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
"""Test the settings page of the dialog."""
languages = [("en", "English"), ("de", "German")]
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages)
(fncPath / "nw_en.qm").touch()
(fncPath / "nw_de.qm").touch()
(fncPath / "project_en.json").touch()
(fncPath / "project_de.json").touch()
CONFIG._nwLangPath = fncPath
# Create new project
buildTestProject(nwGUI, projPath)
mockRnd.reset()
CONFIG.setBackupPath(fncPath)
# Set some values
project = SHARED.project
project.data.setLanguage("en")
project.data.setSpellLang("en")
project.data.setAuthor("Jane Smith")
project.data.setAutoReplace({"A": "B", "C": "D"})
# Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS)
projSettings.show()
qtbot.addWidget(projSettings)
# Settings Tab
settings = projSettings.settingsPage
assert settings.projName.text() == "New Project"
assert settings.projAuthor.text() == "Jane Smith"
assert settings.projLang.currentData() == "en"
assert settings.spellLang.currentData() == "en"
assert settings.noBackup.isChecked() is False
settings.projName.setText("Project Name")
settings.projAuthor.setText("Jane Doe")
settings.projLang.setCurrentIndex(settings.projLang.findData("de"))
settings.spellLang.setCurrentIndex(settings.spellLang.findData("de"))
settings.noBackup.setChecked(True)
projSettings._doSave()
assert project.data.name == "Project Name"
assert project.data.author == "Jane Doe"
assert project.data.language == "de"
assert project.data.spellLang == "de"
assert project.data.doBackup is False
nwGUI._processProjectSettingsChanges()
assert nwGUI.windowTitle() == "Project Name - novelWriter"
# qtbot.stop()
@pytest.mark.gui
def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the status and importance pages of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(nwGUI, projPath)
# Set some values
project = SHARED.project
project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
nwGUI.projView.projTree.setSelectedHandle(C.hCharRoot)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
nwGUI.projView.projTree.setSelectedHandle(C.hWorldRoot)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
hPlotNote = "0000000000010"
hCharNote = "0000000000011"
hWorldNote = "0000000000012"
project.tree[hPlotNote].setImport(C.iMajor) # type: ignore
project.tree[hCharNote].setImport(C.iMajor) # type: ignore
project.tree[hWorldNote].setImport(C.iMain) # type: ignore
project.tree.refreshAllItems()
project.countStatus()
assert [e.count for _, e in project.data.itemStatus.iterItems()] == [2, 0, 2, 1]
assert [e.count for _, e in project.data.itemImport.iterItems()] == [3, 0, 2, 1]
# Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS)
projSettings.show()
qtbot.addWidget(projSettings)
# Status Tab
# ==========
status = projSettings.statusPage
assert status.changed is False
assert status.getNewList() == []
assert status.listBox.topLevelItemCount() == 4
# Can't delete the first item (it's in use)
status.listBox.clearSelection()
status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
status.delButton.click()
assert status.listBox.topLevelItemCount() == 4
# Can delete the second item
status.listBox.clearSelection()
status.listBox.setCurrentItem(status.listBox.topLevelItem(1))
status.delButton.click()
assert status.listBox.topLevelItemCount() == 3
# Add a new item
with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
status.addButton.click()
status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
status._onNameEdit("Final")
status.colorButton.click()
status._selectShape(nwStatusShape.CIRCLE)
assert status.listBox.topLevelItemCount() == 4
assert status.changed is True
update = status.getNewList()
assert update[0][0] == C.sNew
assert update[0][1].name == "New"
assert update[0][1].color.getRgb() == (108, 108, 108, 255)
assert update[0][1].shape == nwStatusShape.STAR
assert update[1][0] == C.sDraft
assert update[1][1].name == "Draft"
assert update[1][1].color.getRgb() == (163, 156, 52, 255)
assert update[1][1].shape == nwStatusShape.CIRCLE_T
assert update[2][0] == C.sFinished
assert update[2][1].name == "Finished"
assert update[2][1].color.getRgb() == (41, 102, 41, 255)
assert update[2][1].shape == nwStatusShape.STAR
assert update[3][0] is None
assert update[3][1].name == "Final"
assert update[3][1].color.getRgb() == (20, 30, 40, 255)
assert update[3][1].shape == nwStatusShape.CIRCLE
# Move items, none selected -> no change
status.listBox.clearSelection()
status._moveItem(1)
assert [x[0] for x in status.getNewList()] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Move items, first selected, move up -> no change
status.listBox.clearSelection()
status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
status._moveItem(-1)
assert [x[0] for x in status.getNewList()] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Move items, last selected, move up -> allowed
status.listBox.clearSelection()
status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
status._moveItem(-1)
assert [x[0] for x in status.getNewList()] == [
C.sNew, C.sDraft, None, C.sFinished
]
# Move items, same selected, move down -> allowed
status._moveItem(1)
assert [x[0] for x in status.getNewList()] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Importance Tab
# ==============
importance = projSettings.importPage
projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT)
# Delete unused entry
importance.listBox.clearSelection()
importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1))
importance.delButton.click()
assert importance.listBox.topLevelItemCount() == 3
# Add a new entry
with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
importance.addButton.click()
importance.listBox.clearSelection()
importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3))
importance._onNameEdit("Final")
importance.colorButton.click()
importance._selectShape(nwStatusShape.TRIANGLE)
assert importance.listBox.topLevelItemCount() == 4
assert importance.changed is True
update = importance.getNewList()
assert update[0][0] == C.iNew
assert update[0][1].name == "New"
assert update[0][1].color.getRgb() == (179, 90, 179, 255)
assert update[0][1].shape == nwStatusShape.SQUARE
assert update[1][0] == C.iMajor
assert update[1][1].name == "Major"
assert update[1][1].color.getRgb() == (179, 90, 179, 255)
assert update[1][1].shape == nwStatusShape.BLOCK_3
assert update[2][0] == C.iMain
assert update[2][1].name == "Main"
assert update[2][1].color.getRgb() == (179, 90, 179, 255)
assert update[2][1].shape == nwStatusShape.BLOCK_4
assert update[3][0] is None
assert update[3][1].name == "Final"
assert update[3][1].color.getRgb() == (20, 30, 40, 255)
assert update[3][1].shape == nwStatusShape.TRIANGLE
# Check Project
projSettings._doSave()
statusItems = dict(project.data.itemStatus.iterItems())
assert statusItems[C.sNew].name == "New"
assert statusItems[C.sDraft].name == "Draft"
assert statusItems[C.sFinished].name == "Finished"
assert statusItems["s000013"].name == "Final"
importItems = dict(project.data.itemImport.iterItems())
assert importItems[C.iNew].name == "New"
assert importItems[C.iMajor].name == "Major"
assert importItems[C.iMain].name == "Main"
assert importItems["i000014"].name == "Final"
# qtbot.stop()
@pytest.mark.gui
def testDlgProjSettings_StatusImportExport(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the status and importance import/export."""
buildTestProject(nwGUI, projPath)
# Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS)
projSettings.show()
qtbot.addWidget(projSettings)
status = projSettings.statusPage
assert status.listBox.topLevelItemCount() == 4
expFile = projPath / "status.csv"
# Export Error
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (str(expFile), ""))
mp.setattr("builtins.open", causeOSError)
status._exportLabels()
assert expFile.is_file() is False
# Export File
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (str(expFile), ""))
status._exportLabels()
assert expFile.is_file() is True
assert expFile.read_text().split() == [
"STAR,#6c6c6c,New",
"TRIANGLE,#a62a2d,Note",
"CIRCLE_T,#a39c34,Draft",
"STAR,#296629,Finished",
]
# Import Error
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(expFile), ""))
mp.setattr("builtins.open", causeOSError)
status._importLabels()
assert status.listBox.topLevelItemCount() == 4
assert status.changed is False
# Import File
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(expFile), ""))
status._importLabels()
assert status.listBox.topLevelItemCount() == 8
assert status.changed is True
item4 = status.listBox.topLevelItem(4)
assert item4 is not None
assert item4.text(0) == "New"
item5 = status.listBox.topLevelItem(5)
assert item5 is not None
assert item5.text(0) == "Note"
item6 = status.listBox.topLevelItem(6)
assert item6 is not None
assert item6.text(0) == "Draft"
item7 = status.listBox.topLevelItem(7)
assert item7 is not None
assert item7.text(0) == "Finished"
@pytest.mark.gui
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the auto-replace page of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(nwGUI, projPath)
# Set some values
project = SHARED.project
project.data.setAutoReplace({
"A": "B", "C": "D"
})
# Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
projSettings.show()
qtbot.addWidget(projSettings)
# Auto-Replace Tab
# ================
replace = projSettings.replacePage
assert replace.listBox.topLevelItem(0).text(0) == "<A>" # type: ignore
assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
assert replace.listBox.topLevelItem(1).text(0) == "<C>" # type: ignore
assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
assert replace.listBox.topLevelItemCount() == 2
# Nothing to save or delete
replace.listBox.clearSelection()
replace._onEntryDeleted()
assert replace.listBox.topLevelItemCount() == 2
# Create a new entry
qtbot.mouseClick(replace.addButton, QtMouseLeft)
assert replace.listBox.topLevelItemCount() == 3
assert replace.listBox.topLevelItem(2).text(0) == "<keyword3>" # type: ignore
assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore
# Edit the entry
replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2))
replace._onKeyEdit("Th is ")
replace._onValueEdit("With This Stuff ")
assert replace.listBox.topLevelItem(2).text(0) == "<This>" # type: ignore
assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
# Create a new entry again
replace.listBox.clearSelection()
qtbot.mouseClick(replace.addButton, QtMouseLeft)
assert replace.listBox.topLevelItemCount() == 4
# The list is sorted, so we must find it
newIdx = -1
for i in range(replace.listBox.topLevelItemCount()):
if replace.listBox.topLevelItem(i).text(0) == "<keyword4>": # type: ignore
newIdx = i
break
assert newIdx >= 0
# Then delete the new item
replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx))
qtbot.mouseClick(replace.delButton, QtMouseLeft)
assert replace.listBox.topLevelItemCount() == 3
# Check Project
projSettings._doSave()
assert project.data.autoReplace == {
"A": "B", "C": "D", "This": "With This Stuff"
}
# qtbot.stop()