""" novelWriter – Main GUI Editor Class Tester ========================================== This file is a part of novelWriter Copyright 2018–2024, 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 . """ from __future__ import annotations import sys import pytest from shutil import copyfile from tools import C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMenu, QInputDialog from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.tools.welcome import GuiWelcome from novelwriter.dialogs.editlabel import GuiEditLabel KEY_DELAY = 1 @pytest.mark.gui def testGuiMain_ProjectBlocker(nwGUI): """Test the blocking of features when there's no project open.""" # Test no-project blocking assert nwGUI.closeProject() is True assert nwGUI.saveProject() is False assert nwGUI.openDocument(None) is False assert nwGUI.openNextDocument(None) is False assert nwGUI.viewDocument(None) is False assert nwGUI.importDocument() is False # END Test testGuiMain_ProjectBlocker @pytest.mark.gui def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath): """Test the handling of launch tasks.""" monkeypatch.setattr(GuiWelcome, "exec_", lambda *a: None) CONFIG.lastNotes = "0x0" buildTestProject(nwGUI, projPath) # Open Lipsum project nwGUI.postLaunchTasks(projPath) nwGUI.closeProject() # Project open fails with monkeypatch.context() as mp: mp.setattr(SHARED, "openProject", lambda *a: False) assert nwGUI.openProject(projPath) is False # Handle locked project with monkeypatch.context() as mp: mp.setattr(SHARED, "openProject", lambda *a, **k: False) SHARED._lockedBy = ["a", "b", "c", "d"] assert nwGUI.openProject(projPath) is False SHARED._lockedBy = None assert nwGUI.openProject(projPath) is True nwGUI.closeProject() # Check that latest release info updated CONFIG.lastNotes != "0x0" # Check that project open dialog launches nwGUI.postLaunchTasks(None) qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiWelcome) is not None, timeout=1000) assert isinstance(welcome := SHARED.findTopLevelWidget(GuiWelcome), GuiWelcome) welcome.show() welcome.close() # qtbot.stop() # END Test testGuiMain_Launch @pytest.mark.gui def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test handling of project tree items based on GUI focus states.""" buildTestProject(nwGUI, projPath) sHandle = "000000000000f" nwGUI.openSelectedItem() assert nwGUI.docEditor.docHandle is None # Project Tree has focus nwGUI._changeView(nwView.PROJECT) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle is None nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle == sHandle nwGUI.closeDocument() # Novel Tree has focus nwGUI._changeView(nwView.NOVEL) nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle is None selItem = nwGUI.novelView.novelTree.topLevelItem(2) nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle == sHandle nwGUI.closeDocument() # Project Outline has focus nwGUI._changeView(nwView.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle is None selItem = nwGUI.outlineView.outlineTree.topLevelItem(2) nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle == sHandle nwGUI.closeDocument() # qtbot.stop() # END Test testGuiMain_ProjectTreeItems @pytest.mark.gui def testGuiMain_UpdateTheme(qtbot, nwGUI): """Test updating the theme in the GUI.""" mainTheme = SHARED.theme CONFIG.guiTheme = "default_dark" CONFIG.guiSyntax = "default_dark" mainTheme.loadTheme() mainTheme.loadSyntax() nwGUI._processConfigChanges(True, True, True, True) syntaxBack = SHARED.theme.colBack assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack # qtbot.stop() # END Test testGuiMain_UpdateTheme @pytest.mark.gui def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): """Test the document editor.""" monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create new, save, close project buildTestProject(nwGUI, projPath) assert nwGUI.saveProject() assert nwGUI.closeProject() assert len(SHARED.project.tree) == 0 assert len(SHARED.project.tree._order) == 0 assert len(SHARED.project.tree._roots) == 0 assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "" assert SHARED.project.data.author == "" assert SHARED.project.data.spellCheck is False # Check the files projFile = projPath / "nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Initial_nwProject.nwx" compFile = tstPaths.refDir / "guiEditor_Main_Initial_nwProject.nwx" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Re-open project assert nwGUI.openProject(projPath) # Check that we loaded the data assert len(SHARED.project.tree) == 8 assert len(SHARED.project.tree._order) == 8 assert len(SHARED.project.tree._roots) == 4 assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "New Project" assert SHARED.project.data.author == "Jane Doe" assert SHARED.project.data.spellCheck is False # Check that tree items have been created assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None assert nwGUI.projView.projTree._getTreeItem(C.hPlotRoot) is not None assert nwGUI.projView.projTree._getTreeItem(C.hCharRoot) is not None assert nwGUI.projView.projTree._getTreeItem(C.hWorldRoot) is not None assert nwGUI.projView.projTree._getTreeItem(C.hTitlePage) is not None assert nwGUI.projView.projTree._getTreeItem(C.hChapterDir) is not None assert nwGUI.projView.projTree._getTreeItem(C.hChapterDoc) is not None assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) nwGUI.mainMenu._toggleSpellCheck() # Change some settings CONFIG.hideHScroll = True CONFIG.hideVScroll = True CONFIG.autoScrollPos = 80 CONFIG.autoScroll = True # Add a Character File nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.openSelectedItem() # Text Editor # =========== docEditor: GuiDocEditor = nwGUI.docEditor # Type something into the document nwGUI.switchFocus(nwWidget.EDITOR) qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY) for c in "# Jane Doe": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@tag: Jane": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "This is a file about Jane.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.openSelectedItem() # Type something into the document nwGUI.switchFocus(nwWidget.EDITOR) qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY) for c in "# Main Plot": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@tag: MainPlot": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "This is a file detailing the main plot.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Add a World File nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.openSelectedItem() # Add Some Text docEditor.replaceText("Hello World!") assert docEditor.getText() == "Hello World!" docEditor.replaceText("") # Type something into the document nwGUI.switchFocus(nwWidget.EDITOR) qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY) for c in "# Main Location": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@tag: Home": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "This is a file describing Jane's home.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Trigger autosaves before making more changes nwGUI._autoSaveDocument() nwGUI._autoSaveProject() # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True) nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True) nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True) nwGUI.openSelectedItem() # Type something into the document nwGUI.switchFocus(nwWidget.EDITOR) qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY) for c in "# Novel": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "## Chapter": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@pov: Jane": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@plot: MainPlot": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "### Scene": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "% How about a comment?": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@pov: Jane": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@plot: MainPlot": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@location: Home": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "#### Some Section": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "@char: Jane": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "This is a paragraph of nonsense text.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Don't allow Shift+Enter to insert a line separator (issue #1150) for c in "This is another paragraph": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY) for c in "with a line separator in it.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Auto-Replace # ============ for c in ( "This is another paragraph of much longer nonsense text. " "It is in fact 1 very very NONSENSICAL nonsense text! " ): qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "Isn't that nice? ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "Ellipsis? Not a problem either ... ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "How about three hyphens - -": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Left, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Right, delay=KEY_DELAY) for c in "- for long dash? It works too.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "\"Full line double quoted text.\"": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "'Full line single quoted text.'": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) # Insert spaces before and after quotes docEditor._typPadBefore = "\u201d" docEditor._typPadAfter = "\u201c" for c in "Some \"double quoted text with spaces padded\".": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) docEditor._typPadBefore = "" docEditor._typPadAfter = "" # Insert spaces before colon, but ignore tags and synopsis docEditor._typPadBefore = ":" for c in "@object: NoSpaceAdded": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "% synopsis: No space before this colon.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "Add space before this colon: See?": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "But don't add a double space : See?": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) docEditor._typPadBefore = "" # Indent and Align # ================ for c in "\t\"Tab-indented text\"": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in ">\"Paragraph-indented text\"": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in ">>\"Right-aligned text\"": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in "\t'Tab-indented text'": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in ">'Paragraph-indented text'": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) for c in ">>'Right-aligned text'": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) docEditor.wCounterDoc.run() # Spell Checking # ============== for c in "Some text with tesst in it.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) currPos = docEditor.getCursorPosition() assert docEditor._qDocument.spellErrorAtPos(currPos) == ("", -1, -1, []) errPos = currPos - 13 if not sys.platform.startswith("win32"): # Skip on Windows as spell checking is off there # This check will fail without an 'en' dictionary, like aspell-en word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos) assert word == "tesst" assert cPos == 15 assert cLen == 5 assert "test" in suggest with monkeypatch.context() as mp: mp.setattr(QMenu, "exec_", lambda *a: None) docEditor.setCursorPosition(errPos) docEditor._openContextFromCursor() # Check Files # =========== # Save the document assert docEditor.docChanged nwGUI.saveDocument() assert docEditor.docChanged is False nwGUI.rebuildIndex() # Open and view the edited document nwGUI.switchFocus(nwWidget.VIEWER) assert nwGUI.openDocument(C.hSceneDoc) assert nwGUI.viewDocument(C.hSceneDoc) assert nwGUI.saveProject() assert nwGUI.closeViewerPanel() # Check the files projFile = projPath / "nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx" compFile = tstPaths.refDir / "guiEditor_Main_Final_nwProject.nwx" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "