Files
UmbraEdit/tests/dialogs/test_projectsettings.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

664 lines
24 KiB
Python

"""
UmbraEdit - Project Settings Dialog Tests
===========================================
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 datetime import date
from pathlib import Path
import pytest
from PyQt6.QtCore import QDate
from PyQt6.QtGui import QAction, QColor
from PyQt6.QtWidgets import QColorDialog, QFileDialog, QToolButton
from pytestqt.qtbot import QtBot
from umbraedit import CONFIG, SHARED
from umbraedit.core.project import NWProject
from umbraedit.core.spellcheck import SpellEnchant
from umbraedit.dialogs.editlabel import GuiEditLabel
from umbraedit.dialogs.projectsettings import GuiProjectSettings
from umbraedit.enum import nwItemType, nwStatusShape
from umbraedit.guimain import GuiMain
from umbraedit.types import QtAccepted, QtMouseLeft
from tests.helpers import C, MockRnd, buildTestProject, checkDialogFreedOnClose
from tests.mocked import causeOSError
KEY_DELAY = 1
@pytest.mark.gui
def testGuiProjectSettings_Dialog(qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, nwGUI: GuiMain):
"""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_GOALS).click()
assert projSettings.mainStack.currentWidget() == projSettings.goalsPage
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
# Selecting an unknown page does nothing
projSettings.sidebar.setSelected(9999)
# A button that isn't part of the sidebar's group is ignored
otherButton = QToolButton()
projSettings.sidebar._buttonClicked(otherButton)
assert projSettings.mainStack.currentWidget() == projSettings.replacePage
# Clean Up
projSettings.close()
# qtbot.stop()
@pytest.mark.gui
def testGuiProjectSettings_SettingsPage(
qtbot: QtBot,
monkeypatch: pytest.MonkeyPatch,
nwGUI: GuiMain,
fncPath: Path,
projPath: Path,
mockRnd: MockRnd,
):
"""Test the settings page of the dialog."""
languages = [("en", "English"), ("de", "German")]
monkeypatch.setattr(SpellEnchant, "listDictionaries", lambda *a: languages)
(fncPath / "ue_en.qm").touch()
(fncPath / "ue_de.qm").touch()
(fncPath / "project_en.json").touch()
(fncPath / "project_de.json").touch()
CONFIG._ueLangPath = fncPath
# Create new project
buildTestProject(NWProject(), projPath)
nwGUI.openProject(projPath)
mockRnd.reset()
CONFIG.setBackupPath(fncPath)
# Set some values
project = SHARED.project
project.data.setLanguage("en", False)
project.data.setSpellLang("en")
project.data.setAuthor("Jane Smith")
project.data.setAutoReplace({"A": "B", "C": "D"}, False)
# 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.isRTL.isChecked() is False
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.isRTL.setChecked(True)
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.isRTL is True
assert project.data.spellLang == "de"
assert project.data.doBackup is False
nwGUI._processProjectSettingsChanges()
assert nwGUI.windowTitle() == "Project Name - UmbraEdit"
# Without Enchant, only the default spell check entry is available
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "hasEnchant", False)
noEnchant = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS)
qtbot.addWidget(noEnchant)
assert noEnchant.settingsPage.spellLang.count() == 1
# qtbot.stop()
@pytest.mark.gui
def testGuiProjectSettings_GoalsPage(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
"""Test the goals page of the dialog."""
buildTestProject(NWProject(), projPath)
nwGUI.openProject(projPath)
project = SHARED.project
# Create Dialog
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_GOALS)
projSettings.show()
qtbot.addWidget(projSettings)
goals = projSettings.goalsPage
# No deadline is set by default, so the date field and the auto-goal
# switch must both start out disabled
assert goals.targetDeadlineEnabled.isChecked() is False
assert goals.targetDeadline.isEnabled() is False
assert goals.dailyGoalAuto.isEnabled() is False
# Enabling the deadline switch enables the date field and the auto-goal switch
goals.targetDeadlineEnabled.setChecked(True)
assert goals.targetDeadline.isEnabled() is True
assert goals.dailyGoalAuto.isEnabled() is True
# The count mode defaults to words, which is reflected in both the
# switch and the unit labels next to the target fields
assert goals.countCharacters.isChecked() is False
assert goals._editableUnit["targetCount"].text() == "words"
assert goals._editableUnit["dailyGoal"].text() == "words"
# Toggling the switch updates both unit labels to characters
goals.countCharacters.setChecked(True)
assert goals._editableUnit["targetCount"].text() == "characters"
assert goals._editableUnit["dailyGoal"].text() == "characters"
# Set some values and save
goals.targetCount.setValue(50000)
goals.targetDeadline.setDate(QDate(2030, 1, 1))
goals.dailyGoal.setValue(500)
goals.dailyGoalAuto.setChecked(True)
projSettings._doSave()
assert project.data.targetCount == 50000
assert project.data.targetCountChars is True
assert project.data.targetDeadline == date(2030, 1, 1)
assert project.data.dailyGoal == 500
assert project.data.dailyGoalAuto is True
# Disabling the deadline switch disables the date field and the
# auto-goal switch again, and clears the deadline on save
goals.targetDeadlineEnabled.setChecked(False)
assert goals.targetDeadline.isEnabled() is False
assert goals.dailyGoalAuto.isEnabled() is False
projSettings._doSave()
assert project.data.targetDeadline is None
# qtbot.stop()
@pytest.mark.gui
def testGuiProjectSettings_StatusImport(
qtbot: QtBot,
monkeypatch: pytest.MonkeyPatch,
nwGUI: GuiMain,
projPath: Path,
mockRnd: MockRnd,
):
"""Test the status and importance pages of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(NWProject(), projPath)
nwGUI.openProject(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 = "0000000000011"
hCharNote = "0000000000012"
hWorldNote = "0000000000013"
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]
# The Trash root, created when the project is opened, also counts
# towards the default "New" import flag
assert [e.count for _, e in project.data.itemImport.iterItems()] == [4, 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
# Nothing happens when no item is selected
status.listBox.clearSelection()
status.delButton.click()
assert status.listBox.topLevelItemCount() == 4
status._onNameEdit("Ignored")
# Cancelling the colour dialog does nothing
with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor())
status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
status.colorButton.click()
# 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()
shapeAction = QAction(status)
shapeAction.setData(nwStatusShape.CIRCLE)
status._shapeSelected(shapeAction)
assert status.listBox.topLevelItemCount() == 4
# Non-shape data is ignored
otherAction = QAction(status)
otherAction.setData("not-a-shape")
status._shapeSelected(otherAction)
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()
shapeAction = QAction(importance)
shapeAction.setData(nwStatusShape.TRIANGLE)
importance._shapeSelected(shapeAction)
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["s000014"].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["i000015"].name == "Final"
# qtbot.stop()
@pytest.mark.gui
def testGuiProjectSettings_StatusImportExport(
qtbot: QtBot,
monkeypatch: pytest.MonkeyPatch,
nwGUI: GuiMain,
projPath: Path,
mockRnd: MockRnd,
):
"""Test the status and importance import/export."""
buildTestProject(NWProject(), projPath)
nwGUI.openProject(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 Cancelled
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: ("", ""))
status._exportLabels()
assert expFile.is_file() is False
# Import Cancelled
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: ("", ""))
status._importLabels()
assert status.listBox.topLevelItemCount() == 4
# 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"
# Invalid rows in the import file are skipped
badFile = projPath / "status_bad.csv"
badFile.write_text("NOT_A_SHAPE,#000000,Bad\nSTAR,#123456,Good\n", encoding="utf-8")
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(badFile), ""))
status._importLabels()
assert status.listBox.topLevelItemCount() == 9
projSettings.close()
@pytest.mark.gui
def testGuiProjectSettings_Replace(
qtbot: QtBot,
monkeypatch: pytest.MonkeyPatch,
nwGUI: GuiMain,
projPath: Path,
mockRnd: MockRnd,
):
"""Test the auto-replace page of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(NWProject(), projPath)
nwGUI.openProject(projPath)
# Set some values
project = SHARED.project
project.data.setAutoReplace({"A": "B", "C": "D"}, False)
# 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
newItem = replace.listBox.topLevelItem(2)
replace.listBox.setCurrentItem(newItem)
replace._onKeyEdit("Th is ")
replace._onValueEdit("With This Stuff ")
assert newItem.text(0) == "<This>" # type: ignore
assert newItem.text(1) == "With This Stuff " # type: ignore
# A key that strips to empty is not applied
replace._onKeyEdit("!!!")
assert newItem.text(0) == "<This>" # type: ignore
# Nothing happens for key/value edits when no item is selected
replace.listBox.clearSelection()
replace._onKeyEdit("Other")
replace._onValueEdit("Other")
assert newItem.text(0) == "<This>" # type: ignore
assert newItem.text(1) == "With This Stuff " # type: ignore
# Items whose key strips to empty are skipped when building the new list
newItem.setText(0, "!!!") # type: ignore
assert "This" not in replace.getNewList()
newItem.setText(0, "<This>") # 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"}
assert project.data.autoReplaceIgnoreCase is False
# Toggling ignore case alone is saved
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
qtbot.addWidget(projSettings)
replace = projSettings.replacePage
assert replace.swtIgnoreCase.isChecked() is False
assert replace.changed is False
replace.swtIgnoreCase.setChecked(True)
assert replace.changed is True
projSettings._doSave()
assert project.data.autoReplace == {"A": "B", "C": "D", "This": "With This Stuff"}
assert project.data.autoReplaceIgnoreCase is True
# The saved state is loaded
projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
qtbot.addWidget(projSettings)
assert projSettings.replacePage.swtIgnoreCase.isChecked() is True
# qtbot.stop()
@pytest.mark.gui
def testGuiProjectSettings_MemoryLeakRegression(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
"""Test that the dialog is freed when it is closed."""
buildTestProject(NWProject(), projPath)
nwGUI.openProject(projPath)
checkDialogFreedOnClose(qtbot, lambda: GuiProjectSettings(nwGUI))