""" novelWriter – Manuscript Build Settings Dialog Tester ===================================================== This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen 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 . """ import pytest from tools import C, buildTestProject from pathlib import Path from pytestqt.qtbot import QtBot from PyQt5.QtGui import QFont from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog from novelwriter import CONFIG from novelwriter.guimain import GuiMain from novelwriter.constants import nwHeadFmt from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.tools.manussettings import ( GuiBuildSettings, _OutputTab, _FormatTab, _ContentTab, _HeadingsTab, _FilterTab ) @pytest.mark.gui def testBuildSettings_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd): """Test the initialisation of the GuiBuildSettings dialog.""" buildTestProject(nwGUI, projPath) nwGUI.openProject(projPath) build = BuildSettings() # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() # Flip through pages bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click() assert isinstance(bSettings.toolStack.currentWidget(), _OutputTab) bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click() assert isinstance(bSettings.toolStack.currentWidget(), _FormatTab) bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click() assert isinstance(bSettings.toolStack.currentWidget(), _ContentTab) bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click() assert isinstance(bSettings.toolStack.currentWidget(), _HeadingsTab) bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click() assert isinstance(bSettings.toolStack.currentWidget(), _FilterTab) # Check dialog buttons triggered = False @pyqtSlot(BuildSettings) def _testNewSettingsReady(new: BuildSettings): nonlocal build, triggered assert new is build triggered = True # Capture Apply button with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Apply)) assert triggered # Capture Save button triggered = False with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Save)) assert triggered # Close manually # This pops the ask to save dialog, which is automatically handled triggered = False build._changed = True bSettings.show() with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) bSettings.close() assert triggered # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Init @pytest.mark.gui def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd): """Test the Filter Tab of the GuiBuildSettings dialog.""" buildTestProject(nwGUI, projPath) nwGUI.openProject(projPath) build = BuildSettings() switchMap = { "incNovel": 1, "incNotes": 2, "incInactive": 3, "novelRoot": 6, "plotRoot": 7, "charRoot": 8, "worldRoot": 9, } hPlotDoc = nwGUI.theProject.newFile("Main Plot", C.hPlotRoot) hCharDoc = nwGUI.theProject.newFile("Jane Doe", C.hCharRoot) nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc) nwGUI.projView.projTree.revealNewTreeItem(hCharDoc) nwGUI.theProject.tree[hPlotDoc].setActive(False) # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() filterTab = bSettings.optTabSelect bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click() assert bSettings.toolStack.currentWidget() is filterTab # Check content assert filterTab.optTree.topLevelItemCount() == 4 # The 4 root folders assert filterTab.filterOpt._index == 10 # 2 headers, 1 sep, 3 opt and 4 roots # Untoggle note folders filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root assert filterTab.optTree.topLevelItemCount() == 3 assert C.hWorldRoot in build._skipRoot filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(False) # Char Root assert filterTab.optTree.topLevelItemCount() == 2 assert C.hCharRoot in build._skipRoot filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(False) # Plot Root assert filterTab.optTree.topLevelItemCount() == 1 assert C.hPlotRoot in build._skipRoot # Reset Plot and Char filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(True) filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(True) assert filterTab.optTree.topLevelItemCount() == 3 # Switch off novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False) assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (False, FilterMode.FILTERED), C.hSceneDoc: (False, FilterMode.FILTERED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (False, FilterMode.FILTERED), C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (False, FilterMode.FILTERED), C.hWorldRoot: (False, FilterMode.SKIPPED), } # Switch on note docs filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True) assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (False, FilterMode.FILTERED), C.hSceneDoc: (False, FilterMode.FILTERED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (False, FilterMode.FILTERED), # Set to inactive C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (True, FilterMode.FILTERED), # Now enabled C.hWorldRoot: (False, FilterMode.SKIPPED), } # Switch on inactive docs filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True) assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (False, FilterMode.FILTERED), C.hSceneDoc: (False, FilterMode.FILTERED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (True, FilterMode.FILTERED), # Now enabled C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (True, FilterMode.FILTERED), C.hWorldRoot: (False, FilterMode.SKIPPED), } # Set chapter and scene docs to included filterTab._treeMap[C.hChapterDoc].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab.includedButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (True, FilterMode.INCLUDED), # Now included C.hSceneDoc: (True, FilterMode.INCLUDED), # Now included C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (True, FilterMode.FILTERED), C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (True, FilterMode.FILTERED), C.hWorldRoot: (False, FilterMode.SKIPPED), } # Set char and plot docs to excluded filterTab.optTree.clearSelection() filterTab._treeMap[hPlotDoc].setSelected(True) filterTab._treeMap[hCharDoc].setSelected(True) filterTab.excludedButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (True, FilterMode.INCLUDED), C.hSceneDoc: (True, FilterMode.INCLUDED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (False, FilterMode.EXCLUDED), # Now excluded C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (False, FilterMode.EXCLUDED), # Now excluded C.hWorldRoot: (False, FilterMode.SKIPPED), } # Switch on novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True) assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (True, FilterMode.INCLUDED), C.hSceneDoc: (True, FilterMode.INCLUDED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (False, FilterMode.EXCLUDED), C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (False, FilterMode.EXCLUDED), C.hWorldRoot: (False, FilterMode.SKIPPED), } # Set everything back to filtered filterTab.optTree.clearSelection() filterTab._treeMap[C.hChapterDoc].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab._treeMap[hPlotDoc].setSelected(True) filterTab._treeMap[hCharDoc].setSelected(True) filterTab.filteredButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDoc: (True, FilterMode.FILTERED), C.hSceneDoc: (True, FilterMode.FILTERED), C.hPlotRoot: (False, FilterMode.SKIPPED), hPlotDoc: (True, FilterMode.FILTERED), C.hCharRoot: (False, FilterMode.SKIPPED), hCharDoc: (True, FilterMode.FILTERED), C.hWorldRoot: (False, FilterMode.SKIPPED), } # Check handling of invalid project items assert list(filterTab._treeMap.keys()) == [ C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, ] nwGUI.theProject.tree[hCharDoc].setRoot(None) # Char doc has no root handle nwGUI.theProject.tree[hPlotDoc].setParent(None) # Plot doc has no parent handle filterTab._populateTree() assert list(filterTab._treeMap.keys()) == [ C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hPlotRoot, C.hCharRoot ] # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Filter @pytest.mark.gui def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain): """Test the Headings Tab of the GuiBuildSettings dialog.""" build = BuildSettings() ttTitle = f"Title: {nwHeadFmt.TITLE}" chTitle = f"Chapter: {nwHeadFmt.TITLE}" unTitle = f"Interlude: {nwHeadFmt.TITLE}" scTitle = f"Scene: {nwHeadFmt.TITLE}" sxTitle = f"Section: {nwHeadFmt.TITLE}" build.setValue("headings.fmtTitle", ttTitle) build.setValue("headings.fmtChapter", chTitle) build.setValue("headings.fmtUnnumbered", unTitle) build.setValue("headings.fmtScene", scTitle) build.setValue("headings.fmtSection", sxTitle) build.setValue("headings.hideScene", False) build.setValue("headings.hideSection", False) # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() headTab = bSettings.optTabHeadings bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click() assert bSettings.toolStack.currentWidget() is headTab # Check initial values assert headTab.fmtTitle.text() == ttTitle assert headTab.fmtChapter.text() == chTitle assert headTab.fmtUnnumbered.text() == unTitle assert headTab.fmtScene.text() == scTitle assert headTab.fmtSection.text() == sxTitle assert headTab.swtScene.isChecked() is False assert headTab.swtSection.isChecked() is False # Edit in Turn # ============ # Nothing to apply assert headTab._editing == 0 assert headTab.editTextBox.isEnabled() is False headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Title headTab.btnTitle.click() assert headTab._editing == headTab.EDIT_TITLE assert headTab.editTextBox.isEnabled() is True assert headTab.editTextBox.toPlainText() == ttTitle headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Chapter headTab.btnChapter.click() assert headTab._editing == headTab.EDIT_CHAPTER assert headTab.editTextBox.isEnabled() is True assert headTab.editTextBox.toPlainText() == chTitle headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Unnumbered headTab.btnUnnumbered.click() assert headTab._editing == headTab.EDIT_UNNUM assert headTab.editTextBox.isEnabled() is True assert headTab.editTextBox.toPlainText() == unTitle headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Scene headTab.btnScene.click() assert headTab._editing == headTab.EDIT_SCENE assert headTab.editTextBox.isEnabled() is True assert headTab.editTextBox.toPlainText() == scTitle headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Section headTab.btnSection.click() assert headTab._editing == headTab.EDIT_SECTION assert headTab.editTextBox.isEnabled() is True assert headTab.editTextBox.toPlainText() == sxTitle headTab.btnApply.click() assert headTab.editTextBox.isEnabled() is False # Edit a Heading # ============== # Create new format of all bits headTab.btnChapter.click() allFmt = ( f"{nwHeadFmt.TITLE}{nwHeadFmt.CH_NUM}{nwHeadFmt.CH_WORD}{nwHeadFmt.CH_ROMU}" f"{nwHeadFmt.CH_ROML}{nwHeadFmt.SC_NUM}{nwHeadFmt.SC_ABS}" ) headTab.editTextBox.clear() assert headTab.editTextBox.toPlainText() == "" headTab.aInsTitle.trigger() headTab.aInsChNum.trigger() headTab.aInsChWord.trigger() headTab.aInsChRomU.trigger() headTab.aInsChRomL.trigger() headTab.aInsScNum.trigger() headTab.aInsScAbs.trigger() assert headTab.editTextBox.toPlainText() == allFmt headTab.btnApply.click() assert build.getStr("headings.fmtChapter") == allFmt # Check complex format headTab.btnChapter.click() headTab.editTextBox.setPlainText(f"Chapter {nwHeadFmt.CH_NUM}\n{nwHeadFmt.TITLE}\n") headTab.btnApply.click() assert build.getStr("headings.fmtChapter") == f"Chapter {nwHeadFmt.CH_NUM}//{nwHeadFmt.TITLE}" # Set all to plain title headTab.btnTitle.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() assert build.getStr("headings.fmtTitle") == nwHeadFmt.TITLE headTab.btnChapter.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() assert build.getStr("headings.fmtChapter") == nwHeadFmt.TITLE headTab.btnUnnumbered.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() assert build.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE headTab.btnScene.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE headTab.btnSection.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() assert build.getStr("headings.fmtSection") == nwHeadFmt.TITLE # Check hide switches headTab.swtScene.setChecked(True) headTab.swtSection.setChecked(True) headTab.saveContent() assert build.getBool("headings.hideScene") is True assert build.getBool("headings.hideSection") is True # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Headings @pytest.mark.gui def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain): """Test the Content Tab of the GuiBuildSettings dialog.""" build = BuildSettings() build.setValue("text.includeSynopsis", False) build.setValue("text.includeComments", False) build.setValue("text.includeKeywords", False) build.setValue("text.includeBodyText", False) build.setValue("text.addNoteHeadings", False) # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() contTab = bSettings.optTabContent bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click() assert bSettings.toolStack.currentWidget() is contTab # Check initial values assert contTab.incSynopsis.isChecked() is False assert contTab.incComments.isChecked() is False assert contTab.incKeywords.isChecked() is False assert contTab.incBodyText.isChecked() is False assert contTab.addNoteHead.isChecked() is False # Toggle all contTab.incSynopsis.setChecked(True) contTab.incComments.setChecked(True) contTab.incKeywords.setChecked(True) contTab.incBodyText.setChecked(True) contTab.addNoteHead.setChecked(True) # Save values contTab.saveContent() assert build.getBool("text.includeSynopsis") is True assert build.getBool("text.includeComments") is True assert build.getBool("text.includeKeywords") is True assert build.getBool("text.includeBodyText") is True assert build.getBool("text.addNoteHeadings") is True # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Content @pytest.mark.gui def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): """Test the Format Tab of the GuiBuildSettings dialog.""" build = BuildSettings() textFont = str(CONFIG.textFont) build.setValue("format.buildLang", "en_US") build.setValue("format.textFont", "") # Will fall back to config value build.setValue("format.textSize", 12) build.setValue("format.lineHeight", 1.2) build.setValue("format.justifyText", False) build.setValue("format.stripUnicode", False) build.setValue("format.replaceTabs", False) # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() fmtTab = bSettings.optTabFormat bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click() assert bSettings.toolStack.currentWidget() is fmtTab # Check initial values assert fmtTab.buildLang.currentData() == "en_US" assert fmtTab.textFont.text() == textFont assert fmtTab.textSize.value() == 12 assert fmtTab.lineHeight.value() == 1.2 assert fmtTab.justifyText.isChecked() is False assert fmtTab.stripUnicode.isChecked() is False assert fmtTab.replaceTabs.isChecked() is False # Change values fmtTab.buildLang.setCurrentIndex(fmtTab.buildLang.findData("en_GB")) fmtTab.textFont.setText("Arial") fmtTab.textSize.setValue(11) fmtTab.lineHeight.setValue(1.15) fmtTab.justifyText.setChecked(True) fmtTab.stripUnicode.setChecked(True) fmtTab.replaceTabs.setChecked(True) # Save values fmtTab.saveContent() assert build.getStr("format.buildLang") == "en_GB" assert build.getStr("format.textFont") == "Arial" assert build.getInt("format.textSize") == 11 assert build.getFloat("format.lineHeight") == 1.15 assert build.getBool("format.justifyText") is True assert build.getBool("format.stripUnicode") is True assert build.getBool("format.replaceTabs") is True # Check that the font dialog doesn't fail with monkeypatch.context() as mp: font = QFont() font.setFamily("Times") font.setPointSize(10) mp.setattr(QFontDialog, "getFont", lambda *a: (font, True)) fmtTab.btnTextFont.click() assert fmtTab.textFont.text() == "Times" assert fmtTab.textSize.value() == 10 # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Format @pytest.mark.gui def testBuildSettings_Output(qtbot: QtBot, nwGUI: GuiMain): """Test the Output Tab of the GuiBuildSettings dialog.""" build = BuildSettings() build.setValue("odt.addColours", False) build.setValue("html.addStyles", False) # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) bSettings.show() bSettings.loadContent() outTab = bSettings.optTabOutput bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click() assert bSettings.toolStack.currentWidget() is outTab # Check initial values assert outTab.odtAddColours.isChecked() is False assert outTab.htmlAddStyles.isChecked() is False # Toggle all outTab.odtAddColours.setChecked(True) outTab.htmlAddStyles.setChecked(True) # Save values outTab.saveContent() assert build.getBool("odt.addColours") is True assert build.getBool("html.addStyles") is True # Finish bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) # qtbot.stop() # END Test testBuildSettings_Output